Systemnahe Programmierung mit Rust
https://rust-book.cs.brown.edu
Letzter Update: 2024-11-05
Die Abschnitte 6 bis 19 sind noch in Bearbeitung.
Getting Started
What is the name of the command-line tool for managing the version of Rust on your machine?
Every executable Rust program must contain a function with the name:
Let’s say you have the following program in a file hello.rs:
fn main() {
println!("Hello world!");
}
Say you then run the command rustc hello.rs
from the
command-line. Which statement best describes what happens next?
rustc
will print an error because this is an invalid
program
rustc
reformats hello.rs
according to
the Rust style guide
rustc
executes the program and prints out Hello
world!
rustc
generates a binary executable named
hello
Say you just downloaded a Cargo project, and then you run cargo run at the command-line. Which statement is NOT true about what happens next?
A Guessing Game
No Quiz
Common Programming Concepts
Which statement best describes what it means if a variable x is immutable?
What is the keyword used after let to indicate that a variable can be mutated?
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
fn main() {
let x = 1;
println!("{x}");
+= 1;
x println!("{x}");
}
This program:
( ) DOES compile. Output: __________
( ) does NOT compile
Which of the following statements is correct about the difference between using let and const to declare a variable?
const can be used in the global scope, and let can only be used in a function
They are just different syntaxes for declaring variables with the same semantics
A const can only be assigned to a literal, not an expression involving computation
The compiler will error if a const variable’s name is not using UPPER_SNAKE_CASE
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
const TWO: u32 = 1 + 1;
fn main() {
println!("{TWO}");
}
This program:
( ) DOES compile
( ) does NOT compile
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
fn main() {
let mut x: u32 = 1;
{
let mut x = x;
+= 2;
x }
println!("{x}");
}
This program:
( ) DOES compile. Output __________
( ) does NOT compile
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
fn main() {
let mut x: u32 = 1;
= "Hello world";
x println!("{x}");
}
This program:
( ) DOES compile. Output __________
( ) does NOT compile
The largest number representable by the type i128
is:
2^128
2^128 - 1
2^127
2^127 - 1
if x : u8 = 0
, what will happen when computing
x - 1
?
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
fn main() {
let x: fsize = 2.0;
println!("{x}");
}
This program:
( ) DOES compile: Output __________
( ) does NOT compile
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
fn main() {
let message = "The temperature today is:";
let x = [message, 100];
println!("{} {}", x[0], x[1]);
}
This program:
( ) DOES compile: Output __________
( ) does NOT compile
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
fn main() {
let t = ([1; 2], [3; 4]);
let (a, b) = t;
println!("{}", a[0] + t.1[0]);
}
This program:
( ) DOES compile: Output __________
( ) does NOT compile
The keyword for declaring a new function in Rust is:
Your input: ____
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
fn f(x) {
println!("{x}");
}
fn main() {
0);
f(}
This program:
( ) DOES compile: Output __________
( ) does NOT compile
In Rust, a curly-brace block like { ... }
is:
1 An expression
2 A statement
3 A syntactic scope
2 and 3
1 and 3
1 only
2 only
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
fn f(x: i32) -> i32 {
+ 1
x }
fn main() {
println!(
"{}",
{
f(let y = 1;
+ 1
y })
;
)}
This program:
( ) DOES compile: Output __________
( ) does NOT compile
True/false: executing these two pieces of code results in the same value for x.
Snippet 1:
let x = if cond { 1 } else { 2 };
Snippet 2:
let x;
if cond {
= 1;
x } else {
= 2;
x }
(Note: both of these snippets compile!)
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
fn main() {
let x = 1;
let y = if x { 0 } else { 1 };
println!("{y}");
}
This program:
( ) DOES compile: Output __________
( ) does NOT compile
True/false: this code will terminate (that is, it will not loop forever).
fn main() {
let mut x = 0;
'a: loop {
+= 1;
x 'b: loop {
if x > 10 {
continue 'a;
} else {
break 'b;
}
}
break;
}
}
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
fn main() {
let a = [5; 10];
let mut sum = 0;
for x in a {
+= x;
sum }
println!("{sum}");
}
This program:
( ) DOES compile: Output __________
( ) does NOT compile
4.1 Understanding Ownership
Which of the following is NOT a kind of undefined behavior?
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
fn add_suffix(mut s: String) -> String {
.push_str(" world");
s
s}
fn main() {
let s = String::from("hello");
let s2 = add_suffix(s);
println!("{}", s2);
}
This program:
() DOES compile. Output: ___________
() does NOT compile
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
fn main() {
let s = String::from("hello");
let s2;
let b = false;
if b {
= s;
s2 }
println!("{}", s);
}
This program:
() DOES compile. Output: __________
() does NOT compile
Say we have a function that moves a box, like this:
fn move_a_box(b: Box<i32>) {
// This space intentionally left blank
}
Below are four snippets which are rejected by the Rust compiler. Imagine that Rust instead allowed these snippets to compile and run. Select each snippet that would cause undefined behavior, or select “None of the above” if none of these snippets would cause undefined behavior.
None of the above
let b = Box::new(0);
;
move_a_box(b)println!("{}", b);
let b = Box::new(0);
let b2 = b;
println!("{}", b);
; move_a_box(b2)
let b = Box::new(0);
let b2 = b;
; move_a_box(b)
let b = Box::new(0);
;
move_a_box(b)let b2 = b;
4.2 References and Borrowing
Consider the following program, showing the state of memory after the last line:
let x = Box::new(0);
let y = Box::new(&x); // [L1]
If you wanted to copy out the number 0 through y, how many
dereferences would you need to use? Write your answer as a digit. For
example, if the correct expression is *y
, then the answer
is 1.
Response: ___________
Consider the following program, showing the state of memory after the last line:
fn get_first(vr: &Vec
fn main() {
let mut v = vec![0, 1, 2];
let n = get_first(&v);
println!("{} {}", n, v[1]);
L1}
Which of the following best explains why v is not deallocated after calling get_first?
Consider the permissions in the following program:
At the point marked /* here */
, what are the permissions
on the path s? Select each permission below, or select “no permissions”
if the path has no permissions.
Consider the permissions in the following program:
Which of the following best explains why strs loses and regains write permissions?
get_first returns an immutable reference to data within strs, so strs is not writable while first is live
strs is not writable while the immutable reference &strs passed to get_first is live
strs does not need write permissions until the strs.push(..) operation, so it only regains write permissions at that statement
Because first refers to strs, then strs can only be mutated within a nested scope like the if-statement
Consider this unsafe program:
let v1 = vec![1, 2, 3];
let mut v2 = v1;
.push(4);
v2println!("{}", v1[0]); // L1
Which of the following best describes the point at which undefined behavior occurs in this program?
Response
v1 has been moved into v2 on line 2
v1[0] reads v1, which points to deallocated memory
v1 has its pointer invalidated by the push on line 3
v2 owns the vector data on the heap, while v1 does not
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
fn incr(n: &mut i32) {
*n += 1;
}
fn main() {
let mut n = 1;
&n);
incr(println!("{n}");
}
This program:
() DOES compile. Output: __________
() does NOT
compile
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
fn main() {
let mut s = String::from("hello");
let s2 = &s;
let s3 = &mut s;
.push_str(" world");
s3println!("{s2}");
}
This program:
() DOES compile. Output __________
() does NOT
compile
Consider this Rust function that pushes a number onto the end of a vector, and then removes and returns the number from the front of the vector:
fn give_and_take(v: &Vec<i32>, n: i32) -> i32 {
.push(n);
v.remove(0)
v}
Normally, if you try to compile this function, the compiler returns the following error:
error[E0596]: cannot borrow `*v` as mutable, as it is behind a `&` reference
--> test.rs:2:5
|
1 | fn give_and_take(v: &Vec<i32>, n: i32) -> i32 {
| --------- help: consider changing this to be a mutable reference: `&mut Vec<i32>`
2 | v.push(n);
| ^^^^^^^^^ `v` is a `&` reference, so the data it refers to cannot be borrowed as mutable
Assume that the compiler did NOT reject this function. Select each (if any) of the following programs that could possibly cause undefined behavior if executed. If none of these programs could cause undefined behavior, then check “None of these programs” .
let v = vec![1, 2, 3];
let n = &v[0];
&v, 4);
give_and_take(println!("{}", n);
let v = vec![1, 2, 3];
let v2 = &v;
&v, 4);
give_and_take(println!("{}", v2[0]);
let v = vec![1, 2, 3];
let n = &v[0];
let k = give_and_take(&v, 4);
println!("{}", k);
4.3 Fixing Ownership Errors
Which of the following is NOT a valid kind of fix to the issue of returning a stack reference from a function?
Let’s say a programmer tried writing the following function:
/// Returns a person's name with "Ph.D." added as a title
fn award_phd(name: &String) -> String {
let mut name = *name;
.push_str(", Ph.D.");
name
name}
The Rust compiler rejects their code with the following error:
error[E0507]: cannot move out of `*name` which is behind a shared reference
--> test.rs:3:20
|
3 | let mut name = *name;
| ^^^^^
| |
| move occurs because `*name`has type`String`, which does not implement the `Copy`trait
| help: consider borrowing here:`&*name`
Given the stated purpose of the function, which of the following would be the most idiomatic fix to the program? The differences from the function above are highlighted.
Let’s say a programmer tried writing the following function:
/// Rounds all the floats in a vector to the nearest integer, in-place
fn round_in_place(v: &Vec<f32>) {
for n in v {
*n = n.round();
}
}
The Rust compiler rejects their code with the following error:
error[E0594]: cannot assign to `*n`, which is behind a `&` reference
--> test.rs:4:9
|
3 | for n in v {
| - this iterator yields `&` references
4 | *n = n.round();
| ^^^^^^^^^^^^^^ `n` is a `&` reference, so the data it refers to cannot be written
Given the stated purpose of the function, which of the following would be the most idiomatic fix to the program? The differences from the function above are highlighted.
Which of the following best explains why an i32 can be copied without a move, but a String cannot?
The following code snippet does not compile:
let s = String::from("Hello world");
let s_ref = &s;
let s2 = *s_ref;
println!("{s2}");
Which of the following best describes the undefined behavior that could occur if this program were allowed to execute?
Response
*s_ref
is a use of freed memoryThe following program does not compile:
fn copy_to_prev(v: &mut Vec<i32>, i: usize) {
let n = &mut v[i];
*n = v[i - 1];
}
fn main() {
let mut v = vec![1, 2, 3];
&mut v, 1);
copy_to_prev(}
Which of the following best describes the undefined behavior that could occur if this program were allowed to execute?
Response
v[i - 1]
is a use of freed memory*n
is a use of freed memory&mut v[i]
creates a pointer to freed
memoryConsider this function that is a simplified variant of the function from the previous quiz:
/// Adds "Ph.D." to a person's name
fn award_phd(name: &String) {
let mut name = *name;
.push_str(", Ph.D.");
name}
The Rust compiler rejects this function with the following error:
error[E0507]: cannot move out of `*name` which is behind a shared reference
--> test.rs:3:20
|
3 | let mut name = *name;
| ^^^^^
| |
| move occurs because `*name`has type`String`, which does not implement the `Copy`trait
| help: consider borrowing here:`&*name`
Assume that the compiler did NOT reject this function. Select each (if any) of the following programs that could possibly cause undefined behavior if executed. If none of these programs could cause undefined behavior, then check “None of these programs” .
Response
let name = String::from("Ferris");
let name_ref = &name;
&name);
award_phd(println!("{}", name_ref);
None of these programs
let name = String::from("Ferris");
&name); award_phd(
let name = String::from("Ferris");
&name);
award_phd(println!("{}", name);
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
fn main() {
let mut point = [0, 1];
let mut x = point[0];
let y = &mut point[1];
+= 1;
x *y += 1;
println!("{} {}", point[0], point[1]);
}
This program:
() DOES compile. Output: __________
() does NOT compile
4.4 The Slice Type
Consider the variables s2 and s3 in the following program. These two variables will be located in memory within the stack frame for main. Each variable has a size in memory on the stack, not including the size of pointed data. Which statement is true about the sizes of s2 and s3?
fn main() {
let s = String::from("hello");
let s2: &String = &s;
let s3: &str = &s[..];
}
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
fn main() {
let mut s = String::from("hello");
for &item in s.as_bytes().iter() {
if item == b'l' {
.push_str(" world");
s}
}
println!("{s}");
}
This program:
() DOES compile. Output: __________
() does NOT compile
4.5 Ownership Recap
Say you are writing a function with the following spec:
round_all takes as input a list of floating point numbers, and it rounds each number in-place to the nearest integer.
Which of the following is the most appropriate type signature for a function implementing this spec?
fn round_all(v: &Vec<f32>);
fn round_all(v: &Vec<f32>) -> Vec<f32>;
fn round_all(v: &mut Vec<f32>);
fn round_all(v: Vec<f32>);
Say you are writing a function with the following spec:
find_contains
takes as input a collection of strings and
a target substring. It returns a list of all the strings in the
collection that contain the target substring.
Which of the following is the most appropriate type signature for a function implementing this spec?
fn find_contains(haystack: &[String], needle: &str) -> Vec<&String>;
fn find_contains(haystack: &Vec<String>, needle: &str) -> &[String];
fn find_contains(haystack: &Vec<String>, needle: String) -> Vec<String>;
fn find_contains(haystack: &[String], needle: &str) -> Vec<String>;
Rust normally disallows multiple mutable accesses to the same array, even when those accesses are disjoint. For example, this function does not compile:
fn main() {
let mut v = vec![0, 1, 2, 3];
let (r0, r1) = (&mut v[0..2], &mut v[2..4]);
0] += 1;
r0[0] += 1;
r1[}
However, the Rust standard library has a function
slice::split_at_mut
that does permit this
functionality:
fn main() {
let mut v = vec![0, 1, 2, 3];
let (r0, r1) = v.split_at_mut(2);
0] += 1;
r0[0] += 1;
r1[}
Which of the following best describes how it’s possible to implement
split_at_mut
?
split_at_mut is a special compiler primitive that cannot be implemented within the language
split_at_mut uses unsafe code to disable the borrow checker from checking the safety of mutable references
split_at_mut uses unsafe code to circumvent the borrow checker with raw pointers
split_at_mut calls into a C library that can’t be analyzed by Rust
Consider the permissions in the following program:
Which of the following best explains why *s_ref
does not
have the O (own) permission?
Ownership permits reading, and reading *s_ref
can
cause a use-after-free
Ownership permits moving, and moving out of a reference can cause a double-free
Ownership permits borrowing, and reborrowing *s_ref
can cause a double-free
Ownership permits mutation, and mutating *s_ref
can
cause a use-after-free
Consider the set of Rust programs that contain no unsafe code. Select each of the following statements that is true about the kinds of programs accepted and rejected by the borrow checker:
fn extract(b: &Box<i32>) -> i32 {
let b2: Box<i32> = *b;
*b2
}
Imagine that the borrow checker did not reject this function. Determine whether there exists an input such that the function would cause undefined behavior if executed on that input.
The function transfer_string is rejected by the borrow checker:
fn get_first(strs: &mut (String, String)) -> &mut String {
&mut strs.0
}
fn get_second(strs: &mut (String, String)) -> &mut String {
&mut strs.1
}
fn transfer_string(strs: &mut (String, String)) {
let fst = get_first(strs);
let snd = get_second(strs);
.push_str(snd);
fst.clear();
snd}
Imagine that the borrow checker did not reject this function. Determine whether there exists an input such that the function would cause undefined behavior if executed on that input.
Response
This function could NOT cause undefined behavior
This function COULD cause undefined behavior
Using Structs
5.1 Defining and instantiating structs
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
struct Point {
: i32,
x: i32,
y}
fn main() {
let mut a = Point { x: 1, y: 2 };
.x += 1;
alet b = Point { y: 1, ..a };
.x += 1;
aprintln!("{}", b.x);
}
This program:
( ) DOES compile. Output __________
( ) does NOT compile
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
struct Point {
: i32,
x: i32,
y}
fn main() {
let mut p = Point { x: 1, y: 2 };
let x = &mut p.x;
let y = &mut p.y;
*x += 1;
*y += 1;
println!("{} {}", p.x, p.y);
}
This program:
( ) DOES compile. Output: __________
( ) does NOT compile
5.2 An example program using structs
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
#[derive(Debug)]
struct Rectangle {
: u32,
width: u32,
height}
fn main() {
let rect1 = Rectangle {
: 30,
width: 50,
height};
let a = area(rect1);
println!("{} * {} = {}", rect1.width, rect1.height, a);
}
fn area(rectangle: Rectangle) -> u32 {
.width * rectangle.height
rectangle}
This program:
( ) DOES compile. Output: __________
( ) does NOT compile
Which statement best describes a difference between the Display and Debug traits?
There is no difference, Display and Debug are aliases for the same trait.
Display is for presenting values to an end-user, while Debug is for developers’ internal use
Display is for printing values to the console, while Debug is for viewing values in a debugger
Display cannot be implemented for structs, while Debug can be implemented for structs
5.3 Method Syntax
What is the keyword for constructor functions in Rust?
struct Point(i32, i32);
fn main() {
let p = Point(1, 2);
impl p {
fn x(&self) -> i32 {
self.0
}
}
println!("{}", p.x());
}
This program:
( ) DOES compile. Output: __________
() does NOT compile
Say you have a variable v
of type
&mut Vec<i32>
, and you want to call the
len
method with the following signature:
impl Vec<i32> {
fn len(&self) -> usize {
/* ... */
}
}
If you try to compile the expression v.len()
, which of
the following statements best describes what happens?
&self
can take any kind of
reference&mut
reference is implicitly
reborrowed as an &
referencev
is not explicitly
dereferenced&mut Vec<i32>
is
not the same type as &Vec<i32>
Consider these two methods that increment a field of a struct. Which style would be more idiomatic for Rust?
struct Point(i32, i32);
impl Point {
fn incr_v1(mut self) {
self.0 += 1;
}
fn incr_v2(&mut self) {
self.0 += 1;
}
}
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
struct Point(i32, i32);
impl Point {
fn incr_x(&mut self) {
self.0 += 1;
}
}
fn main() {
let mut p = Point(0, 0);
.incr_x();
pprintln!("{}", p.0);
}
This program:
( ) DOES compile. Output: __________
( ) does NOT compile
Determine whether the program will pass the compiler. If it passes, write the expected output of the program if it were executed.
struct Point {
: i32,
x: i32,
y}
impl Point {
fn get_x(&mut self) -> &mut i32 {
&mut self.x
}
}
fn main() {
let mut p = Point { x: 1, y: 2 };
let x = p.get_x();
*x += 1;
println!("{} {}", *x, p.y);
}
This program:
( ) DOES compile. Output: __________
( ) does NOT compile