Почему не рекомендуется принимать ссылку на String (& String), Vec (& Vec) или Box (& Box) в качестве аргумента функции?

128

Я написал код на 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
}

Однако я получил некоторые отзывы о том, что делать это таким образом - не очень хорошая идея. Почему нет?

Shepmaster
источник

Ответы:

163

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


  1. Одна из основных причин использования a Stringили a Vecзаключается в том, что они позволяют увеличивать или уменьшать емкость. Однако, если вы принимаете неизменяемую ссылку, вы не можете использовать какие-либо из этих интересных методов в Vecили String.

  2. Прием &String, &Vecили &Boxже требует аргумент , чтобы быть выделена в куче , прежде чем вы можете вызвать функцию. Принятие a &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.

Вместо этого вы должны принять строку slice ( &str), slice ( &[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может вызываться со ссылкой на array ( &[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();
    }
}
Shepmaster
источник
5
Как насчет tl; dr в начале? Этот ответ уже довольно длинный. Что-то вроде « &strявляется более общим (например, накладывает меньше ограничений) без ограниченных возможностей»? Также: я думаю, пункт 3 часто не так важен. Обычно Vecs и Strings будут жить в стеке и часто даже где-то рядом с текущим кадром стека. Стек обычно горячий, и разыменование будет выполняться из кеша процессора.
Лукас Калбертодт,
3
@Shepmaster: Что касается стоимости распределения, возможно, стоит упомянуть конкретную проблему подстрок / срезов, когда речь идет об обязательном распределении. total_price(&prices[0..4])не требует выделения нового вектора для среза.
Matthieu M.
4
Это отличный ответ. Я только начинаю работать с Rust и занимаюсь выяснением того, когда мне следует использовать a &strи почему (исходящий из Python, поэтому я обычно не имею дела с типами явно). Всё
прояснилось
2
Замечательные советы по параметрам. Просто нужно одно сомнение: «Принятие & String, & Vec или & Box также требует выделения, прежде чем вы сможете вызвать метод.» ... Почему это так? Не могли бы вы указать на ту часть документации, где я могу прочитать это подробно? (Я новичок). Кроме того, можно ли получить аналогичные советы по возвращаемым типам?
Nawaz
2
Мне не хватает информации о том, почему требуется дополнительное выделение. Строка хранится в куче, при принятии & String в качестве аргумента, почему Rust просто не передает указатель, хранящийся в стеке, который указывает на пространство кучи, я не понимаю, почему для передачи & String потребуется дополнительное выделение, передача строки slice также должен требовать отправки указателя, хранящегося в стеке, который указывает на пространство кучи?
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]>и т. Д.

Питер Холл
источник