In today's lecture, we will talk about
- struct/impl
- lifetime
A struct, or structure, is a custom data type that lets you name and package together multiple related values that make up a meaningful group. Struct allows you to group multiple pieces of data with different types together. You can name each piece of data and query them by name. Each piece of data and its name is called a field. More at TRPL and Rust By Example.
A structure (struct) can be defined by the keyword struct
, followed by the name of the Struct
, and then the fields in the Struct.
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
To instantiate a struct, use the struct name followed by curly brankets and specify the value for each field.
let user1 = User {
email: String::from("[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
struct
can be mutable, but making a single field mutable is not allowed.
let mut user1 = User {
email: String::from("[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
user1.email = String::from("[email protected]");
When the name of a field is the same as a valid variable, we can use the field init shorthand syntax to remove some repetition.
let email = String::from("[email protected]");
let username = String::from("someusername123");
let user = User {
email,
username,
active: true,
sign_in_count: 1,
};
Using struct update syntax, you can use one instance to help to create another instance of a Struct.
let user1 = User {
email: String::from("[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
// The following two commands are same
// with struct update syntax
let user2 = User {
email: String::from("[email protected]"),
username: String::from("anotherusername567"),
..user1
};
// without struct update syntax
let user2 = User {
email: String::from("[email protected]"),
username: String::from("anotherusername567"),
active: user1.active,
sign_in_count: user1.sign_in_count,
};
Structs can be very similar to tuples, called tuple structs.
struct Color(i32, i32, i32);
let black = Color(0, 0, 0);
You can destructure the fields into their individual pieces, you can use a .
followed by the index to access an individual value.
Rust provides many derived traits for us. These traits can be plug-and-use. Some of them are very useful for developing your program, such as Debug
trait. To use these derived traits, you need to add the annotation just before the struct definition. For example, #[derive(Debug)]
. This trait lets Rust know how to print your Structs.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {:?}", rect1);
}
Putting the specifier :?
inside the curly brackets tells println!
we want to use Debug
output format. If your struct is very complex, you can use :#?
to make the output nicer.
In OOP, sometimes you want to assign some specific methods that an instance of a struct can call. In Rust, this is called methods of structs.
To implement methods for structs, we need to use the impl
keyword. One struct can have multiple impl
blocks.
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
fn double(&mut self) {
*self.width = self.width * 2;
*self.height = self.height * 2;
}
fn take_ownership(self) {
}
}
fn main() {
let mut rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 60,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
rect1.double();
println!("Can doubled rect1 hold rect2? {}", rect1.can_hold(&rect2));
}
In the signature for the area()
, we use &self
instead of rectangle: &Rectangle
because Rust knows the type of self
is Rectangle
due to this method’s being inside the impl Rectangle
context. Note that we still need to use the &
before self
, just as we did in &Rectangle
. Methods can take ownership of self, borrow self
immutably as we’ve done here, or borrow self
mutably, just as they can any other parameter.
Sometimes we want to define functions for structs. The difference between a method and a function of a struct is that a function of a struct doesn't need an instance of the struct to work with.
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
To call this associated function, we use the ::
syntax with the struct name; let sq = Rectangle::square(3);
is an example. This function is namespaced by the struct: the ::
syntax is used for both associated functions and namespaces created by modules.
Structs can store references to data owned by something else, but to do so requires the use of lifetimes.
Lifetimes ensure that the data referenced by a struct is valid for as long as the struct is. Let’s say you try to store a reference in a struct without specifying lifetimes, like this, which won’t work:
struct User {
username: &str,
email: &str,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: "[email protected]",
username: "someusername123",
active: true,
sign_in_count: 1,
};
}
The compiler will complain that it needs lifetime specifiers.
The lifetime of a variable starts at the time when the variable is defined and ends after the last time that the variable is used.
{
let r; // ---------+-- 'a
// |
{ // |
let x = 5; // -+-- 'b |
r = &x; // | |
} // -+ |
// |
println!("r: {}", r); // |
} // ---------+
Lifetime of a reference is not always explicit. For example:
fn longest(x:&str, y:&str) -> &str {
if x.len() > y.len() { x } else { y}
} // this funcion doesn't work
When running this function, Rust doesn't know the lifetime of x
and y
. The following situation may happen:
{
let x = String::from(“hi”);
let z;
{ let y = String::from(“there”);
z = longest(&x,&y); //will be &y
} //drop y, and thereby z
println!(“z = {}”,z);//yikes!
}
To fix it, we need to explicitly specify that x
and y
must have the same lifetime, and the returned reference shares it. This can be done using the apostrophe '
followed by a lowercase, like generic types. By convention, we use 'a
.
&i32 // a reference
&'a i32 // a reference with an explicit lifetime
&'a mut i32 // a mutable reference with an explicit lifetime
So, the problem of the previous longest()
definition is that that function may return x
or y
, so Rust cannot tell what's the exact lifetime of the return value. To solve this, we need to tell Rust explicitly that x
and y
need to have the same lifetime, or we will return the one with the shorter lifetime. Lifetimes on function or method parameters are called input lifetimes, and lifetimes on return values are called output lifetimes.
fn longest<'a>(x:&'a str, y:&'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
Note:
- Each reference to a value of type
t
has alifetime parameter
.&t
(and&mut t
) – lifetime is implicit&'a t
(and&'a mut t
) – lifetime'a
is explicit
- Where do the lifetime names come from?
- When left implicit, they are generated by the compiler
- Global variables have lifetime
'static
, which are encoded in the binary of the program.
-
How does the Rust compiler figure out lifetimes?
- The first rule is that each parameter that is a reference gets its lifetime parameter. In other words, a function with one parameter gets one lifetime parameter:
fn foo<'a>(x: &'a i32)
; a function with two parameters gets two separate lifetime parameters:fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
; and so on. - The second rule is if there is exactly one input lifetime parameter, that lifetime is assigned to all output lifetime parameters:
fn foo<'a>(x: &'a i32) -> &'a i32
. - The third rule is if there are multiple input lifetime parameters, but one of them is
&self
or&mut self
because this is a method, the lifetime ofself
is assigned to all output lifetime parameters. This third rule makes methods much nicer to read and write because fewer symbols are necessary.
- The first rule is that each parameter that is a reference gets its lifetime parameter. In other words, a function with one parameter gets one lifetime parameter:
-
When do we use explicit lifetimes?
- When more than one var/type needs the same lifetime (like the
longest
function)
- When more than one var/type needs the same lifetime (like the
-
How do I tell the compiler exactly which lines of code lifetime
'a
covers?- You can't. The compiler will (always) figure it out
-
How does lifetime subsumption work?
- If lifetime
'a
is longer than'b
, we can use'a
where'b
is expected; can require this with'b: 'a
.- Permits us to call
longest(&x,&y)
whenx
andy
have different lifetimes, but one outlives the other.
fn longest<'b, 'a: 'b>(x:&'a str, y:&'b str) -> &'b str { if x.len() > y.len() { x } else { y} } // this funcion doesn't work
- Permits us to call
- If lifetime
-
Can we use lifetimes in data definitions?
- Yes; we will see this later when we define structs, enums, etc.
Rust has been updated to support NLL -- lifetimes that end before the surrounding scope:
fn main() { // SCOPE TREE
//
let mut names = // +- `names` scope start
["abe", "beth", "cory", "diane"]; // |
// |
let alias = &mut names[0]; // | +- `alias` scope start
// | |
*alias = "alex"; // <------------------------ write to `*alias`
// | |
println!("{}", names[0]); // <--------------- read of `names[0]`
// | |
// | +- `alias` scope end
// +- `name` scope end
}
In practice, this is important when
fn main() {
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// r1 and r2 are no longer used after this point
let r3 = &mut s; // no problem
println!("{}", r3);
}
So how do we use references in struct definition?
struct User<'a> {
username: &'a str,
email: &'a str,
sign_in_count: u64,
active: bool,
}
#![allow(unused)]
fn main() {
let user1 = User {
email: "[email protected]",
username: "someusername123",
active: true,
sign_in_count: 1,
};
}
-
Will the following code compile? why? If not, how to fix it?
fn main() { let x = String::from("hello"); let y = x; println!("{}, world!", y); println!("{}, world!", x); }
- No, becuase it called
x
after moving its value toy
. To fix this, we can definey
aslet y = x.clone()
- No, becuase it called
-
Will the following code compile? why? If not, how to fix it?
let x = 5; let y = x; println!("{} = 5!", y); println!("{} = 5!", x);
- Yes, it can compile. This is because primitive types implement
Copy
trait, so their value will be copied when assigning.
- Yes, it can compile. This is because primitive types implement
-
Owner of str’s data at HERE ?
fn foo(str:String) { let x = str; let y = &x; let z = x; let w = &y; // HERE }
- x
- y
- z
- w
- z, because x is assigned to z.
-
in question #3, when will the value of
x
be dropped?- when the scope defined by the curly brackets ends.
-
If we have a string
let s = String::from("hello")
, What are the differences (structure, layer of indirection) between&s
and&s[..]
?&s
is an immutable reference. it consists of a pointer to the string variable, which locates on the stack and has a pointer to the actual value in the heap.&s[..]
is a string slice. It consists of a pointer and a length, and its pointer points directly to the starting position of the actual value in the heap.
-
Will the following code compile? why? If not, how to fix it?
fn main() { let s1 = String::from(“hello”); let s2 = id(s1); println!(“{}”,s2); println!(“{}”,s1); } fn id(s:String) -> String { s }
-
No, because the function took ownership of the string, so we cannot access it after evaluating the function. To fix this, the function should take a reference as the input.
fn id(s:&String) -> String { s.to_string() }
- What does this evaluate to?
{ let mut s1 = String::from(“Hello!“);
{
let s2 = &s1;
s2.push_str(“World!“);
println!(“{}“, s2)
}
}
A. "Hello!"
B. "Hello! World!"
C. Error
D. "Hello!World!"
- C.
s2
is an immutable reference, we cannot use it to modify the value.
- What is printed?
fn foo(s: &mut String) -> usize {
s.push_str("Bob");
s.len()
}
fn main() {
let mut s1 = String::from("Alice");
println!("{}",foo(&mut s1));
}
A. 0
B. 8
C. Error
D. 5
- B. The string is modified using
s1
as "AliceBob". So the total length is$8$ .
- What's wrong here?
let mut s1 = String::from(“hello”);
{
let s2 = &s1;
let s3 = &s1;
let s4 = &mut s1;
let s5 = &mut s1;
println!(”String is {}”,s1);
println!(”String is {}”,s2);
println!(”String is {}”,s3);
println!(”String is {}”,s4);
}
s1.push_str(“ there”);
println!(”String is {}”,s1);
- We can create multiple immutable references OR one mutable reference.