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