When dealing with ownership and references, I sometimes feel tempted to either use clone()
, or create a new struct which is almost identical to an existing struct except for the fact that some fields include a lifetime annotation. This way, I can have ownership when I need to, but at the same time, use reference types when possible to avoid cloning.
But it seems inelegant to clutter the codebase with things like:
struct MyStruct {
value: T
}
struct MyStructRef <'a> {
value: &'a T
}
When there is absolutely no difference between the two structs except for the fact that one deals with lifetimes and the other doesn't.
So I'm wondering: is there something I'm missing that actually makes this pattern completely unnecessary? Or is this something that even experienced Rust programmers deal with regularly?
I've included some code to help illustrate the problem I'm having:
// We intend to marshall a Vec<i32> and include a timestamp of the time when the Vec was marshalled:
struct ItemsTimestamped {
items: Vec<i32>,
timestamp_unix_epoch: u64,
}
const TIMESTAMP_UNIX_EPOCH: u64 = 1600000000; // just use a constant value for demo purposes.
fn unmarshall() -> ItemsTimestamped {
// Let's just pretend we're using something like serde to deserialize the struct from a JSON file.
ItemsTimestamped {
items: vec![1, 2, 3],
timestamp_unix_epoch: TIMESTAMP_UNIX_EPOCH,
}
}
// Marshalling requires only a reference to the struct.
fn marshall(my_struct_timestamped: &ItemsTimestamped) {}
// However, even if the struct itself is just a reference, the fields within this struct are owned.
// But we want to include the Vec<i32> in the struct without taking ownership of the
// original Vec<i32>. So we want to be able to write something like the following:
fn solution_1() {
// obtain ownership of the items.
let items = unmarshall().items;
// marshall the items without losing ownership:
marshall_cloned(&items);
// just to illustrate that we haven't lost ownership:
take_ownership(items);
}
// 1st solution: Just clone the Vec. This can be inefficient if the Vec is large.
fn marshall_cloned(items: &Vec<i32>) {
let timestamped = &ItemsTimestamped {
items: items.clone(),
timestamp_unix_epoch: TIMESTAMP_UNIX_EPOCH,
};
marshall(timestamped)
}
// 2nd solution: Take ownership of the struct to serialize, and then return this exact same struct.
// So the caller does lose ownership of the Vec (which we actually want to avoid), but the caller
// can then just use the return value in place of the Vec that was moved.
// The disadvantage is that, when reading the code, it is not immediately obvious why a Vec<i32>
// is passed and also returned. Usually, for a function with a type signature like (T) -> T, you would
// expect that the function does not simply return the input without making any modifications.
fn marshall_ref_return_original(items: Vec<i32>) -> Vec<i32> {
let timestamped = ItemsTimestamped {
items,
timestamp_unix_epoch: TIMESTAMP_UNIX_EPOCH,
};
marshall(×tamped);
timestamped.items
}
// 3rd solution: Introduce a new type that basically has the same meaning as ItemsTimestamped,
// but includes lifetimes. This also works and does not involve cloning, but now we need
// a new type just to cope with lifetimes.
struct ItemsTimestampedRef<'a> {
items: &'a Vec<i32>,
timestamp_unix_epoch: u64,
}
fn marshall_ref(_my_struct_timestamped: ItemsTimestampedRef) {}
fn marshall_new_type(items: &Vec<i32>) {
let timestamped = ItemsTimestampedRef {
items,
timestamp_unix_epoch: TIMESTAMP_UNIX_EPOCH,
};
marshall_ref(timestamped)
}
fn solution_2() {
let items = unmarshall().items;
let items = marshall_ref_return_original(items);
take_ownership(items);
}
fn solution_3() {
let items = unmarshall().items;
marshall_new_type(&items);
take_ownership(items);
}
fn take_ownership(_items: Vec<i32>) {}
fn main() {
solution_1();
solution_2();
solution_3();
}