Search code examples
rustlifetimetrait-objects

Understanding Rust's Trait Objects and Lifetime Annotations in Different Function Signatures


I am studying the Traits and the Trait Object in Rust. In the Trait chapter, I solved the 6th exercise differently than the compiler suggestion. The following code defines two structs (Sheep and Cow) and a trait (Animal). Both Sheep and Cow implement the Animal trait by providing their own noise method implementations.

There are two functions, the random_animal_if and the random_animal_match that take an argument and return a reference to a dynamic trait object.

struct Sheep {}
struct Cow {}

trait Animal {
    fn noise(&self) -> String;
}

impl Animal for Sheep {
    fn noise(&self) -> String {
        "baaaaah!".to_string()
    }
}

impl Animal for Cow {
    fn noise(&self) -> String {
        "moooooo!".to_string()
    }
}

fn random_animal_if(random_number: f64) -> &'static dyn Animal {
    if random_number < 10.0 {
        &Sheep {}
    } else if random_number > 20.0 {
        &Cow {}
    } else {
        panic!()
    }
}

fn random_animal_match(random_string: &str) -> &dyn Animal {
    match random_string {
        "sheep" => &Sheep {},
        "cow" => &Cow {},
        _ => panic!(),
    }
}

fn main() {
    let animal = random_animal_if(21.0);
    println!("Randomly animal says {}", animal.noise());
    let animal = random_animal_match("sheep");
    println!("Randomly animal says {}", animal.noise());
}

Both function creates and returns either a Sheep or Cow object based on the input. One of them uses conditionals on a floating number input. The other uses pattern matching on a given string slice. The logic is identical, but if I omit the &'static lifetime specification at random_animal_if return type then the compiler throws this error:

error[E0106]: missing lifetime specifier

Interestingly, if the input parameter type is changed from f64 to &str then the static lifetime annotation can be removed. Why? What is the difference between the two types?


Solution

  • This is due to Rust's lifetime elision rules.

    If you have a function that takes a reference as an argument and returns a reference, then the compiler infers that the output borrows from the input, which looks like this:

    fn random_animal_match<'a>(random_str: &'a str) -> &'a dyn Animal {
    

    In fact, the only way for a function to return a non-static reference is if the returned data is borrowed from an argument.

    However, in your code, the body of random_animal_match does not borrow the return value from the argument. The compiler still infers the elided lifetimes as if that is the case but in fact the lifetime in the return type is always 'static. This means that the lifetime in your function's return type is overly restrictive. A caller of the function will get compiler errors if they try to use the returned &dyn Animal after the input &str is dropped, even though this shouldn't actually be a problem:

    fn main() {
        let animal = {
            let sheep = String::from("sheep");
            random_animal_match(&sheep)
        }; // - `sheep` dropped here while still borrowed
    
        // `sheep` does not live long enough
        println!("Randomly animal says {}", animal.noise());
    }
    

    To maximise the flexibility of this function, you should make the lifetime in the return type 'static:

    fn random_animal_match(random_str: &str) -> &'static dyn Animal {