More on struct and trait

There are a couple of things that we already used without explaining in the previous chapters, so let's tie up some loose ends.

More on struct

You might remember how we created a new Box or a new String in some of the earlier examples.

fn main() {
    let s = String::from("a");
    let b = Box::new(1);

    println!("This is a String: {}", s);
    println!("This is a Box: {}", b);
}

from() and new() are called associated functions and they are associated with struct String and struct Box, accordingly. The syntax for defining and calling an associated function is as follows.

struct Ex {
    field1: i32,
    field2: bool,
}

impl Ex {
    fn associated_fn(x: i32, b: bool) -> Ex {
        println!("Creating a new Ex with {} and {}", x, b);
        Ex { field1: x, field2: b }
    }
}

fn main() {
    let ex = Ex::associated_fn(1, true);
}

There is another type of functions that you can define for a struct and they are called methods. The difference between an associated function and a method is that a method takes self as the first parameter by default, which refers to a struct instance. This is similar to a Python class. The following example extends the above example and includes methods.

struct Ex {
    field1: i32,
    field2: bool,
}

impl Ex {
    fn associated_fn(x: i32, b: bool) -> Ex {
        println!("Creating a new Ex with {} and {}", x, b);
        Ex { field1: x, field2: b }
    }

    fn print_field1(self) {
        println!("field1 is {}", self.field1);
    }
}

fn main() {
    let ex = Ex::associated_fn(1, true);
    ex.print_field1(); // Invoking a method with a `.`
                        // `self` is automatically passed.
}

Now, the parameter self needs to adhere to the same borrow checker rules. Thus, when you call ex.print_field1(), ex moves into print_field1() since self is passed into it. What that means is that the next example does not work.

struct Ex {
    field1: i32,
    field2: bool,
}

impl Ex {
    fn associated_fn(x: i32, b: bool) -> Ex {
        println!("Creating a new Ex with {} and {}", x, b);
        Ex { field1: x, field2: b }
    }

    fn print_field1(self) {
        println!("field1 is {}", self.field1);
    }

    fn print_field2(self) {
        println!("field2 is {}", self.field2);
    }
}

fn main() {
    let ex = Ex::associated_fn(1, true);
    ex.print_field1();
    ex.print_field2();
}

As the compiler says, in the first call (ex.print_field1()), ex moves into print_field1(). Thus, the second call (ex.print_field2()) cannot use ex anymore. However, you can borrow self, just like any other variables/parameters.

struct Ex {
    field1: i32,
    field2: bool,
}

impl Ex {
    fn associated_fn(x: i32, b: bool) -> Ex {
        println!("Creating a new Ex with {} and {}", x, b);
        Ex { field1: x, field2: b }
    }

    fn print_field1(&self) { // Immutable borrow
        println!("field1 is {}", self.field1);
    }

    fn print_field2(&mut self) { // Mutable borrow
        println!("field2 is {}", self.field2);
    }
}

fn main() {
    let mut ex = Ex::associated_fn(1, true);
    ex.print_field1();
    ex.print_field2();
}

Oftentimes, you use associated functions for initialization. You use methods for instance-specific operations.

trait

Earlier, we mentioned that if a type implements a Copy trait, Rust does not move ownership but copies the value directly. We also mentioned that unsafe can be used to define an unsafe trait. A trait is similar to an interface or a template in other languages, and used to define a shared behavior across different types. It only defines functions and a type needs to implement those functions. For example, the following code defines a trait called TraitEx with a single function to implement shared_behavior().

#![allow(unused)]
fn main() {
trait TraitEx {
    fn shared_behavior(&self) -> String;
}
}

You can implement a trait for your struct as follows.

#![allow(unused)]
fn main() {
trait TraitEx {
    fn shared_behavior(&self) -> String;
}

struct StructEx;

impl TraitEx for StructEx {
    fn shared_behavior(&self) -> String {
        String::from("string")
    }
}
}

trait is heavily used in Rust and you will frequently encounter things like the following that might look confusing (below are taken from the Rust book).

#![allow(unused)]
fn main() {
fn notify<T: Summary>(item: &T) {}

fn notify(item: &impl Summary) {}
}

The above two are actually the same definition. What they mean is that the type of the parameter item can be a borrow of any type (hence the use of the generic parameter type T) that implements the Summary trait. This is called a trait bound, meaning that we are binding a parameter type to a trait. In fact, we can have multiple trait bounds for a parameter.

#![allow(unused)]
fn main() {
fn notify<T: Summary + Display>(item: &T) {}
}

The above defines the parameter item to have a type that implements two traits, Summary and Display. If we want to use many trait bounds, we can use where as follows.

#![allow(unused)]
fn main() {
fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{}
}