Чому не рекомендується приймати посилання на String (& String), Vec (& Vec) або Box (& Box) як аргумент функції?


127

Я написав код Rust, який бере &Stringаргумент:

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

Я також написав код, який посилається на Vecабо Box:

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

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

Однак я отримав певний відгук про те, що робити це так не дуже добре. Чому ні?

Відповіді:


162

TL; DR: замість того, щоб можна використовувати &str, &[T]або &Tдля забезпечення більш загального коду.


  1. Однією з головних причин використання a Stringабо a Vecє те, що вони дозволяють збільшити або зменшити ємність. Однак, приймаючи незмінні посилання, ви не можете використовувати жоден із цих цікавих методів на Vecабо String.

  2. Прийом &String, &Vecабо &Boxж вимагає аргумент , щоб бути виділена в купі , перш ніж ви можете викликати функцію. Прийняття дозволу &strдає рядковий літерал (збережений у програмі) та приймає &[T]або &Tдозволяє масив або змінну, виділену стеком. Непотрібний розподіл - це втрата продуктивності. Зазвичай це виявляється відразу, коли ви намагаєтеся викликати ці методи в тесті або mainметоді:

    awesome_greeting(&String::from("Anna"));
    total_price(&vec![42, 13, 1337])
    is_even(&Box::new(42))
  3. Ще один розгляд продуктивності полягає в тому &String, що &Vecі &Boxвведіть непотрібний шар непрямості, оскільки вам доведеться знеструмити значення, &Stringщоб отримати а, Stringа потім виконати другий відвід, щоб закінчитися в &str.

Натомість слід прийняти рядковий фрагмент ( &str), фрагмент ( &[T]) або просто посилання ( &T). A &String, &Vec<T>або &Box<T>буде автоматично примусово до a &str, &[T]або &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
}

Тепер ви можете викликати ці методи ширшим набором типів. Наприклад, awesome_greetingможна викликати рядковим літералом ( "Anna") або виділеним String. total_priceможна назвати з посиланням на масив ( &[1, 2, 3]) або виділений Vec.


Якщо ви хочете додати або видалити елементи з Stringабо Vec<T>, ви можете скористатися змінною посиланням ( &mut Stringабо &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);
}

Спеціально для скибочок ви також можете прийняти &mut [T]або &mut str. Це дозволяє мутувати певне значення всередині фрагмента, але ви не можете змінити кількість елементів всередині фрагмента (це означає, що це дуже обмежено для рядків):

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();
    }
}

5
Як щодо tl; dr на початку? Ця відповідь вже дещо довга. Щось на кшталт " &strбільш загальне (як у: накладає менше обмежень) без знижених можливостей"? Також пункт 3 часто не є таким важливим, як я думаю. Зазвичай Vecs і Strings житимуть у стеці, а часто навіть десь біля поточного кадру стека. Стек зазвичай гарячий, і дереференція подаватиметься з кешу CPU.
Лукас Калбертодт

3
@Shepmaster: Стосовно вартості розподілу, можливо, варто згадати особливу проблему підрядків / фрагментів, коли мова йде про обов'язкове розподіл. total_price(&prices[0..4])не вимагає виділення нового вектора для зрізу.
Матьє М.

4
Це чудова відповідь. Я тільки почав працювати в Rust і був пов'язаний, щоб зрозуміти, коли я повинен використовувати a &strі чому (що надходить з Python, тому я, як правило, явно не маю справу з типами). Прочистили все це ідеально
C.Nivs

2
Дивовижні поради щодо параметрів. Потрібно лише одне сумнів: "Прийняття & String, & Vec або & Box також вимагає виділення, перш ніж ви зможете викликати метод." ... Чому це так? Не могли б ви вказати частину в документах, де я можу це детально прочитати? (Я жебрачок). Також, чи можемо мати подібні поради щодо типів повернення?
Наваз

2
Мені не вистачає інформації про те, чому потрібен додатковий розподіл. Рядок зберігається в купі, коли приймає & String як аргумент, чому просто Руст не передає покажчик, збережений на стеку, який вказує на пробіл купи, я не розумію, чому для передачі & String потрібно буде додаткове виділення, передаючи рядок фрагмент також повинен вимагати надсилання покажчика, збереженого на стеку, який вказує на кучу простору?
cjohansson

22

На додаток до відповіді Shepmaster в , ще одна причина , щоб прийняти &str(а так само і &[T]т.д.) з - за всіх інших типів , крім String і &strякі також задовольняють Deref<Target = str>. Один з найпомітніших прикладів - Cow<str>це ви можете бути дуже гнучкими щодо того, чи маєте ви справу з власними або запозиченими даними.

Якщо у вас є:

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

Але вам потрібно зателефонувати до нього Cow<str>, вам доведеться це зробити:

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());

Змінюючи тип аргументу на &str, ви можете використовувати Cowбезперешкодно, без зайвого виділення, як і 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);

Прийняття &strробить виклик вашої функції більш рівномірним та зручним, а "найпростіший" спосіб тепер є також найефективнішим. Ці приклади також будуть працювати з Cow<[T]>і т.д.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.