Structures and Methods

Rust isn't an object-oriented language (whatever that means). For example, Rust doesn't force you to place everything in classes, unlike Java. However, Rust does provide some object-oriented features, and designed these features better than Java and C++.

Object-oriented paradigm has three pillars:

  • Encapsulation
  • Inheritance
  • Polymorphism

Encapsulation means to package the data and operations on them together. Rust provides encapsulation by structures and methods.

Structures

A structure contains a sequence of data, called fields.


# #![allow(unused_variables)]
#fn main() {
struct Apple {
    color: i32,
    weight: f64,
}

let fuji = Apple{ color: 1, weight: 1.2 };
assert_eq!(fuji.color, 1);
assert_eq!(fuji.weight, 1.2);
#}

If you wish to modify the fields of a struct, you must bind the struct to a mutable variable. Mutability is a property of variable binding, meaning that a program

  • may not declare a field of a struct to be mutable.
  • may bind the same struct to both immutable and mutable variables.

# #![allow(unused_variables)]
#fn main() {
struct Apple {
    color: i32,
    weight: f64,
}

let fuji = Apple{ color: 1, weight: 1.2 };
let mut golden = Apple{ color: 2, weight: 0.8 };
golden.weight = 1.2;
#}

You may create a struct variable from an existing variable of the same type.


# #![allow(unused_variables)]
#fn main() {
struct Apple {
    color: i32,
    weight: f64,
}

let fuji = Apple{ color: 1, weight: 1.2 };
// `golden` takes the value for the `weight` field from `fuji`.
let golden = Apple{ color: 2, .. fuji };
assert_eq!(golden.weight, 1.2);
#}

Tuple structs

In tuple structs, fields have types but not names. A program accesses the fields of a struct by their indices using the . operator, e.g., .0.

struct Apple (i32, f64);

let fuji = Apple(1, 1.2);
assert_eq!(fuji.0, 1);
assert_eq!(fuji.1, 1.2);

Unit-like structs

A unit-like struct is a struct with no field.

struct Nothing {}
let x = Nothing{};

They are similar to the unit type (), an empty tuple, but there is a difference. Tuples are unnamed, so there is only one unit type. However, since structs have names, you may create many different unit-like structs. Unit-like structs are useful when you wish to use a data type that contains no value but that doesn't implement the Copy trait.

struct Nothing {}
let x = Nothing{};
// `x` is moved into `y`.
let y = x;
// Cannot use `x` any more.

Methods

Unlike C++, Java, or Python, Rust separates data and operations on them. A program defines data in structures, and operations on them in the implementation block for the structure. These operations are called methods.

Like Python, Rust passes the value on which the method is invoked to the method as the first parameter. But unlike Python, self is a keyword in Rust. If the first parameter is &self, it gets a shared reference to the struct; if it is &mut self, it gets a mutable reference; if it is self, the caller's value is moved into it.


# #![allow(unused_variables)]
#fn main() {
struct Apple {
    color: i32,
    weight: f64,
}

impl Apple {
    fn get_color(&self) -> i32 {
        self.color
    }
    
    fn set_color(&mut self, color: i32) {
        self.color = color
    }
    
    fn consume(self) {
    }

    // Similar to a *constructor* in C++/Java
    fn new(color: i32, weight: f64) -> Apple {
        Apple{ color: color, weight: weight }
    }
    
}

// Constructors
let mut fuji = Apple::new(1, 1.2);
let mut golden = Apple::new(2, 0.9);
// &self
assert_eq!(fuji.get_color(), 1);
assert_eq!(Apple::get_color(&golden), 2);
// &mut self
fuji.set_color(2);
assert_eq!(fuji.get_color(), 2);
Apple::set_color(&mut golden, 3);
assert_eq!(golden.get_color(), 3);
// self
fuji.consume();
Apple::consume(golden);
// Error: `fuji` is moved.
// fuji.get_color();
// Error: `golden` is moved.
// golden.get_color();
#}

You may call a method in two ways:

  • Use the . operator on the struct value, e.g., fuji.get_color(). In this case, the compiler automatically takes a reference to fuji and passes it to the &self parameter of get_color().

  • Call the method as a function and pass the struct value as the first argument, e.g., Apple::get_color(&fuji). Note that you must qualify the method name get_color with the struct name Apple using the :: operator. Also you must take a reference of fuji explicitly and pass it to the function.

Associated functions

The function new() in Apple takes no self, &self, or &mut self parameter. Therefore, a program need not (and cannot) call new() on a value. Instead, the program calls Apple::new() directly to create a new value. Such functions are called associated functions. They are similar to constructors in C++ or Java, but

  • they aren't automatically invoked when binding new variables (unlike C++). The program must call them explicitly.
// Doesn't call `Apple::new()`
let fuji: Apple;
// Doesn't call `Apple::new()`
let fuji = Apple;
// Call `Apple::new()` explicitly
let fuji = Apple::new();
  • they may have any names. new isn't a keyword in Rust, although it is conveniently and conventionally used.

Rust provides a keyword Self to represent the struct that the implementation block is for. Therefore, the following two declarations are equivalent:

impl Apple {
    // These two declarations are equivalent.
    fn new(color: i32, weight: f64) -> Apple { ... }
    fn new(color: i32, weight: f64) -> Self { ... }
}

In fact, &self is a syntactic sugar for self: &Self, and &mut self for self: &mut Self.

Lifetime in structures

If a struct contains a reference, it must specify the lifetime of the reference explicitly.


# #![allow(unused_variables)]
#fn main() {
let name = "Fuji";

struct Apple<'a> {
    color: i32,
    weight: f64,
    name: &'a str,
}

let fuji = Apple{ color: 1, weight: 1.2, name: name };
assert_eq!(fuji.name, name);
#}

In the program, 'a is not the lifetime of the structure value fuji itself, but is the lifetime of the borrowed content. We call 'a the inner lifetime, and 'b (the lifetime of the structure value fuji) the outer lifetime. Their relationship is as follows.

Since the structure value contains the borrowed content, the outer lifetime must not outlive the inner lifetime, so 'a: 'b. See lifetime subtyping.

If a program has an (outer) reference to a structure value that contains a (inner) reference, it can retrieve the inner reference, but the lifetime of the retrieved reference depends on whether the inner reference is shared or mutable.

  • If the inner reference is mutable, then the lifetime of the retrieved reference, either shared or mutable, is the same as that of the outer reference.

# #![allow(unused_variables)]
#fn main() {
struct Foo<'a> {
    x: &'a mut i32,
}

impl<'a> Foo<'a> {
    // Error: `'a` outlives the lifetime of `self`
    fn get(&self) -> &'a i32 {
        self.x
    }

    // OK. This is equivalent to `fn get2<'b>(&'b self) -> &'b i32`
    fn get2(&self) -> &i32 {
        self.x
    }
}
#}
  • But if the inner reference is shared, then the lifetime of the retrieved reference can be that of the inner reference, which is larger than that of the outer reference.

# #![allow(unused_variables)]
#fn main() {
struct Foo<'a> {
    x: &'a i32,
}

impl<'a> Foo<'a> {
    // OK
    fn get(&self) -> &'a i32 {
        self.x
    }
}
#}

The reason for the difference is as follows. In the first case, the structure value acquired exclusive access to the inner value via the mutable inner reference. When the structure value goes out of scope, this exclusive access expires, so any reference to the inner value that the program acquired via the outer reference must also expire.

The second case works because the shared access to the inner value is valid throughout the lifetime of the inner reference, so it is no harm to return the inner reference with this lifetime.