Now that we've got the basics under our belt, we're going to move on to some more complicated things! The primary goal of this chapter is to teach you about two very important things in Rust: ownership and borrowing. We'll be doing this by building another project! Time to bust out cargo new
:
❯ cargo new school-calculations
Created binary (application) `school-calculations` package
❯ cd school-calculations
We're going to read in some data about some students in CSV format, and print some information about them. First, we are going to try to build this parser ourselves, but then we'll switch to using a pre-built one.
Here is the first version of our program:
use std::fs;
fn main() {
let students = fs::read_to_string("students.csv").unwrap();
for line in students.lines() {
let name = line.split(',').next().unwrap();
dbg!(name);
}
}
You'll also need to create a new file, student.csv
, in your school-calculations
directory. Not in the src
subdirectory!
Steve, History, 4.0
Steve, Math, 3.5
Steve, Handwriting, 2.5
Ferris, History, 3.0
Ferris, Math, 4.0
Ferris, Handwriting, 3.5
If you run it, you should see this output:
❯ cargo run
Compiling school-calculations v0.1.0 (C:\\Users\\steve\\tmp\\school-calculations)
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target\\debug\\school-calculations.exe`
[src\\main.rs:8] name = "Steve"
[src\\main.rs:8] name = "Steve"
[src\\main.rs:8] name = "Steve"
[src\\main.rs:8] name = "Ferris"
[src\\main.rs:8] name = "Ferris"
[src\\main.rs:8] name = "Ferris"
Let's break it down. First, we use std::fs
. This is short for "filesystem," and contains lots of goodies for working with files. We then use the fs::read_to_string
function to read a file into a string. This operation may fail; for example, the file may not exist. We're gonna just crash for now, so we use our trusty friend unwrap()
.
We talked a bit about use
previously, but we didn't get into a ton of detail. use
brings a particular name into scope. This means that you can use use
to shorten lines that you may repeat. For example, all three of these variants would work:
// no use at all
std::fs::read_to_string("students.csv").unwrap();
// use the `fs` module, like we did in the program
use std::fs;
fs::read_to_string("students.csv").unwrap();
// use the `read_to_string` function directly!
use std::fs::read_to_string;
read_to_string("students.csv").unwrap();
All of these are valid options! But there's two things I'd like to share: the first is, you may note that you never needed to use std;
for the std
name to be in scope. This is because Rust automatically inserts a use std;
for you; using the standard library is so common, it would end up being in basically every program. The second is, we chose the version we did on purpose: it's generally considered idiomatic to import the parent module, and then use it to call the function. fs::read_to_string
lets you know that read_to_string
comes from somewhere else. The exception to this is types; types use the third version, and are imported directly. We haven't written any type signatures for functions yet, but they get used often enough that it can feel very verbose to include the parent module in them.
In the end, it's your code: use
it how you want to.
The second thing we have to talk about with this call to read_to_string
is the return type. We've seen that string literals have the type &str
. But read_to_string
doesn't return a &str
, it returns a type called String
. This duality is in Rust a lot:
String
: owned type&str
: borrowed typeIn other words, we sometimes say that types "have ownership" or "are borrowed." What does that mean, though? For now, we're going to go with this definition:
String
has ownership of the contents of the string. This means that they can grow, shrink, and generally change.&str
borrows the contents of another string. This means that it's a view into another string. This means that it doesn't grow, shrink, or change.