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 {} }