In what scenarios are APIs that don't borrow preferred?

  • A+
Category:Languages

Rust has the concepts of ownership and borrowing. If a function doesn't borrow its parameter as a reference, the arguments to that function are moved and will be deallocated once they go out of scope.

Take this function:

fn build_user(email: String, username: String) -> User {     User {         email: email,         username: username,     } } 

This function can be called as:

let email = String::from("foo@example.com"); let username = String::from("username");  let user = build_user(email, username); 

Since email and username have been moved, they can no longer be used after build_user was called.

This can be fixed by making the API use borrowed references instead.

With that in mind, which scenarios would one always prefer to not use borrowing when designing APIs?

 


This list may not be exhaustive, but there are plenty of times when it's advantageous to choose not to borrow an argument.

1. Efficiency with small Copy types

If a type is small and implements Copy, it is usually more efficient to copy it around, rather than passing pointers. References mean indirection - apart from having to do two steps to get to the data, values behind pointers are less likely to be stored compactly in memory and therefore are slower to copy into CPU caches, for example if you are iterating over them.

2. To transfer ownership

When you need data to stick around, but the current owner needs to be cleaned up and go out of scope, you might transfer ownership by moving it somewhere else. For example, you might have a local variable in a function, but move it into a Box so that it can live on after the function has returned.

3. Method chaining

If a set of methods all consume self and return Self, you can conveniently chain them together, without needing intermediate local variables. You will often see this approach used for implementing builders. Here is an example taken from the derive_builder crate documentation:

let ch = ChannelBuilder::default()     .special_info(42u8)     .token(19124)     .build()     .unwrap(); 

4. Statically enforcing invariants

Sometimes, you want a value to be consumed by a function to guarantee that it cannot be used again, as a way of enforcing assumptions at the type-level. For example, in the futures crate, the Future::wait method consumes self:

fn wait(self) -> Result<Self::Item, Self::Error>  where     Self: Sized, 

This signature is specifically designed to prevent you from calling wait twice. The implementation doesn't have to check at runtime to see if the future is already in a waiting state - the compiler just won't allow that situation.

It also gives protection from errors when using method-chained builders. The design statically prevents you from doing things out of order - you can't accidentally set a field on a builder after the object is created because the builder is consumed by its build method.

5. To make cloning costs explicit to callers

Some objects just need to own their data because it can't be statically determined how long they will live - or because they will live for a long time, and it's inconvenient to have to ensure they don't outlive objects they refer to. In these cases, you may choose to make a clone of the original value.

This could be done by accepting a reference and then calling clone within the function, but this may not always be ideal because it hides the potentially expensive clone operation from the caller. Accepting the value rather than a reference means the caller must explicitly clone (or move) the value and can make better design choices with that in mind.

Comment

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: