Nullability and Optionality
In C#, null is often used to represent a value that is missing, absent or
logically uninitialized. For example:
int? some = 1;
int? none = null;
Rust has no null and consequently no nullable context to enable. Optional or
missing values are instead represented by Option<T>. The
equivalent of the C# code above in Rust would be:
let some: Option<i32> = Some(1);
let none: Option<i32> = None;
Option<T> in Rust is practically identical to 'T option from F#.
Control flow with optionality
In C#, you may have been using if/else statements for controlling the flow
when using nullable values.
uint? max = 10;
if (max is { } someMax)
{
Console.WriteLine($"The maximum is {someMax}."); // The maximum is 10.
}
You can use pattern matching to achieve the same behavior in Rust:
let max = Some(10u32);
match max {
Some(max) => println!("The maximum is {}.", max), // The maximum is 10.
None => ()
}
It would even be more concise to use if let:
let max = Some(10u32);
if let Some(max) = max {
println!("The maximum is {}.", max); // The maximum is 10.
}
Null-conditional operators
The null-conditional operators (?. and ?[]) make dealing with null in C#
more ergonomic. In Rust, they are best replaced by using either the
map method or the and_then method, depending on
the nesting of the Option. The following snippets show the correspondence:
string? some = "Hello, World!";
string? none = null;
Console.WriteLine(some?.Length); // 13
Console.WriteLine(none?.Length); // (blank)
record Name(string FirstName, string LastName);
record Person(Name? Name);
{
Person? person = new Person(new Name("John", "Doe"));
Console.WriteLine(person1?.Name?.FirstName); // John
}
{
Person? person = new Person(null);
Console.WriteLine(person1?.Name?.FirstName); // (blank)
}
{
Person? person = null;
Console.WriteLine(person1?.Name?.FirstName); // (blank)
}
let some: Option<String> = Some(String::from("Hello, World!"));
let none: Option<String> = None;
println!("{:?}", some.map(|s| s.len())); // Some(13)
println!("{:?}", none.map(|s| s.len())); // None
struct Name { first_name: String, last_name: String }
struct Person { name: Option<Name> }
let person: Option<Person> = Some(Person {
name: Some(Name {
first_name: "John".into(),
last_name: "Doe".into(),
}),
});
println!("{:?}", person.and_then(|p| p.name.map(|name| name.first_name))); // Some("John")
let person: Option<Person> = Some(Person { name: None });
println!("{:?}", person.and_then(|p| p.name.map(|name| name.first_name))); // None
let person: Option<Person> = None;
println!("{:?}", person.and_then(|p| p.name.map(|name| name.first_name))); // None
The ? operator (mentioned in the previous chapter), can also be used to
handle an Option. It returns from the function with None if a None is
encountered, else continues with the Some value:
fn foo(optional: Option<i32>) -> Option<String> {
let value = optional?;
Some(value.to_string())
}
Null-coalescing operator
The null-coalescing operator (??) is typically used to default to another
value when a nullable is null:
int? some = 1;
int? none = null;
Console.WriteLine(some ?? 0); // 1
Console.WriteLine(none ?? 0); // 0
In Rust, you can use unwrap_or to get the same behavior:
let some: Option<i32> = Some(1);
let none: Option<i32> = None;
println!("{:?}", some.unwrap_or(0)); // 1
println!("{:?}", none.unwrap_or(0)); // 0
Note: If the default value is expensive to compute, you can use
unwrap_or_else instead. It takes a closure as an argument, which allows you to
lazily initialize the default value.
Null-forgiving operator
The null-forgiving operator (!) does not correspond to an equivalent construct
in Rust, as it only affects the compiler's static flow analysis in C#. In Rust,
there is no need to use a substitute for it. unwrap is close,
though: it panics if the value is None. expect is similar but
allows you to provide a custom error message. Note that as previously said,
panics should be reserved to unrecoverable situations.