The Ultimate Guide to self, Self in Rust

The Ultimate Guide to self, Self in Rust

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