In the previous post, we discussed Rust’s error handling.
Closely related to this is the management of so-called sentinel values in the language. When I first heard that Rust
essentially has no null
, I was a bit taken aback.
What are sentinel values? Link to heading
Sentinel values are typically used to indicate the end of a data stream or to signal specific states within a program.
Examples include null
, -1
, or NaN
. A common source of bugs in programs is when developers forget to handle these
states properly. That’s why, in Rust, they have been practically eliminated them.
Let’s look at a loosely written JavaScript program whose task is to transform the first character of user input into uppercase.
1function getFirstCharacterInUppercase(str) {
2 return str[0].toUpperCase() + str.slice(1);
3}
4
5const userInput = null;
6
7console.log(getFirstCharacterInUppercase(userInput)); // Output: TypeError: can't access property "toUpperCase" of null
This code, although it lacks error handling, could be delivered to a production environment without issues, where the user would notice the shortcomings. However, this is far from optimal. After the user points out the problem to the developer, it is likely that the small program will be fixed in the following way:
1function getFirstCharacterInUppercase(str) {
2 if (typeof str !== 'string' || str.length === 0) {
3 return '';
4 }
5 return str[0].toUpperCase() + str.slice(1);
6}
7
8const userInput = null;
9
10console.log(getFirstCharacterInUppercase(userInput)); // Output: ''
Rust’s advanced type system helps avoid the part where the user has to signal the developer that sentinel value handling was omitted. Let’s look at two Rust versions of our previous JavaScript function:
1fn get_first_char_in_uppercase1(input: Option<&str>) -> String {
2 match input {
3 Some(s) if !s.is_empty() => {
4 let mut chars = s.chars();
5 match chars.next() {
6 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
7 None => "".to_string(),
8 }
9 }
10 _ => "".to_string(),
11 }
12}
13
14fn get_first_char_in_uppercase2(input: &str) -> String {
15 let mut chars = input.chars();
16 match chars.next() {
17 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
18 None => "".to_string(),
19 }
20}
21
22fn main() {
23 let user_input1: &str = "hello";
24
25 println!("{}", get_first_char_in_uppercase1(user_input1)); // it won't compile because I did not call it with Option parameter
26
27 println!("{}", get_first_char_in_uppercase2(user_input1)); // Output: 'Hello'
28
29 let user_input2: Option<String> = None;
30
31 println!("{}", get_first_char_in_uppercase1(user_input2)); // Output: ''
32
33 println!("{}", get_first_char_in_uppercase2(user_input2)); // it won't compile because I did not call it with &str parameter
34}
Of course, if I really want to have an error reporting cycle with the user, there is also a way to do this in Rust, but
I just need to indicate it separately in the code with .unwrap()
:
1// ...
2fn main() {
3 let user_input2: Option<String> = None;
4
5 println!("{}", get_first_char_in_uppercase2(user_input2.unwrap())); // this will compile, and it will panic at runtime
6}
See more: Option