问题
In Rust, I've seen the Borrow
trait used to define functions that accept both an owned type or a reference, e.g. T
or &T
. The borrow()
method is then called in the function to obtain &T
.
Is there some trait that allows the opposite (i.e. a function that accepts T
or &T
and obtains T
) for Copy
types?
E.g. for this example:
use std::borrow::Borrow;
fn foo<T: Borrow<u32>>(value: T) -> u32 {
*value.borrow()
}
fn main() {
println!("{}", foo(&5));
println!("{}", foo(5));
}
This calls borrow()
to obtain a reference, which is then immediately dereferenced.
Is there another implementation that just copies the value if T
was passed in, and derefences if &T
was given? Or is the above the idiomatic way of writing this sort of thing?
回答1:
There is not really an inverse trait for Borrow
, because it's not really useful as a bound on functions the same way Borrow
is. The reason has to do with ownership.
Why is "inverse Borrow
" less useful than Borrow
?
Functions that need references
Consider a function that only needs to reference its argument:
fn puts(arg: &str) {
println!("{}", arg);
}
Accepting String
would be silly here, because puts
doesn't need to take ownership of the data, but accepting &str
means we might sometimes force the caller to keep the data around longer than necessary:
{
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
The problem being that output
isn't needed after it's passed to puts
, and the caller knows this, but puts
requires a reference, so output
has to stay alive until the end of the block. Obviously you can always fix this in the caller by adding more blocks and sometimes a let
, but puts
can also be made generic to let the caller delegate the responsibility of cleaning up output
:
fn puts<T: Borrow<str>>(arg: T) {
println!("{}", arg.borrow());
}
Accepting T: Borrow
for puts
gives the caller the flexibility to decide whether to keep the argument around or to move it into the function.
Functions that need owned values
Now consider the case of a function that actually needs to take ownership:
struct Wrapper(String);
fn wrap(arg: String) -> Wrapper {
Wrapper(arg)
}
In this case accepting &str
would be silly, because wrap
would have to call to_owned()
on it. If the caller has a String
that it's no longer using, that would needlessly copy the data that could have just been moved into the function. In this case, accepting String
is the more flexible option, because it allows the caller to decide whether to make a clone or pass an existing String
. Having an "inverse Borrow
" trait would not add any flexibility that arg: String
does not already provide.
But String
isn't always the most ergonomic argument, because there are several different kinds of string: &str
, Cow<str>
, Box<str>
... We can make wrap
a little more ergonomic by saying it accepts anything that can be converted into
a String
.
fn wrap<T: Into<String>>(arg: T) -> Wrapper {
Wrapper(arg.into())
}
This means you can call it like wrap("hello, world")
without having to call .to_owned()
on the literal. Which is not really a flexibility win -- the caller can always call .into()
instead without loss of generality -- but it is an ergonomic win.
What about Copy
types?
Now, you asked about Copy
types. For the most part the arguments above still apply. If you're writing a function that, like puts
, only needs a &A
, using T: Borrow<A>
might be more flexible for the caller; for a function like wrap
that needs the whole A
, it's more flexible to just accept A
. But for Copy
types the ergonomic advantage of accepting T: Into<A>
is much less clear-cut.
- For integer types, because generics mess with type inference, using them usually makes it less ergonomic to use literals; you may end up having to explicitly annotate the types.
- Since
&u32
doesn't implementInto<u32>
, that particular trick wouldn't work here anyway. - Since
Copy
types are readily available as owned values, it's less common to use them by reference in the first place. - Finally, turning a
&A
into anA
whenA: Copy
is as simple as just adding*
; being able to skip that step is probably not a compelling enough win to counterbalance the added complexity of using generics in most cases.
In conclusion, foo
should almost certainly just accept value: u32
and let the caller decide how to get that value.
See also
- Is it more conventional to pass-by-value or pass-by-reference when the method needs ownership of the value?
回答2:
With the function you have you can only use a u32
or a type that can be borrowed as u32
.
You can make your function more generic by using a second template argument.
fn foo<T: Copy, N: Borrow<T>>(value: N) -> T {
*value.borrow()
}
This is however only a partial solution as it will require type annotations in some cases to work correctly.
For example, it works out of the box with usize
:
let v = 0usize;
println!("{}", foo(v));
There is no problem here for the compiler to guess that foo(v)
is a usize
.
However, if you try foo(&v)
, the compiler will complain that it cannot find the right output type T
because &T
could implement several Borrow
traits for different types. You need to explicitly specify which one you want to use as output.
let output: usize = foo(&v);
来源:https://stackoverflow.com/questions/63465672/opposite-of-borrow-trait-for-a-copy-type-in-rust