The starting block for most Rust application are impl and traits. While writing out impl and traits, you'll see Self
, self
and their counterpart with the borrow sign &self
, &Self
. While Self
is mostly used for denoting a Rust type, &self
can either be used for referencing the current module or marking the receiver of a method.
Before we look deeply into Self
and self
, we'll first explore structs and borrowing.
Borrowing in Rust
In Rust, a resource can only have one owner to avoid the double free error. Therefore, to use a value from another owner, you have to borrow it. For example, the code below will throw a borrow of moved value: me
error.
fn main() {
let me = "Ugochi".to_string();
let you = me;
println!("{}, {}", me, you)
}
This is because me
has ownership for Ugochi while you
tries to take ownership of Ugochi. To fix the error, we'll borrow Ugochi from me
without taking ownership.
fn main() {
let me = "Ugochi".to_string();
let you = &me;
println!("{}, {}", me, you)
}
If we follow the same pattern of our example for integers, we won't get any error.
fn main() {
let me = 5;
let you = me;
println!("{}, {}", me, you)
}
This is because integers have the Copy Trait
. What this means is that there's no double freeing error because the allocator doesn't allocate different space portion for variables me
and you
since you
is a copy of me
. For data types without the Copy Trait
, me
is moved, thus me
and you
are allocated different spaces in the memory by the allocator therefore causing this error: borrow of moved value: me
.
If you'll be borrowing a variable that does not have the Copy Trait
, it is important that you add the borrow sign to the variable type. The example below, adds the borrow sign to type String
because we'll be borrowing the variable a
.
fn fullname(a: &String) -> String {
a.to_string()
}
fn main() {
let a = "Ugochi".to_string();
let d = "Hanny".to_string();
let b = fullname(&a);
let c = fullname(&d);
println!("{}, {}, {}", a, b, c)
}
It is important to add the borrow sign to String
because without it, you'll get a mismatched error type: expected struct String, found &String
. Since Rust is statically typed, it expects that the data type you assign to a variable is what is returned.
Also, if you'll be borrowing the return value of a function, be sure to specify it in the function.
Rust Structs
Rust struct or structure is a custom Rust data type that can be used for grouping multiple related values together. Rust struct can take values of different types. For instance, a struct can have a String
, i32
and bool
type.
Structs are pretty popular in Rust because they can take in different data types. To define a Struct, you'll use the struct
keyword.
// Defining Structs
struct Person {
name: String,
age: i32,
christian: bool
}
Then, you can instantiate the struct you created earlier in your function.
fn main() {
// Instantiating Structs
let ugochi = Person {name: "Ukpai Ugochi".to_string(), age: 2, christian: true};
// Printing the name of a person from Struct
println!("The name of the person is: {}", ugochi.name)
}
The code above, will print out Ukpai Ugochi
in the console. If you want to print out all the values in your struct, use #[derive(Debug)]
.
#[derive(Debug)]
struct Person {
name: String,
age: i32,
christian: bool
}
fn main() {
// Instantiating Structs
let ugochi = Person {name: "Ukpai Ugochi".to_string(), age: 2, christain: true};
// Printing the name of a person from Struct
println!("The name of the person is: {:?}", ugochi)
}
Next, let's explore impl
and also we'll be looking at how Self
and self
is used in impl
.
Impl
Impl
or implementation allows developers to create implementations for a struct and add methods that are specific to the struct. Let's create a method to print out the full name of a person with impl
.
// Declaring Struct
struct Person {
first_name: String,
last_name: String,
}
// Implement Struct Person
impl Person {
// You can replace Self with Person. The new keyword can be used with double colon, it is an associate method.
fn new(first_name: String, last_name: String) -> Self {
Person {
first_name, last_name
}
}
// This is a Rust method that can be used with a full stop and paranthesis e.g .full_name()
fn full_name(self) -> String {
// The format macro is used for specifying format for printout.
format!("{}, {}", self.first_name, self.last_name)
}
}
fn main() {
// Declaring the new method and full_name method to instantiate Struct
let person_name = Person::new("Ugochi".to_string(), "Ukpai".to_string()).full_name();
println!("What is your full name: {}", person_name);
}
From the example above, we have created two functions (new and full_name). One of the functions we created - new
is an associated function. To declare the new
function, you'll be using a double semicolon (::) instead of a fullstop.
What if we would like to borrow a variable from our struct instance in the implementation like in the borrowing section. Then we'll have to use the borrow sign in our struct instance, just like the example below.
// Declaring Struct
struct Person {
first_name: String,
last_name: String,
}
// Implement Struct Person
impl Person {
// You can replace Self with Person. The new keyword can be used with double colon, it is an associate method.
fn new(first_name: String, last_name: String) -> Self {
Person {
first_name, last_name
}
}
// This is a Rust method that can be used with a full stop and paranthesis e.g .full_name()
fn full_name(&self) -> String {
// The format macro is used for specifying format for printout.
format!("{}, {}", self.first_name, self.last_name)
}
}
fn main() {
// Declaring the new method and full_name method to instantiate Struct
let person_name = Person::new("Ugochi".to_string(), "Ukpai".to_string());
let borrow = &person_name;
let full_name = borrow.full_name();
println!("What is your full name: {}", full_name);
}
Conclusion
In this article, we have explored self
and Self
in Rust by applying them in structs and impl. A brief note to keep in mind when dealing with self
and Self
in Rust is that they correspond to the block below when you use them in your code.
Self => Struct Type, mostly used as return type.
self => Instance of the Struct
From the example below, we can see that Self
is the return type for the function me
. Also, self
is an instance of the struct Person
. So, if Person
have a value first_name, it can be called with self.first_name just like this.first_name in JavaScript.
impl Person {
fn me(self) -> Self {
....
}
}