We can create custom data types in Rust using struct, trait, and enum. A struct is a composite data type that groups variables in a memory block. These variables are then accessible via the struct’s instance – also referred to as an object in OOP languages. Moreover, a struct can have its methods (or functions) with implementations; and a trait can add new methods to it.
We can also use the enum keyword to create custom data types whose values are generally only those predefined in its definition.
Custom Type With Struct
There are three ways to define a struct, but only 2 are generally useful – a struct with named fields and a struct with unnamed fields. When we declare a variable of a struct type and initialize it, we are creating an instance of that struct type.
Consider the codes below – we created instances of a struct type in lines 9 and 16.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | struct Person { last_name: String, first_name: String, company: String } fn main() { let person_JohnLee = Person { last_name: String::from("Lee"), first_name: String::from("John"), company: String::from("Turreta.com") }; let person_BartSimpson = Person { last_name: String::from("Simpson"), first_name: String::from("Bart"), company: String::from("Turreta.com") }; println!("Person 1: {} {} from {}", person_JohnLee.first_name, person_JohnLee.last_name, person_JohnLee.company); println!("Person 2: {} {} from {}", person_BartSimpson.first_name, person_BartSimpson.last_name, person_BartSimpson.company); } |
Struct With Named Fields
A custom data type based on struct has a name and may have named fields, which are variable declarations with variable names and only their data types.
In the following codes, we have a Person struct data type with two named fields – last_name and first_name, and we access them using the dot notation like in lines 13 and 14.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | struct Person { last_name: String, first_name: String } fn main() { let person: Person = Person { first_name: String::from("Karl"), last_name: String::from("San Gabriel") }; println!("{}", person.first_name); println!("{}", person.last_name); } |
Struct With Unnamed Fields
A struct can have a definition without specifying named fields, and it works like a Tuple. In this case, we only define the data types. To access each field, we need to use its ordinal value with the struct instance. Consider the codes below – the struct type Coordinate has three unnamed fields. The first will have an ordinal value of 0, and the second will have an ordinal value of 1, and so on. Hence, coordinate_instance.0 and coordinate_instance.1 refer to the first and second fields, respectively.
1 2 3 4 5 6 7 8 9 | // x, y, z struct Coordinate(i32, i32, i32); fn main() { let coordinate_instance = Coordinate(100, 220, 66); println!("{}", coordinate_instance.0); println!("{}", coordinate_instance.1); println!("{}", coordinate_instance.2); } |
Custom Type With Trait
A struct may have methods, and a trait can override or add new instance methods to it.
Struct With Methods
There are two types of methods a struct can have – static and instance methods. A static method does not require an instance of the struct to call it, while an instance method needs an instance of a struct to invoke it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | struct Dog { name: String } impl Dog { // Instance method fn get_name(&self) -> String { return self.name.clone(); } // Instance method fn get_type(&self) -> String { return String::from("Dog Struct"); } // Static Method fn get_struct_version() -> String { return String::from("v1.0"); } } fn main() { // Static function println!("{}", Dog::get_struct_version()); let dog: Dog = Dog { name: String::from("Bart") }; // Invoking an instance method println!("{}", dog.get_name()); } |
Struct With Methods And Trait
A trait can have zero or more instance methods without implementations for structs to implement.
1 2 3 4 5 6 | trait MyTrait { fn method1(&self); fn method2(&self); fn method3(&self, param1: i32); fn method4(&self, param1: String) -> String; } |
Similar to structs, instance methods specify &self as their first function parameter.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | struct Dog { name: String } impl Dog { // Instance method fn get_name(&self) -> String { return self.name.clone(); } // Instance method fn get_type(&self) -> String { return String::from("Dog Struct"); } // Static Method fn get_struct_version() -> String { return String::from("v1.0"); } } trait Pet { fn beg_for_food(&self); } impl Pet for Dog { fn beg_for_food(&self) { println!("Beg!"); } } fn main() { // Static function println!("{}", Dog::get_struct_version()); let dog: Dog = Dog { name: String::from("Bart") }; // Invoking an instance method println!("{}", dog.get_name()); // Invoking an instance method dog.beg_for_food(); } |
To implement a trait for a struct, we use the impl keyword and provide implementations for trait methods.
What if we implement a trait with only a static method? The method becomes part of the struct as if we defined it in the struct. Consider the code snippet below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | trait MyTrait { fn a_static_method(param1: String) -> String; } struct MyStruct { } impl MyStruct {} impl MyTrait for MyStruct { fn a_static_method(param1: String) -> String { return "static method from trait".to_string() } } fn main() { // a_static_method becomes part of MyStruct println!("{}", MyStruct::a_static_method("any".to_string())); } |
With traits, we can use a custom data type based on a struct in different ways in our codes. For example, we can pass an instance to any functions that accept a parameter of any of the traits the struct implements.
Custom Type With Enum
We can also create custom data types with enum.