Напротив свойства «Заимствовать» для типов копирования?
Я видел 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
было задано? Или это идиоматический способ написания подобных вещей?
Ответы
На самом деле нет обратной черты для 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: Borrow
for 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
немного более эргономичным, говоря , он принимает все , что может быть конвертирован .into
String
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
вA
whenA: Copy
так же просто, как просто добавить*
; возможность пропустить этот шаг, вероятно, не является достаточно убедительной победой, чтобы уравновесить дополнительную сложность использования дженериков в большинстве случаев.
В заключение, foo
почти наверняка следует просто принять value: u32
и позволить вызывающей стороне решить, как получить это значение.
Смотрите также
- Что является более традиционным - передавать по значению или по ссылке, когда методу требуется владение значением?
С помощью имеющейся у вас функции вы можете использовать только 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);