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:

In 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: