Mengenal Struct, self dan Self pada Rust

30 Dec 2021

Rust merupakan bahasa pemrograman sistem yang tidak berbasis objek namun penggunaan struct dan trait konsepnya mirip
pada bahasa pemgrograman oop(berbasis objek). Rust tidak menggunakan klas untuk mengatur struktur kode, melainkan menggunakan struk untuk mewakili object/klas dan trait untuk mewakili behaviornya atau dalam bahasa lain disebut dengan interface.

Kali ini saya ingin mengenalkan sedikit tentang struct untuk mengelola struktur kode dan sedikit tentang trait dan bagaimana penggunaannya. Karena pembahasan struct dan trait bisa sangat panjang maka disini saya hanya menjelaskan secara umum belum sampai mendalam.

Kemudian kita juga akan mengenal apa itu keyword self di Rust. Karena jika kamu sudah menggunakan struct dan ingin mengimplementasi method pada struk Rust tentu akan bertemu dengan keyword self. Dan juga Rust memiliki keyword Self (dengan huruf S besar) yang akan menunjuk ke nama struct yang sedang di impl. self (dengan huruf s kecil) pada rust biasanya digunakan pada saat deklarasi method untuk mengakses anggota struct, ya method kalau dalam pemrograman berbasis objek adalah fungsi yang menempel pada objek. self mirip self pada python atau this pada Javascript. Penggunaan self pada Rust biasanya pada tipe data struct/enum.

Mengenal Struct

Struct atau structure adalah tipe data custom yang membolehkanmu untuk memberinya nama dan mempaketkan nilai yang saling berhubungan dan membuatnya memiliki arti. Jika kamu terbiasa dengan bahasa pemrograman OOP struct itu mirip class

Mendefinisikan dan instantiating struct

Karena arti instantiating bahasa Indonesia menurut saya agak aneh maka saya gunakan bahasa inggris saja. Karena seharusnya instantiating kalau dalam OOP berarti new NamaKlass. Ok kita balik lagi ke struk, pertama kita akan belajar cara mendefinisikan struct. Untuk mendefinisikan struct dapat digunakan keyword struct diikuti nama struk kemudian didalam kurung kurawal kita definisikan nama dan tipe data anggota struk atau biasa dikenal dengan istilah fields.

Untuk lebih memahami struk mari kita lihat contoh berikut ini:

struct User {
    id: i32,
    username: String,
    email: String,
    active: bool,
}

Jadi disini kita membuat struk dengan nama User dan memiliki 4 fields yaitu id, username, email dan active. Masing-masing memiliki tipe data.

Untuk menggunakannya kita perlu membuat instance dari struk dengan memberikan nilai-nilai pada field yang ada pada struk. Untuk contohnya bisa dilihat pada kode berikut ini:

fn main() {
    let user = User{
        id: 1, 
        username: String::from("agus"), 
        email: String::from("agus@gmail.com"),
        active: true,
    };
}

Untuk mengakses field pada struk bisa menggunakan notasi dot (.). Jika kita hanya ingin mendapatkan email saja maka kita bisa gunakan user.email dimanapun kita ingin menginginkan nilai ini. Jika instance struct mutable(dapat diubah) kita bisa merubah nilai dari field tersebut menggunakan notasi dot. Kode berikut menunjukkan bagaimana cara mengganti nilai pada field email dari instansi User.

fn main() {
    let mut user = User{
        id: 1, 
        username: String::from("agus"), 
        email: String::from("agus@gmail.com"),
        active: true,
    };
    user.email = "hello@gmail.com";
}

Perlu dicatat bahwa seluruh instance harus mutable; Rust tidak membolehkan kita untuk menandai hanya field tertentu saja yang dapat diubah.

Mengenal impl dan self

Untuk mendefinisikan method pada struk kita bisa menggunakan keyword impl diikuti dengan nama struk yang ingin di implementasi method. Contoh berikut ini akan mengimpl method print pada struk User.

impl Struct {
    fn print(&self) {
        println!("ID: {}, Username: {}, Email: {}, Active: {}",
            self.id, self.username, self.email, self.active);
    }
}

fn main() {
    let user = User{
        id: 1, 
        username: String::from("agus"), 
        email: String::from("agus@gmail.com"),
        active: true,
    };
    user.print();
}

Jika kode diatas dijalankan maka memberikan output seperti ini:

Disini self mirip this pada javascript yang akan bisa mengakses field pada struct karena impl User. Kalau kode diatas ditulis dengan javascript akan seperti ini

class User {
  id = 0;
  username;
  email:
  active = false;

  print() {
    console.log(`ID: ${this.id}, Username: ${this.username}, 
      Email: ${this.email}, Active: ${this.active}`)
  }
}

Jika kita mendefinisikan method pada struct dengan impl namun pada parameter fungsi tidak ada self/&self/&mut self maka fungsi tersebut akan menjadi static. Kita tidak perlu menginstansi struct sebelum menggunakan fungsi tersebut. Biasanya digunakan untuk membuat fungsi instatnsi struck/ model builder begitu. Kalau dalam javascript mirip fungsi static pada class, untuk lebih jelasnya bisa diliat pada kode berikut:

class User {
  static add(x, y) {
    const result = x + y
    console.log(`${x} + ${y} = ${result}`)
  }
}

User.add(1, 2)

Bisa dilihat pada kode di atas bahwa untuk mengakses fungsi add kita langsung memanggil nama klass dan nama fungsinya tanpa harus instansi terlebih dulu. Nah pada Rust kurang lebih mirip dengan javascript dengan perbedaan bahwa kita tidak bisa mengakses field struck.

impl User {
    fn new(id: i32, username: String, email: String, active: bool) -> User {
        User {
            id, 
            username, 
            email,
            active,
        }
    }
}

Untuk memanggil fungsi static maka kita bisa gunakan tanda :: setelah nama struck. contoh:

let user = User::new(1, "agus".to_owned(), "agus@gmail.com".to_owned(), true);

Pada definisi fungsi new di atas terdapat tipe kembalian berupa struct User, agar lebih idiom ke Rust maka kita bisa menggantinya dengan Self, Self akan mengembalikan struct sesuai dengan yang di impl. seperti pada contoh code di atas jika Self didalam impl User maka akan mengembalikan struct User. Keuntungan menggunakan Self dari pada nama struct adalah jika struct berubah kita tidak perlu merubah tipe kembaliannya.

Contoh kode yang sudah diganti dengan Self:

impl User {
    fn new(id: i32, username: String, email: String, active: bool) -> Self {
        Self {
            id, 
            username, 
            email,
            active,
        }
    }
}

Mengenal Trait

Trait adalah kumpulan definisi fungsi yang belum diimplementasi, trait dapat berisi 1 atau lebih definisi fungsi. Namun trait juga dapat berisi implementasi default dari suatu fungsi yang akan dipakai jika struct/enum/type tidak mengimplementasi definisi fungsi pada trait.

Contoh definisi trait pada trait std::io::Read

pub trait Read {
    fn read(&mut self, buf: &mut [u8]) -> Result<usize>;
}

Pada kode diatas saya mengambil contoh satu fungsi read pada trait Read dimana parameternya berupa &mut [u8] yang artinya kita harus menyediakan variabel mutable dengan tipe slice u8. Semua tipe yang mengimplementasi trait Read kita dapat mengakses fungsi read tersebut. Kita ambil contoh dari struct File pada module std::fs::File.

use std::io::prelude::*;
use std::io;
use std::fs::File;

fn main() -> io::Result<()> {
    let mut f = File::open("foo.txt")?;
    let mut buffer = [0; 10];

    let n = f.read(&mut buffer)?;
    println!("Hasil: {:?}", &buffer[..n]);

    Ok(())
}

Karena struct File mengimplementasi trait Read maka kita bisa menggunakan fungsi read untuk membaca file. Pada kode di atas pertama kita membuka file pada kode let mut f, kemudian kita membuat buffer file let mut buffer dengan nilai 0 sebanyak 10. Lalu pada let n kita baca file f.read(&mut buffer)? dan taruh pada variabel buffer.

Kemudian kita mencetak datanya sesuai dengan hasil pembacaan dari varibel n berapa banyak yang sudah dibaca &buffer[..n].

Contoh lain penggunaan trait

struct News;
struct Blog;

trait Summary {
    fn read_more(&self) -> String;
}

impl Summary for News {
    fn read_more(&self) -> String {
        "Baca berita lagi..".to_owned()
    }
}

impl Summary for Blog {
    fn read_more(&self) -> String {
        "Baca blog lagi..".to_owned()
    }
}

fn print_summary(sum: impl Summary) -> {
    println!("{}", sum.read_more());
}

fn main() {
    let news = News;
    let blog = Blog;
    print_summary(news);
    print_summary(blog);
}

Dapat dilihat pada kode di atas bahwa kita mendefinisikan 2 buah struk News dan Blog, dan juga trait Summary. Lalu mengimplementasi trait Summary untuk struk News dan Blog dengan default implementasi pada fungsi read_more. Kita juga membuat fungsi print_summary dengan parameter sum dengan aturan bahwa sum harus implement dari trait Summary. Selama sum mengimplementasi dari trait Summary maka kode akan berjalan.

Untuk lebih memahami trait kita bisa mengubah kode di atas ke dalam bahasa PHP. kurang lebih kodenya seperti ini:

interface Summary {
    public function read_more(): string;
}

class News implements Summary
{
    public function read_more(): string
    {
        return 'Baca berita lagi..';
    }
}

class Blog implements Summary
{
    public function read_more(): string
    {
        return 'Baca blog lagi..';
    }
}

function print_summary(Summary $sum) {
    echo $sum->read_more(), PHP_EOL;
}

$news = new News();
$blog = new Blog();
print_summary($news);
print_summary($blog);

Karena di PHP tidak ada struk maka disini saya pake class. Semoga bermanfaat.