- Structured Data
- CIS 198 Lecture 2
- Structured Data
- Structs
- Structs
- Structs
- Structs
- Structs
- Enums
- Enums
- Enums
- Recursive Types
- Recursive Types
- Boxes, Briefly
- Methods
- Methods
- Methods
- Associated Functions
- Implementations
- Patterns
- Lifetimes
- Lifetimes
- Lifetimes
- Stop here briefly to discuss
- Lifetimes
- Multiple Lifetime Parameters
- Lifetimes
- Lifetimes - structs
- Live demo!
- Lifetimes - structs
- Lifetimes - 'static
Structured Data
CIS 198 Lecture 2
Structured Data
-
Rust has two simple ways of creating structured data types:
-
Structs: C-like structs to hold data.
- Enums: OCaml-like; data that can be one of several types.
- Structs and enums may have one or more implementation blocks (
impls) which define methods for the data type.
Structs
- A struct declaration:
- Fields are declared with
name: type.
1. struct Point {
2. x: i32,
3. y: i32,
4. }
- By convention, structs have
CamelCasenames, and their fields havesnake_casenames. - Structs may be instantiated with fields assigned in braces.
1. let origin = Point { x: 0, y: 0 };
Structs
- Struct fields may be accessed with dot notation.
- Structs may not be partially-initialized.
- You must assign all fields upon creation, or declare an uninitialized struct that you initialize later.
1. let mut p = Point { x: 19, y: 8 };
2. p.x += 1;
3. p.y -= 1;
Structs
- Structs do not have field-level mutability.
- Mutability is a property of the variable binding, not the type.
- Field-level mutability (interior mutability) can be achieved via
Celltypes. - More on these very soon.
1. struct Point {
2. x: i32,
3. mut y: i32, // Illegal!
4. }
Structs
- Structs are namespaced with their module name.
- The fully qualified name of
Pointisfoo::Point. - Struct fields are private by default.
- They may be made public with the
pubkeyword. - Private fields may only be accessed from within the module where the struct is declared.
1. mod foo {
2. pub struct Point {
3. pub x: i32,
4. y: i32,
5. }
6. }
8. fn main() {
9. let b = foo::Point { x: 12, y: 12 };
10. // ^~~~~~~~~~~~~~~~~~~~~~~~~~~
11. // error: field `y` of struct `foo::Point` is private
12. }
Structs
1. mod foo {
2. pub struct Point {
3. pub x: i32,
4. y: i32,
5. }
7. // Creates and returns a new point
8. pub fn new(x: i32, y: i32) -> Point {
9. Point { x: x, y: y }
10. }
11. }
newis inside the same module asPoint, so accessing private fields is allowed.
Struct matching
- Destructure structs with
matchstatements.
1. pub struct Point {
2. x: i32,
3. y: i32,
4. }
6. match p {
7. Point { x, y } => println!("({}, {})", x, y)
8. }
Struct matching
- Some other tricks for struct
matches:
1. match p {
2. Point { y: y1, x: x1 } => println!("({}, {})", x1, y1)
3. }
5. match p {
6. Point { y, .. } => println!("{}", y)
7. }
- Fields do not need to be in order.
- List fields inside braces to bind struct members to those variable names.
- Use
struct_field: new_var_bindingto change the variable it’s bound to. - Omit fields: use
..to ignore all unnamed fields.
Struct Update Syntax
- A struct initializer can contain
.. sto copy some or all fields froms. - Any fields you don’t specify in the initializer get copied over from the target struct.
- The struct used must be of the same type as the target struct.
- No copying same-type fields from different-type structs!
1. struct Foo { a: i32, b: i32, c: i32, d: i32, e: i32 }
3. let mut x = Foo { a: 1, b: 1, c: 2, d: 2, e: 3 };
4. let x2 = Foo { e: 4, .. x };
6. // Useful to update multiple fields of the same struct:
7. x = Foo { a: 2, b: 2, e: 2, .. x };
Tuple Structs
- Variant on structs that has a name, but no named fields.
- Have numbered field accessors, like tuples (e.g.
x.0,x.1, etc). - Can also
matchthese.
1. struct Color(i32, i32, i32);
3. let mut c = Color(0, 255, 255);
4. c.0 = 255;
5. match c {
6. Color(r, g, b) => println!("({}, {}, {})", r, g, b)
7. }
Tuple Structs
- Helpful if you want to create a new type that’s not just an alias.
- This is often referred to as the “newtype” pattern.
- These two types are structurally identical, but not equatable.
1. // Not equatable
2. struct Meters(i32);
3. struct Yards(i32);
5. // May be compared using `==`, added with `+`, etc.
6. type MetersAlias = i32;
7. type YardsAlias = i32;
Unit Structs (Zero-Sized Types)
- Structs can be declared to have zero size.
- This struct has no fields!
- We can still instantiate it.
- It can be used as a “marker” type on other data structures.
- Useful to indicate, e.g., the type of data a container is storing.
1. struct Unit;
3. let u = Unit;
Enums
- An enum, or “sum type”, is a way to express some data that may be one of several things.
- Much more powerful than in Java, C, C++, C#…
- Each enum variant can have:
- no data (unit variant)
- named data (struct variant)
- unnamed ordered data (tuple variant)
1. enum Resultish {
2. Ok,
3. Warning { code: i32, message: String },
4. Err(String)
5. }
Enums
- Enum variants are namespaced by their enum type:
Resultish::Ok. - You can import all variants with
use Resultish::*. - Enums, much as you’d expect, can be matched on like any other data type.
1. match make_request() {
2. Resultish::Ok =>
3. println!("Success!"),
4. Resultish::Warning { code, message } =>
5. println!("Warning: {}!", message),
6. Resultish::Err(s) =>
7. println!("Failed with error: {}", s),
8. }
Enums
- Enum constructors like
Resultish::Okand the like can be used as functions. - This is not currently very useful, but will become so when we cover closures & iterators.
Recursive Types
- You might think to create a nice functional-style
Listtype:
1. enum List {
2. Nil,
3. Cons(i32, List),
4. }
Recursive Types
- Such a definition would have infinite size at compile time!
- Structs & enums are stored inline by default, so they may not be recursive.
- i.e. elements are not stored by reference, unless explicitly specified.
- The compiler tells us how to fix this, but what’s a
box?
1. enum List {
2. Nil,
3. Cons(i32, List),
4. }
5. // error: invalid recursive enum type
6. // help: wrap the inner value in a box to make it representable
Boxes, Briefly
- A
box(lowercase) is a general term for one of Rust’s ways of allocating data on the heap. - A
Box<T>(uppercase) is a heap pointer with exactly one owner. - A
Boxowns its data (theT) uniquely— it can’t be aliased. Boxes are automatically destructed when they go out of scope.- Create a
BoxwithBox::new():
1. let boxed_five = Box::new(5);
3. enum List {
4. Nil,
5. Cons(i32, Box<List>), // OK!
6. }
- We’ll cover these in greater detail when we talk more about pointers.
Methods
1. impl Point {
2. pub fn distance(&self, other: Point) -> f32 {
3. let (dx, dy) = (self.x - other.x, self.y - other.y);
4. ((dx.pow(2) + dy.pow(2)) as f32).sqrt()
5. }
6. }
8. fn main() {
9. let p = Point { x: 1, y: 2 };
10. p.distance();
11. }
- Methods can be implemented for structs and enums in an
implblock. - Like fields, methods may be accessed via dot notation.
- Can be made public with
pub. implblocks themselves don’t need to be madepub.- Work for enums in exactly the same way they do for structs.
Methods
- The first argument to a method, named
self, determines what kind of ownership the method requires. &self: the method borrows the value.- Use this unless you need a different ownership model.
&mut self: the method mutably borrows the value.- The function needs to modify the struct it’s called on.
self: the method takes ownership.- The function consumes the value and may return something else.
Methods
1. impl Point {
2. fn distance(&self, other: Point) -> f32 {
3. let (dx, dy) = (self.x - other.x, self.y - other.y);
4. ((dx.pow(2) + dy.pow(2)) as f32).sqrt()
5. }
7. fn translate(&mut self, x: i32, y: i32) {
8. self.x += x;
9. self.y += y;
10. }
12. fn mirror_y(self) -> Point {
13. Point { x: -self.x, y: self.y }
14. }
15. }
distanceneeds to access but not modify fields.translatemodifies the struct fields.mirror_yreturns an entirely new struct, consuming the old one.
Associated Functions
1. impl Point {
2. fn new(x: i32, y: i32) -> Point {
3. Point { x: x, y: y }
4. }
5. }
7. fn main() {
8. let p = Point::new(1, 2);
9. }
- Associated function: like a method, but does not take
self. - This is called with namespacing syntax:
Point::new().- Not
Point.new().
- Not
- Like a “static” method in Java.
- A constructor-like function is usually named
new. - No inherent notion of constructors, no automatic construction.
Implementations
- Methods, associated functions, and functions in general may not be overloaded.
- e.g.
Vec::new()andVec::with_capacity(capacity: usize)are both constructors forVec - Methods may not be inherited.
- Rust structs & enums must be composed instead.
- However, traits (coming soon) have basic inheritance.
Patterns
- Use
...to specify a range of values. Useful for numerics andchars. - Use
_to bind against any value (like any variable binding) and discard the binding.
1. let x = 17;
3. match x {
4. 0 ... 5 => println!("zero through five (inclusive)"),
5. _ => println!("You still lose the game."),
6. }
match: References
- Get a reference to a variable by asking for it with
ref.
1. let x = 17;
3. match x {
4. ref r => println!("Of type &i32: {}", r),
5. }
- And get a mutable reference with
ref mut. - Only if the variable was declared
mut.
1. let mut x = 17;
3. match x {
4. ref r if x == 5 => println!("{}", r),
5. ref mut r => *r = 5
6. }
- Similar to
let ref.
if-let Statements
- If you only need a single match arm, it often makes more sense to use Rust’s
if-letconstruct. - For example, given the
Resultishtype we defined earlier:
1. enum Resultish {
2. Ok,
3. Warning { code: i32, message: String },
4. Err(String),
5. }
if-let Statements
- Suppose we want to report an error but do nothing on
Warnings andOks.
1. match make_request() {
2. Resultish::Err(_) => println!("Total and utter failure."),
3. _ => println!("ok."),
4. }
- We can simplify this statement with an
if-letbinding:
1. let result = make_request();
3. if let Resultish::Err(s) = result {
4. println!("Total and utter failure: {}", s);
5. } else {
6. println!("ok.");
7. }
while-let Statement
- There’s also a similar
while-letstatement, which works like anif-let, but iterates until the condition fails to match.
1. while let Resultish::Err(s) = make_request() {
2. println!("Total and utter failure: {}", s);
3. }
Inner Bindings
- With more complicated data structures, use
@to create variable bindings for inner elements.
1. #[derive(Debug)]
2. enum A { None, Some(B) }
3. #[derive(Debug)]
4. enum B { None, Some(i32) }
6. fn foo(x: A) {
7. match x {
8. a @ A::None => println!("a is A::{:?}", a),
9. ref a @ A::Some(B::None) => println!("a is A::{:?}", *a),
10. A::Some(b @ B::Some(_)) => println!("b is B::{:?}", b),
11. }
12. }
14. foo(A::None); // ==> x is A::None
15. foo(A::Some(B::None)); // ==> a is A::Some(None)
16. foo(A::Some(B::Some(5))); // ==> b is B::Some(5)
Lifetimes
- There’s one more piece to the ownership puzzle: Lifetimes.
- Lifetimes generally have a pretty steep learning curve.
- We may cover them again later on in the course under a broader scope if necessary.
- Don’t worry if you don’t understand these right away.
Lifetimes
- Imagine This:
- I acquire a resource.
- I lend you a reference to my resource.
- I decide that I’m done with the resource, so I deallocate it.
- You still hold a reference to the resource, and decide to use it.
- You crash 😿.
- We’ve already said that Rust makes this scenario impossible, but glossed over how.
- We need to prove to the compiler that step 3 will never happen before step 4.
Lifetimes
- Ordinarily, references have an implicit lifetime that we don’t need to care about:
1. fn foo(x: &i32) {
2. // ...
3. }
- However, we can explicitly provide one instead:
1. fn bar<'a>(x: &'a i32) {
2. // ...
3. }
- 'a, pronounced “tick-a” or “the lifetime a“ is a named lifetime
parameter.
<'a>declares generic parameters, including lifetime parameters.- The type
&'a i32is a reference to ani32that lives at least as long as the lifetime'a.
???
Stop here briefly to discuss
Lifetimes
- The compiler is smart enough not to need
'aabove, but this isn’t always the case. - Scenarios that involve multiple references or returning references often require explicit lifetimes.
- Speaking of which…
Multiple Lifetime Parameters
1. fn borrow_x_or_y<'a>(x: &'a str, y: &'a str) -> &'a str;
- In
borrow_x_or_y, all input/output references all have the same lifetime. xandyare borrowed (the reference is alive) as long as the returned reference exists.
1. fn borrow_p<'a, 'b>(p: &'a str, q: &'b str) -> &'a str;
- In
borrow_p, the output reference has the same lifetime asp. qhas a separate lifetime with no constrained relationship top.pis borrowed as long as the returned reference exists.
Lifetimes
- Okay, great, but what does this all mean?
- If a reference
Rhas a lifetime'a, it is guaranteed that it will not outlive the owner of its underlying data (the value at*R) - If a reference
Rhas a lifetime of'a, anything else with the lifetime'ais guaranteed to live as longR. - This will probably become more clear the more you use lifetimes yourself.
Lifetimes - structs
- Structs (and struct members) can have lifetime parameters.
1. struct Pizza(Vec<i32>);
2. struct PizzaSlice<'a> {
3. pizza: &'a Pizza, // <- references in structs must
4. index: u32, // ALWAYS have explicit lifetimes
5. }
7. let p1 = Pizza(vec![1, 2, 3, 4]);
8. {
9. let s1 = PizzaSlice { pizza: &p1, index: 2 }; // this is okay
10. }
12. let s2;
13. {
14. let p2 = Pizza(vec![1, 2, 3, 4]);
15. s2 = PizzaSlice { pizza: &p2, index: 2 };
16. // no good - why?
17. }
???
Live demo!
Lifetimes - structs
- Lifetimes can be constrained to “outlive” others.
- Same syntax as type constraint:
<'b: 'a>.
1. struct Pizza(Vec<i32>);
2. struct PizzaSlice<'a> { pizza: &'a Pizza, index: u32 }
3. struct PizzaConsumer<'a, 'b: 'a> { // says "b outlives a"
4. slice: PizzaSlice<'a>, // <- currently eating this one
5. pizza: &'b Pizza, // <- so we can get more pizza
6. }
8. fn get_another_slice(c: &mut PizzaConsumer, index: u32) {
9. c.slice = PizzaSlice { pizza: c.pizza, index: index };
10. }
12. let p = Pizza(vec![1, 2, 3, 4]);
13. {
14. let s = PizzaSlice { pizza: &p, index: 1 };
15. let mut c = PizzaConsumer { slice: s, pizza: &p };
16. get_another_slice(&mut c, 2);
17. }
Lifetimes - 'static
- There is one reserved, special lifetime, named
'static. 'staticmeans that a reference may be kept (and will be valid) for the lifetime of the entire program.- i.e. the data referred to will never go out of scope.
- All
&strliterals have the'staticlifetime.
1. let s1: &str = "Hello";
2. let s2: &'static str = "World";
Structured Data With Lifetimes
- Any struct or enum that contains a reference must have an explicit lifetime.
- Normal lifetime rules otherwise apply.
1. struct Foo<'a, 'b> {
2. v: &'a Vec<i32>,
3. s: &'b str,
4. }
Lifetimes in impl Blocks
- Implementing methods on
Foostruct requires lifetime annotations too! - You can read this block as “the implementation using the lifetimes
'aand'bfor the structFoousing the lifetimes'aand'b.”
1. impl<'a, 'b> Foo<'a, 'b> {
2. fn new(v: &'a Vec<i32>, s: &'b str) -> Foo<'a, 'b> {
3. Foo {
4. v: v,
5. s: s,
6. }
7. }
8. }
