Rust has a number of interesting aspects that I had some difficulty understanding as I was learning the language. Whether you find these things peculiar or not probably depends on which programming languages you’ve spent a good amount of time using. My background skews heavily towards garbage-collected imperative languages.

Trait Objects

Many statically-typed languages have the concept of a named group of methods or functions that any type can implement, interfaces in Java and Go, protocols in Objective-C and Swift, etc. In these languages it is common to declare variables, method parameters, and struct or class members using these abstract types so that any concrete type that implements them can be used in those places.

Go Java func readabyte(r Reader) (byte, error) { var buf [1]byte _, err := r.Read(buf[:]) if err != nil { return 0, err } return buf[0], nil } byte readabyte(InputStream r) throws IOException { byte[] buf = new byte[1]; r.read(buf); return buf[0]; }

Rust’s equivalent is the trait. However unlike the examples above it is uncommon to use a trait as a type in function arguments, struct fields, etc. Instead most Rust code uses generics with a trait bound and the compiled code passes an instance of a concrete type that implements the trait.

Rust generics Rust trait object fn readabyte<R: Read>(r: &mut R) -> Result<u8> { let mut buf = [0u8; 1]; r.read(&mut buf)?; Ok(buf[0]) } fn readabyte(r: &mut Read) -> Result<u8> { let mut buf = [0u8; 1]; r.read(&mut buf)?; Ok(buf[0]) }

The syntactic difference is somewhat subtle but the runtime difference is significant. For the generic version the Rust compiler knows exactly which concrete type is passed and can generate a static call to the read method of that type. For trait objects the Rust compiler must generate a vtable and do a dynamic lookup at runtime to find the read method.

Moves

In the Go and Java examples the concrete types implementing Reader or InputStream are copied or passed by reference to the readabyte functions. However the Rust functions require an explicit mutable reference. What happens if we want to pass by value rather than by reference?

fn readabyte<R: Read>(mut r: R) -> Result<u8> { let mut buf = [0u8; 1]; r.read(&mut buf)?; Ok(buf[0]) }

Rust has “move semantics” meaning that unless a type implements Copy it will be moved when passed by value. In this example the value is moved into the readabyte function which takes ownership of it and drops it at the end of the function, providing deterministic destruction.

Sized

Moving a value may require generating code that moves it to a different memory location which can only be done if the size of the value is known at compile time. However traits may be implemented by a type whose size is not known at compile time, for example arrays, so the following code will not compile:

fn readabyte(mut r: Read) -> Result<u8> { let mut buf = [0u8; 1]; r.read(&mut buf)?; Ok(buf[0]) }

| 24 | fn readabyte(mut r: Read) -> Result<u8> { | ^^^^^ the trait `std::marker::Sized` is not implemented for `std::io::Read + 'static` | = note: `std::io::Read + 'static` does not have a constant size known at compile-time = note: all local variables must have a statically known size

Boxes

Unsized types cannot be moved but can be owned by a type that is sized, for example Box which owns a value allocated on the heap.

fn readabyte(mut r: Box<Read>) -> Result<u8> { let mut buf = [0u8; 1]; r.read(&mut buf)?; Ok(buf[0]) }