What's the canonical way to accept as an argument of a function and store as a field either reference, struct or Rc/Arc of that struct?
Can you give an example of what you're trying to achieve?
You could use the AsRef
trait. This just says you can get a reference to &Bar
from a type B: AsRef<Bar>
.
struct Bar;
struct Foo<B: AsRef<Bar>> {
bar: B
}
fn make_foo<B: AsRef<Bar>>(bar: B) -> Foo<B> {
Foo { bar }
}
You can also do
struct Bar;
struct Foo<B: Deref<Target=Bar>> {
bar: B
}
fn make_foo<B: Deref<Target=Bar>>(bar: B) -> Foo<B> {
Foo { bar }
}
There is not much of a difference to AsRef<T>
.
It depends why do you need this flexibility.
If you just want the caller to be able to pass anything, then this seems reasonable:
fn take_foo(&mut self, foo: &Foo) {
self.foo = Arc::new(foo);
}
// or potentially slightly more efficient
fn take_foo(&mut self, foo: impl Into<Arc<Foo>>) {
self.foo = foo.into();
}
In the example I'm always storing Arc
, because that's most flexible for the users of the object.
If you want to store the object using various wrappers, to allow callers to have a bit more efficiency at cost of borrow checker or thread-safety limits, that's trickier, because that touches higher-kinded types.
There's Archery crate that does some clever hacks to allow storing either Rc
or Arc
:
For Cow
that's doable if you add a lifetime parameter to your container type, and make callers use 'static
type for the Owned
case.
struct CowWrapper<'a> {
cow: Cow<'a, Foo>,
}
impl CowWrapper<'static> {
fn new_owned(foo: Foo) -> Self {}
}
impl<'a> CowWrapper<'a> {
fn new_borrowed(foo: &'a Foo) -> Self {}
}
So, my use case is:
I have a struct that performs operations on a list of another struct. Sth like:
struct Resource {};
struct Bundle {
resources: Vec<Resource>
};
impl Bundle {
pub fn add_resource(&mut self, res: Resource) {
self.resources.push(res);
}
}
This works with zero-overhead since Bundle
owns its Resource
. But I have cases where I need the same Resource
to go to multiple Bundle
instances as read-only.
So, I have two ways to solve it. I can either have some separate struct that owns all Resource
instances and hands references to them to Bundle
- which is what I'm doing now.
Or I can accept Rc<Resource>
and store it.
The former is pretty inflexible and causes a lot of troubles for my consumers since they have to fight the lifetimes and adds this requirement of a separate place to allocate the resources.
The latter is very flexible and convenient, so I'd prefer to use it.
But I'm wondering if it would be possible to handle all three and somehow allow:
let bundle = Bundle::new();
bundle.add_resource(Resource::new());
let res = Resource::new();
bundle.add_resource(&res);
let res_arc = Arc::new(Resource::new());
bundle.add_resource(res_arc.clone());
This would give a lot of flexibility to my consumers who could then organize their management of Resource
to their needs, allowing for the simplest mode:
let bundle = Bundle::new();
bundle.add_resource(Resource::new());
and two "more complex" modes where the resources are stored separately either as Arcs, or directly and only reference is passed to bundle.
The reason why I think it should be possible is that I only really read the Resource
struct, and all three - ownership, reference and Arc, allow me to read, right?
So I imagine there to be some version of Cow
that can handle owned, borrowed and Arc and return a reference. Or something else (trait?) that would handle such ergonomics.
Am I missing some limitation?
It's not clear to me whether you expect most Bundle
s to contain all three of &Resource
, Resource
and Arc<Resource>
, which can only be done with an enum
, or if it would work to have three+ different "flavors" of Bundle
which the consumer can choose between. Because if most consumers don't need the flexibility of allowing any type, wrapping the Resource
in an enum
adds unnecessary overhead to the simple case.
Here's what I'm thinking of:
struct Bundle<R> {
resources: Vec<R>,
}
use std::borrow::Borrow;
impl<R> Bundle<R> {
pub fn add_resource(&mut self, res: impl Into<R>) {
self.resources.push(res.into());
}
pub fn use_resources(&self)
where
R: Borrow<Resource>,
{
for foo in &self.resources {
// use foo.borrow() to access the Resource
}
}
}
This has several advantages:
- When the consumer doesn't need flexibility, you don't pay for it:
Bundle<Resource>
can be used in the obvious way with only owned values. - Most of the remaining flexibility can be achieved with
Bundle<Arc<Resource>>
(orRc
in single threaded contexts): you can pass aResource
toadd_resource
and it will be automatically packaged into anArc
. -
Bundle<Cow<'a, Resource>>
also works. You can make passing an owned or borrowedResource
toadd_resource
work by implementingInto
. - If you really want to allow a single
Bundle
to contain all three, you can still write an enum, implementBorrow
for it and useBundle<ResourceEnum<'a>>
. Be aware that implementingBorrow
comes with stipulations:Eq
,Ord
andHash
have to behave the same as for the borrowed type. See theBorrow
documentation for more detail.
The main disadvantage - if it is a disadvantage - is that the creator of the Bundle
has to know how it will be used. In other words, you can't use Bundle<Arc<Resource>>
when you don't know at compile time whether a given Bundle
will or will not be used to store references. And of course if all of your use cases might store references, Arc
s and owned Resource
s together, making Bundle
generic is more complex than just baking in the enum
.
The reason I used Borrow
, instead of AsRef
as @Michael-F-Bryan did, is because Borrow<T>
is implemented for all T
. You could use AsRef
by implementing AsRef<Resource> for Resource
, but I think Borrow<Resource>
just seems more semantically appropriate for abstracting over different ways of storing Resource
s.
bundle.add_resource(&res);
If you store &res
as a reference (without copying it into Arc
), then Rust will have to ensure that lifetime of bundle
is shorter than lifetime of res
, with all the borrow checker complications it brings.
And borrow checking is done compile time based on types, not at runtime based on usage, so even if bundle
user never actually inserts any references, they will still have to deal with lifetimes.
So if ease of use is your goal, taking argument as impl Into<Arc<Resource>>
seems the best.
Storing Vec<Cow<'a, Resource>>
in Bundle<'a>
is the second best option, where you can have a special case of Bundle<'static>
that's good only for Cow::Owned
variants.
@trentj - this is great!!! I was able to turn FluentBundle
to operate on generics and I now have a PR that handles all my needs! Thank you so much!
Here's the PR in case anyone is interested - Turn FluentBundle to be a generic over Borrow<FluentResource> by zbraniecki · Pull Request #114 · projectfluent/fluent-rs · GitHub
Great! I'm glad I was able to help.
This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.