How can I pattern match against an Option?

2020-02-27 18:00发布

问题:

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?

回答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.

OTOH 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_ref().map(String::as_str). The as_ref() makes Option<&String> (preventing move), and 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:

  • Converting from Option<String> to Option<&str>


回答2:

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"),
}


回答3:

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");
    }
};