How can I pattern match against an Option<String>?
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:
-
Change
Option<String>
toOption<&str>
before matching on it:Some(a).as_deref()
. Theas_deref()
is a combo ofas_ref()
that makesOption<&String>
(preventing move), andderef()
/as_str()
then unambiguously references it as a&str
. -
Use match guard:
match Some(a) { Some(ref s) if s == "hello" => … }
.Some(ref s)
matches anyString
, and captures it ass: &String
, which you can then compare in theif
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
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");
}
};
Related videos on Youtube
Comments
-
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 anOption
. 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 theSome(String)
— so I can't change this type in reality as I could do in my example.Any other options?
-
Phil Lord over 6 yearsThis 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 ofmap
? Should this not be aIterator
type? -
Kornel over 6 yearsHere
.map()
is not an iterator at all. It's a helper method onOption
that allows you to change it if it'sSome
and do nothing if it'sNone
. -
Phil Lord over 6 yearsAh, 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 almost 3 yearsAn alternative to point 1 could be
Some(a).as_deref()
-
Kornel almost 3 yearsYou can avoid a temporary allocation by changing order of dereferecing/unwrapping:
option.as_deref().unwrap_or("unhandled string")
. AlsoString::from("_")
generates way less code than the dynamic formatting machinery.