Напротив свойства «Заимствовать» для типов копирования?

Aug 18 2020

Я видел Borrowчерту, используемую для определения функций, которые принимают как собственный тип, так и ссылку, например Tили &T. Затем borrow()метод вызывается в функции для получения &T.

Есть ли какая-то черта, которая допускает противоположное (т. Е. Функция, которая принимает Tили &Tполучает T) для Copyтипов?

Например, для этого примера:

use std::borrow::Borrow;

fn foo<T: Borrow<u32>>(value: T) -> u32 {
    *value.borrow()
}

fn main() {
    println!("{}", foo(&5));
    println!("{}", foo(5));
}

Это вызывает borrow()получение ссылки, которая затем немедленно разыменовывается.

Есть ли другая реализация, которая просто копирует значение, если оно Tбыло передано, и разыменовывает, если &Tбыло задано? Или это идиоматический способ написания подобных вещей?

Ответы

5 trentcl Aug 18 2020 at 14:09

На самом деле нет обратной черты для Borrow, потому что она не очень полезна как ограничение функций таким же образом Borrow. Причина связана с правом собственности.

Почему «обратное Borrow» менее полезно, чем Borrow?

Функции, которым нужны ссылки

Рассмотрим функцию, которой требуется только ссылаться на свой аргумент:

fn puts(arg: &str) {
    println!("{}", arg);
}

Принять здесь Stringбыло бы глупо, потому putsчто не нужно брать на себя ответственность за данные, но принятие &strозначает, что иногда мы можем заставить вызывающего абонента хранить данные дольше, чем необходимо:

{
    let output = create_some_string();
    output.push_str(some_other_string);
    puts(&output);
    // do some other stuff but never use `output` again
} // `output` isn't dropped until here

Проблема в том, что outputэто не нужно после того, как оно было передано puts, и вызывающий знает это, но putsтребует ссылку, поэтому outputдолжен оставаться в живых до конца блока. Очевидно, вы всегда можете исправить это в вызывающей стороне, добавив больше блоков, а иногда и let, но putsтакже можно сделать универсальной, чтобы вызывающая сторона делегировала ответственность за очистку output:

fn puts<T: Borrow<str>>(arg: T) {
    println!("{}", arg.borrow());
}

Принятие T: Borrowfor putsдает вызывающему абоненту возможность решить, сохранить ли аргумент или переместить его в функцию.

Функции, которым нужны собственные значения

Теперь рассмотрим случай функции, которая действительно должна стать владельцем:

struct Wrapper(String);
fn wrap(arg: String) -> Wrapper {
    Wrapper(arg)
}

В этом случае принимать &strбыло бы глупо, потому wrapчто пришлось бы называть to_owned()его. Если у вызывающего абонента есть Stringфункция, которую он больше не использует, это приведет к ненужному копированию данных, которые могли быть только что перемещены в функцию. В этом случае принятие Stringявляется более гибким вариантом, поскольку он позволяет вызывающей стороне решить, создавать ли клон или передавать существующий String. Наличие «обратной Borrow» черты не добавит гибкости, которой arg: Stringеще не было.

Но Stringэто не всегда самый эргономичный аргумент, потому что есть несколько различных видов строки: &str, Cow<str>, Box<str>... Мы можем сделать wrapнемного более эргономичным, говоря , он принимает все , что может быть конвертирован .intoString

fn wrap<T: Into<String>>(arg: T) -> Wrapper {
    Wrapper(arg.into())
}

Это означает, что вы можете назвать это как, wrap("hello, world")не прибегая к .to_owned()буквальному. Что на самом деле не является преимуществом гибкости - .into()вместо этого вызывающий абонент всегда может позвонить без потери общности - но это выигрыш с эргономикой .

А как насчет Copyтипов?

Вы спросили о Copyтипах. По большей части приведенные выше аргументы все еще применимы. Если вы пишете функцию, которая putsтребует только одного &A, использование T: Borrow<A>может быть более гибким для вызывающего; для такой функции wrapтребуется целое A, более гибко просто принять A. Но для Copyтипов эргономическое преимущество принятия T: Into<A>гораздо менее очевидно.

  • Для целочисленных типов, поскольку обобщенные типы не справляются с выводом типов, их использование обычно делает менее эргономичным использование литералов; вам может потребоваться явное аннотирование типов.
  • Поскольку &u32не реализует Into<u32>, этот конкретный трюк здесь все равно не сработает.
  • Поскольку Copyтипы легко доступны как собственные значения, их реже использовать в первую очередь по ссылке.
  • Наконец, превратить &Aв Awhen A: Copyтак же просто, как просто добавить *; возможность пропустить этот шаг, вероятно, не является достаточно убедительной победой, чтобы уравновесить дополнительную сложность использования дженериков в большинстве случаев.

В заключение, fooпочти наверняка следует просто принять value: u32и позволить вызывающей стороне решить, как получить это значение.

Смотрите также

  • Что является более традиционным - передавать по значению или по ссылке, когда методу требуется владение значением?
2 Sunreef Aug 18 2020 at 11:00

С помощью имеющейся у вас функции вы можете использовать только u32тип или тип, который можно заимствовать как u32.

Вы можете сделать свою функцию более универсальной, используя второй аргумент шаблона.

fn foo<T: Copy, N: Borrow<T>>(value: N) -> T {
    *value.borrow()
}

Однако это только частичное решение, поскольку в некоторых случаях для правильной работы требуются аннотации типов.

Например, он работает "из коробки" с usize:

let v = 0usize;
println!("{}", foo(v));

Для компилятора нетрудно догадаться, что foo(v)это файл usize.

Однако, если вы попробуете foo(&v), компилятор пожалуется, что не может найти правильный тип вывода, Tпотому что &Tможет реализовать несколько Borrowпризнаков для разных типов. Вам нужно явно указать, какой из них вы хотите использовать в качестве вывода.

let output: usize = foo(&v);