Dlaczego odradza się akceptowanie odwołania do String (& String), Vec (& Vec) lub Box (& Box) jako argumentu funkcji?

127

Napisałem kod Rusta, który przyjmuje &Stringjako argument:

fn awesome_greeting(name: &String) {
    println!("Wow, you are awesome, {}!", name);
}

Napisałem również kod, który zawiera odniesienie do Veclub Box:

fn total_price(prices: &Vec<i32>) -> i32 {
    prices.iter().sum()
}

fn is_even(value: &Box<i32>) -> bool {
    **value % 2 == 0
}

Otrzymałem jednak opinie, że zrobienie tego w ten sposób nie jest dobrym pomysłem. Dlaczego nie?

Shepmaster
źródło

Odpowiedzi:

162

TL; DR: Można zamiast tego użyć &str, &[T]lub &Tw celu umożliwienia kod bardziej ogólny.


  1. Jednym z głównych powodów używania a Stringlub a Vecjest to, że pozwalają one zwiększyć lub zmniejszyć pojemność. Jeśli jednak zaakceptujesz niezmienne odwołanie, nie możesz użyć żadnej z tych interesujących metod na platformie Veclub String.

  2. Przyjmowanie &String, &Vecczy &Boxteż wymaga argumentu, która ma zostać przydzielona na stercie, zanim będzie można wywołać funkcję. Zaakceptowanie a &strzezwala na literał łańcuchowy (zapisany w danych programu) i akceptuje &[T]lub &Tzezwala na tablicę lub zmienną przydzieloną stosem. Niepotrzebna alokacja to utrata wydajności. Zwykle jest to widoczne od razu, gdy próbujesz wywołać te metody w teście lub mainmetodzie:

    awesome_greeting(&String::from("Anna"));
    total_price(&vec![42, 13, 1337])
    is_even(&Box::new(42))
  3. Inną kwestią związaną z wydajnością jest to &String, &Veci &Boxwprowadź niepotrzebną warstwę pośrednią, ponieważ musisz wyłuskać, &Stringaby uzyskać a, Stringa następnie wykonać drugą dereferencję, aby zakończyć &str.

Zamiast tego powinieneś zaakceptować kawałek ciągu ( &str), kawałek ( &[T]) lub po prostu odwołanie ( &T). A &String, &Vec<T>lub &Box<T>zostanie automatycznie wymuszona odpowiednio na a &str, &[T]lub &T.

fn awesome_greeting(name: &str) {
    println!("Wow, you are awesome, {}!", name);
}
fn total_price(prices: &[i32]) -> i32 {
    prices.iter().sum()
}
fn is_even(value: &i32) -> bool {
    *value % 2 == 0
}

Teraz możesz wywoływać te metody z szerszym zestawem typów. Na przykład awesome_greetingmożna wywołać ciąg literal ( "Anna") lub zaalokować String. total_pricemożna wywołać z odwołaniem do tablicy ( &[1, 2, 3]) lub zaalokowaną Vec.


Jeśli chcesz dodać lub usunąć elementy z Stringlub Vec<T>, możesz wziąć zmienne odwołanie ( &mut Stringlub &mut Vec<T>):

fn add_greeting_target(greeting: &mut String) {
    greeting.push_str("world!");
}
fn add_candy_prices(prices: &mut Vec<i32>) {
    prices.push(5);
    prices.push(25);
}

W szczególności w przypadku plasterków możesz również zaakceptować &mut [T]lub &mut str. Pozwala to na zmianę określonej wartości wewnątrz plasterka, ale nie można zmienić liczby elementów wewnątrz plasterka (co oznacza, że ​​jest to bardzo ograniczone do ciągów znaków):

fn reset_first_price(prices: &mut [i32]) {
    prices[0] = 0;
}
fn lowercase_first_ascii_character(s: &mut str) {
    if let Some(f) = s.get_mut(0..1) {
        f.make_ascii_lowercase();
    }
}
Shepmaster
źródło
5
A co powiesz na tl; dr na początku? Ta odpowiedź jest już dość długa. Coś w stylu „ &strjest bardziej ogólne (np. Nakłada mniej ograniczeń) bez ograniczonych możliwości”? Ponadto: myślę, że punkt 3 często nie jest aż tak ważny. Zazwyczaj Vecs i Strings będą znajdować się na stosie, a często nawet w pobliżu bieżącej ramki stosu. Stos jest zwykle gorący, a dereferencja będzie obsługiwana z pamięci podręcznej procesora.
Lukas Kalbertodt
3
@Shepmaster: Jeśli chodzi o koszt alokacji, warto wspomnieć o konkretnej kwestii podciągów / wycinków, gdy mówimy o obowiązkowej alokacji. total_price(&prices[0..4])nie wymaga przydzielania nowego wektora dla wycinka.
Matthieu M.
4
To świetna odpowiedź. Dopiero zaczynam pracę w Rust i zaczynam zastanawiać się, kiedy powinienem użyć a &stri dlaczego (pochodzi z Pythona, więc zwykle nie zajmuję się bezpośrednio typami). Wszystko to
wyjaśniło
2
Niesamowite wskazówki dotyczące parametrów. Wystarczy jedna wątpliwość: „Zaakceptowanie & String, & Vec lub & Box również wymaga alokacji, zanim będzie można wywołać metodę.” ... Dlaczego tak jest? Czy mógłbyś wskazać część w dokumentach, w której mogę przeczytać to szczegółowo? (Jestem początkującym). Czy możemy mieć podobne wskazówki dotyczące typów zwrotów?
Nawaz
2
Brakuje mi informacji, dlaczego wymagana jest dodatkowa alokacja. Ciąg jest przechowywany na stercie, kiedy przyjmuje & String jako argument, dlaczego Rust nie przekazuje po prostu wskaźnika przechowywanego na stosie, który wskazuje na przestrzeń sterty, nie rozumiem, dlaczego przekazanie & String wymagałoby dodatkowej alokacji, przekazując ciąg plasterek powinien również wymagać wysłania wskaźnika przechowywanego na stosie, który wskazuje na przestrzeń sterty?
cjohansson
22

Oprócz odpowiedzi Shepmaster jest kolejnym powodem do zaakceptowania &str(i podobnie &[T]etc) to z powodu wszystkich innych typów oprócz String i &strże również spełniać Deref<Target = str>. Jednym z najbardziej godnych uwagi przykładów jest to Cow<str>, że pozwala bardzo elastycznie określić, czy masz do czynienia z danymi posiadanymi, czy pożyczonymi.

Jeśli masz:

fn awesome_greeting(name: &String) {
    println!("Wow, you are awesome, {}!", name);
}

Ale musisz to nazwać Cow<str>, musisz to zrobić:

let c: Cow<str> = Cow::from("hello");
// Allocate an owned String from a str reference and then makes a reference to it anyway!
awesome_greeting(&c.to_string());

Gdy zmienisz typ argumentu na &str, możesz używać go Cowpłynnie, bez zbędnego przydziału, tak jak w przypadku String:

let c: Cow<str> = Cow::from("hello");
// Just pass the same reference along
awesome_greeting(&c);

let c: Cow<str> = Cow::from(String::from("hello"));
// Pass a reference to the owned string that you already have
awesome_greeting(&c);

Zaakceptowanie &strsprawia, że ​​wywoływanie funkcji jest bardziej jednolite i wygodne, a „najłatwiejszy” sposób jest teraz również najbardziej efektywny. Te przykłady będą również działać z Cow<[T]>itp.

Peter Hall
źródło