How can I pattern match against an Option<String>?

16,629

Solution 1

It's a known limitation of Rust's patterns.

Method calls (including internal methods for operators like ==) automatically call .deref() as needed, so String gets automagically turned into &str for comparisons with literals.

On the other hand, the patterns are quite literal in their comparisons, and find that String and &str are different.

There are two solutions:

  1. Change Option<String> to Option<&str> before matching on it: Some(a).as_deref(). The as_deref() is a combo of as_ref() that makes Option<&String> (preventing move), and deref()/as_str() then unambiguously references it as a &str.

  2. Use match guard: match Some(a) { Some(ref s) if s == "hello" => … }. Some(ref s) matches any String, and captures it as s: &String, which you can then compare in the if guard which does the usual flexible coercions to make it work.

See also:

Solution 2

As of Rust 1.40, you can now call as_deref on Option<String> to convert it to Option<&str> and then match on it:

match args.nth(1).as_deref() {
    Some("help") => {}
    Some(s) => {}
    None => {}
}

I found this because it is one of the clippy lints.

Solution 3

Look at this.

You cannot match on std::String, as you've found, only on &str. Nested pattern matches work, so if you can match on &str, you can match on Option<&str>, but still not on Option<String>.

In the working example, you turned the std::String into a &str by doing &a[..]. If you want to match on a Option<String>, you have to do the same thing.

One way is to use nested matches:

match a {
    Some(ref s) => match &s[..] {
        "hello" => /* ... */,
        _ => /* ... */,
    },
    _ => /* ... */,
}

But then you have to duplicate the "otherwise" code if it's the same, and it's generally not as nice.

Instead, you can turn the Option<String> into an Option<&str> and match on this, using the map function. However, map consumes the value it is called on, moving it into the mapping function. This is a problem because you want to reference the string, and you can't do that if you have moved it into the mapping function. You first need to turn the Option<String> into a Option<&String> and map on that.

Thus you end up with a.as_ref().map(|s| /* s is type &String */ &s[..]). You can then match on that.

match os.as_ref().map(|s| &s[..]) {
    Some("hello") => println!("It's 'hello'"),
    // Leave out this branch if you want to treat other strings and None the same.
    Some(_) => println!("It's some other string"),
    _ => println!("It's nothing"),
}

Solution 4

In some cases, you can use unwrap_or to replace Option::None with a predefined &str you don't want to handle in any special way.

I used this to handle user inputs:

let args: Vec<String> = env::args().collect();

match args.get(1).unwrap_or(&format!("_")).as_str() {
    "new" => {
        print!("new");
    }
    _ => {
        print!("unknown string");
    }
};

Or to match your code:

let option = Some("hello");

match option.unwrap_or(&format!("unhandled string").as_str()) {
    "hello" => {
        println!("hello");
    }
    _ => {
        println!("unknown string");
    }
};
Share:
16,629

Related videos on Youtube

Phil Lord
Author by

Phil Lord

Occasionally I ask questions on stack exchange

Updated on June 14, 2022

Comments

  • Phil Lord
    Phil Lord almost 2 years

    I can straight-forwardly match a String in Rust:

    let a = "hello".to_string();
    
    match &a[..] {
        "hello" => {
            println!("Matches hello");
        }
        _ => panic!(),
    }
    

    If I have an option type, it fails:

    match Some(a) {
        Some("hello") => {
            println!("Matches some hello");
        }
        _ => panic!(),
    }
    

    because the types don't match:

    error[E0308]: mismatched types
     --> src/main.rs:5:14
      |
    5 |         Some("hello") => {
      |              ^^^^^^^ expected struct `std::string::String`, found reference
      |
      = note: expected type `std::string::String`
                 found type `&'static str`
    

    I can't do the [..] trick because we have an Option. The best that I have come up with so far is:

    match Some(a) {
        Some(b) => match (&b[..]) {
            "hello" => {
                println!("Matches some, some hello");
            }
            _ => panic!(),
        },
        None => panic!(),
    }
    

    which works but is terrible for its verbosity.

    In this case, my code is just an example. I do not control the creation of either the String or the Some(String) — so I can't change this type in reality as I could do in my example.

    Any other options?

  • Phil Lord
    Phil Lord over 6 years
    This is good -- I prefer 1 because it's less verbose as I want to match more than one string. But, I find it confusing. Why can we create an iterator over a single value, on Option? And why can we match a single value on the return of map ? Should this not be a Iterator type?
  • Kornel
    Kornel over 6 years
    Here .map() is not an iterator at all. It's a helper method on Option that allows you to change it if it's Some and do nothing if it's None.
  • Phil Lord
    Phil Lord over 6 years
    Ah, excuse my ignorance. Totally misunderstood the rust-doc for Option. Yes, you are right -- "map" == map one option to another, just with the same name as a collection.
  • Nicolas Del Valle
    Nicolas Del Valle almost 3 years
    An alternative to point 1 could be Some(a).as_deref()
  • Kornel
    Kornel almost 3 years
    You can avoid a temporary allocation by changing order of dereferecing/unwrapping: option.as_deref().unwrap_or("unhandled string"). Also String::from("_") generates way less code than the dynamic formatting machinery.