Difference between match and unwrap_or when determining types

2k Views Asked by At

The following is a valid file I can compile with Rust 1.23.0:

fn main() {
    let r = String::from("a");
    let a = Some(&r);
    let b = match a {
        Some(name) => name,
        None => "",
    };
    println!("{}", b);
}

Whilst the following code

fn main() {
    let r = String::from("a");
    let a = Some(&r);
    let b = a.unwrap_or("");
    println!("{}", b);
}

Fails with the error:

error[E0308]: mismatched types
 --> src/main.rs:4:25
  |
4 |     let b = a.unwrap_or("");
  |                         ^^ expected struct `std::string::String`, found str
  |
  = note: expected type `&std::string::String`
             found type `&'static str`

As far as I can see, the compilers reasoning when determining types here is as follows:

  1. In the case with the match it determines b is a &str, since None => "" is a &str and Some(name) => name is a &String so it can become a &str.

  2. In the case where the argument to unwrap_or is a &str, instead of typing b as a &str, it sees there's a difference between the type held in a (namely &String) and the type of the parameter to unwrap_or.

What is the difference between these two cases that makes type deduction work this way?

Is unwrap_or implemented to accept the exact same type as the one wrapped by the option instead of just accepting a generic value it place inside a match ?

Furthermore, is there any way to make the unwrap_or work in this case, without having to declare a String in the external scope or change the type wrapped by the option ?

One answer I got somewhere else for making them work is this:

let b = a.map(|s| s.as_ref()).unwrap_or("")

Same effect as the match, somewhat shorter, more explicit than the match about what's happening to the types.

1

There are 1 best solutions below

5
On

Loosely speaking, you're half correct.

a is an Option<&String>. An &String can be coerced into an &str, but an &str cannot be coerced into an &String. See this code:

fn main() {
    let a = String::from("This is a String");
    let a_r = &a;
    foo(a_r);
    bar(a_r);

    let b = "This is an &str";
    // Next line will cause a compilation error if uncommented
    // foo(b);
    bar(b);
}

fn foo(s: &String) {
    println!("Foo: {}", s);
}

fn bar(s: &str) {
    println!("Bar: {}", s);
}

Playground link

If foo(b); is uncommented, it'll tell you that it expected String, got str.

The match statement returns an &str (converted from an &String), but unwrap_or expects the same type as the Option that you're calling, an &String, which an &str cannot become without a manual change (String::from, to_string, to_owned, etc)

Now, I'm going to try to give a brief explanation for why this is, but take it with a grain of salt because I am not an expert in this field.


When using a string literal ("foo"), the program, on compilation, creates some segment of memory which represents this text in binary form. This is why a string literal is an &str and not just a str: It isn't raw text, but instead, it's a reference to text that already exists at startup. This means that string literals live in a specific portion of memory which cannot be modified, because they're wedged up beside some other object - ("fooquxegg") - and attempting to add characters to a string literal would cause you to overwrite the next object in line, which is Very Bad.

A String, however, is modifiable. Since we can't determine how many characters are in a String, it has to live in the "heap", where objects can be shifted around if something attempts to "grow into" something else, and the String type basically serves as a pointer to that text in the heap, much like a Box<T> points to a T in the heap.

For this reason, while an &String can become an &str (it's pointing to a certain amount of text somewhere, in this case, somewhere in the heap rather than the static memory), an &str cannot be guaranteed to become an &String. If you converted an string literal (like our "foo"), which lives in a portion of memory that can't be modified, into an &String which could potentially be modified, and then attempted to append some characters to it (say "bar"), it would overwrite something else.


With that being said, look at this code:

fn main() {
   let s = "this is an &str";
   let b = String::from("this is a String");
   let s_o : Option<&str> = Some(s);

   let s_or_def = s_o.unwrap_or(&b);
   println!("{}", s_or_def);
}

Playground link

This code is effectively the inverse of your unwrap_or example: An Option<&str> which is being unwrapped with an &String as a default. This will compile, because &String => &str is a valid conversion, and the compiler will automatically insert the necessary conversion without needing it to be explicit. So no, unwrap_or is not implemented to accept the exact same object type; any object which can coerce to T (In this case, T is &str) is valid.

In other words, your unwrap_or example fails because an &str cannot become an &String, rather than because the unwrap_or method is unusually picky. No, there is likely no way to unwrap_or without using a String. Your best bet is to simply use to_string on a string literal, as per:

fn main() {
    let s = String::from("Hello, world!");
    let o = Some(s);
    let r = o.unwrap_or("Goodbye, world!".to_string());

    println!("{}", r);
}

because map_or, while seeming promising, will not simply allow you to input |s| &s for a map (reference is dropped at the end of the closure).