53

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?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Phil Lord
  • 2,917
  • 1
  • 20
  • 31

4 Answers4

38

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:

Kornel
  • 97,764
  • 37
  • 219
  • 309
  • 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? – Phil Lord Dec 30 '17 at 14:33
  • 2
    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`. – Kornel Dec 30 '17 at 14:39
  • 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. – Phil Lord Dec 30 '17 at 14:53
  • An alternative to point 1 could be `Some(a).as_deref()` – Nicolas Del Valle May 31 '21 at 17:43
30

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.

Deadbeef
  • 1,499
  • 8
  • 20
5

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"),
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Sebastian Redl
  • 69,373
  • 8
  • 123
  • 157
1

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");
    }
};
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
V Bota
  • 557
  • 6
  • 11
  • 1
    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. – Kornel Jun 07 '21 at 11:54