We have a logs.txt file that we want to:
It's fairly simple to read a file (assuming it's in the root directly):
use std::fs; fn main() { let text = fs::read_to_string("logs.txt"); println!("{:?}", text); }
If we print this, we'll notice that the text
variable at the moment has an Ok
wrapped around it. This is very similar to what we saw with the Option
types.
For a quick aside, imagine we're writing our own divide
function.
Of course, we can't divide by 0, so how do we handle this?
fn divide(a: f64, b: f64) -> _ { if b == 0.0 { // Uh oh... } a / b }
We can use a way to represent this as an operation that can succeed or fail. In this case, we can use the Result
enum.
Sidenote: Divide by 0 in Rust returns infinity, not an error.
Let's see the code in use:
use std::io::Error; fn divide(a: f64, b: f64) -> Result<f64, Error> { if b == 0.0 { Err(Error::other("Something went wrong")) } Ok(a / b) }
Result can return Err<ErrorType>
or Ok<ValueType>
.
For the result definition:
enum Result<T, E> { Ok(T), Err(E) }
We use generics to help indicate what the returning types will be.
Let's focus on the std::io::Error
.
This is a struct that can help represent errors.
A lot of other modules also have custom error structs defined (e.g. in std::num
, std::str
).
There isn't a equivalent of Error
in JavaScript.
If we were to use the divide
function we wrote, we can use pattern matching on it:
fn main() { let result = divide(5.0, 3.0); match result { Ok(value) => println!("{}", value), Err(e) => println!("{}", e) } }
There is also an empty Ok variant if we don't want to return a value.
fn validate_email(email: String) -> Result<(), Error> { if email.contains("@") { Ok(()) } else { Err(Error::other("emails must have @")) } }
The convention is to return an empty tuple.
What is a tuple? It's a way to represent a strict set of related data types. Here's an example:
type Rgb = (u8, u8, u8); fn make_rgb() -> Rgb { (0, 128, 255) } fn main() { let color = make_rgb(); let red = color.0; }
In use:
match validate_email(String::from("abc@email.com")) { Ok(..) => println!("email is valid"), Err(reason) => println!("{}", reason) }
The Ok(..)
is also convention for representing that we acknowledge Ok
has a value, but we don't care about it.
use std::fs; use std::io::Error; fn main() { let text = fs::read_to_string("logs.txt"); match text { Ok(content) => { println!("{}", content.len()) } Err(error) => { println!("Error: {}", error) } } }
Strings in Rust can be a bit trickier to get into compared to other languages. There are several different types of strings.
Let's explore this with a test.
fn string_test( a: String, b: &String, c: &str ) { } fn main() { // One alternative string_test( String::from("a"), &String::from("b"), "c" ); // Another alternative string_test( String::from("a"), &String::from("b"), String::from("c").as_str() ); // Another alternative string_test( "a".to_string(), &"b".to_string(), "c" ); }
Why is this? What are the differences?
To understand how strings work, you need to understand how memory works in Rust.
Think of three memory destinations:
Each area has it's own purpose:
Let's take this code let nums = vec![1,2,3,4,5];
. A very common pattern is the following:
This helps to avoid running out of memory in the stack if the data structure grows to hold a lot of data.
Let's analyze the different string types.
let color = String::from("red");
In this scenario, the data segment already has it stored inside due to the source code.
When we run the code, we end up with a pointer in the stack pointing to the data in the heap.
The stack will have the string struct, the heap will copy the data segment to the heap and the stack will point to the heap.
let color_ref = &color;
In the above ref code, we'll create a reference that sits in the stack which points to the other string struct on the stack that we created previously.
let name = "me";
Finally, the &str
type (known as a string slice) doesn't use the heap out at all.
The literal is still in the data segment. Then in the stack, we'll a struct with only two fields: pointer to the text and the length of the string.
The difference here is that the pointer from the string slice points to the data stored in the data segment directly.
let color = String::from("red"); let c = color.as_str();
We can also have the scenario like above, where we create a string slice from a string.
In that scenario, the string slice struct won't point to the data segment, it will point to the data in the heap that was allocated in color
.
Why does any of this matter from the above types?
&String
and &str
: both provide a read-only reference to text data, so why do they exist?
&str
lets your refer to text in the data segment without a heap allocation, which is slightly more performant.For (2), we can create a string slice as well like so:
let color = String::from("blue"); let portion = &color[1..4];
When to use which type?
Type | When |
---|---|
String | Anytime we want ownership of text or want text to grow or shrink |
&String | Rarely used! Rust will automatically talk them into &str for you |
&str | Anytime you don't want to take ownership or want to refer to a portion of a string owned by someone else |
I've changed my code a little from what they had to use iterators here, but this was the code:
use std::fs; fn extract_errors(text: &str) -> Vec<&str> { text.lines().filter(|line| line.contains("ERROR")).collect() } fn main() { let text = match fs::read_to_string("logs.txt") { Ok(content) => content, Err(error) => { println!("Error: {}", error); // Non-zero exit std::process::exit(1); } }; let errors = extract_errors(&text); println!("Errors: {:#?}", errors); }
In the course, it demonstrated code that runs into an error around lifetimes because it was using the
text.split("\n")
method. This returnsVec<&str>
in that example, which means it's only creating pointers to the original text string. It goes out of scope when it's cleaned up within the thematch
scope. The example they had usedlet mut
at the top scope and tried reassigning in the match block.
In the example from the video, the solution was different to mine.
Instead of returning Vec<&str>
, it returns Vec<String>
and converts the code it was working with to convert the string slices to String
types.
I've added in another match
block for this:
use std::fs; fn extract_errors(text: &str) -> Vec<&str> { text.lines().filter(|line| line.contains("ERROR")).collect() } fn main() { let text = match fs::read_to_string("logs.txt") { Ok(content) => content, Err(error) => { println!("Error: {}", error); std::process::exit(1); } }; match fs::write("errors.txt", extract_errors(&text).join("\n")) { Ok(..) => { println!("Errors written to errors.txt"); } Err(error) => { println!("Error writing file: {}", error); std::process::exit(1); } } }
I never did the nested match myself, since the code that I wrote opted to return values from the match
.
The alternative that I could have gone with is the text.expect("error message")
if I was okay for crashing something if it went wrong. I opted for a more gracefully failure that logs the error.
We could have main
return a Result<(), Error>
if we wanted to.
fn main() -> Result<(), Error> { Err(Error::other("asdf")) }
In the above where we return the Error, Rust will print out to show us what went wrong.
Alternatively, we can use another operator ?
.
The ?
try operator can be added onto functions that result a Result
.
let text = fs::read_to_string("logs.txt")?;
Depending on the variant we get back Ok
or Err
. If it's Ok
, it will automatically unwrap that function and bind it.
In the case that it's an Error
variant, then it will unwrap that error and then return it early.
The refactored code now looks like this:
use std::fs; use std::io::Error; fn extract_errors(text: &str) -> Vec<&str> { text.lines().filter(|line| line.contains("ERROR")).collect() } fn main() -> Result<(), Error> { let text = fs::read_to_string("logs.txt")?; fs::write("errors.txt", extract_errors(&text).join("\n"))?; Ok(()) }
We have a number of options for what technique we can apply now for error handling, so when should we use what?
The following is a guide from the course:
Err()
.