หนึ่งในจุดเด่นของภาษา Rust คือ การบริหารจัดการ memory ซึ่งโดยทั่วไปแล้ว ภาษาอื่นๆ ก็มีการบริหารจัดการ memory ให้อยู่แล้ว บางภาษาอาจจะใช้สิ่งที่เรียกว่า garbage collection ที่ทำหน้าที่มองหาตัวแปรที่ไม่ได้มีการใช้งานเป็นเวลานานแล้ว และจัดการคืน memory กลับไป แต่ในภาษา Rust จะมีกฏหรือของการใช้งาน memory เพิ่มเข้ามา และเราสามารถรู้ได้ในทันทีว่ามีตัวแปรไหนบ้างที่ไม่ได้ใช้งาน

บทความนี้เรามาลงรายละเอียดเกี่ยวกับเรื่องนี้กัน แต่ก่อนจะไปทำความเข้าใจเกี่ยวกับ ownership นั้น เรามีเรื่องที่ต้องเข้าใจกันก่อน นั่นคือ Stack และ Heam Memory Allocation ซึ่ง ไปอ่านทำความเข้าใจกันก่อนเลย

Rust language: ทำความเข้าใจเกี่ยวกับ Stack และ Heap
การจัดการหน่วยความจำในภาษาต่างๆ มันจะจัดสรร memory ได้ทั้งแบบ stack และ heap ทั้ง 2 อย่างนี้แตกต่างกันอย่างไร บทความนี้จะพาทำความเข้าใจแบบคร่าวๆ ก่อนอื่นต้องบอกเลยว่า... โปรแกรมหลายภาษาที่เราเขียนๆ กันอยู่กัน เราแทบไม่จำเป็นต้องสนใจเรื่องของ stack และ heap เลย เพราะตัวภาษาเหล่

Variable Scope

เริ่มต้นกันด้วย scope ของตัวแปรกันก่อนเลย ตัวแปรในภาษา rust นั้นจะสามารถใช้งานได้เมื่อมันถูกประกาศขึ้นมา และใช้งานได้เฉพาะภายใน scope { ... } นั้น เช่น

    fn main() {// s จะยังไม่สามารถใช้งานได้ เนื่องจากยังไม่ถูกประกาศ
        let number:i32 = 10;   // s ถูกประกาศและสามารถใช้งานได้แล้ว

        // number สามารถใช้งานได้ภายใน scope นี้
    }
    
    // เมื่อออกจาก scope แล้ว s จะไม่สามารถใช้ได้อีก

variable scope

เราอาจจะจำมันแบบง่ายๆ คือ มันจะใช้งานได้ ภายใต้ scope ที่มันถูกประกาศไว้นั่นเอง

ในกรณีที่เป็นตัวแปร data types ทั่วไปนั้น ค่าจะถูกเก็บลง stack ทำให้ง่ายต่อการค้นหาและจัดการกับมัน rust เลยอนุญาตให้เราสามารถ copy ค่าไปยังตัวแปรใหม่ตัวไหนก็ได้ โดยที่ตัวแปรนั้นจะยังสามารถใช้งานได้อยู่

fn main() {
    let x = 1; // copy only type on the stack such as integer, boolean etc.
    let y = x;
    println!("x: {:?}", x);
    println!("y: {:?}", y);
  }

copy value

หากเราส่งค่าไปยัง function อื่น ตัวแปร x และ y ก็ยังสามารถใช้งานได้เหมือนเดิม

fn main() {
    let x = 1;
    let y = x;
    make_copy(x);
    println!("x: {}", x);
    println!("y: {}", y);
}

fn make_copy(one: i32) {
    println!("make_copy: {}", one);
}

copy to function


Ownership Rules

ในทางกลับกันหากตัวแปรนั้นถูกเก็บไว้ใน heap (เช่น String) มันจะยุ่งยากกว่าในการหาว่าข้อมูลไหนควรที่จะ clean up ออกไปเมื่อไม่มีการใช้งานแล้ว

ดังนั้น rust เลยสร้างกฏของการเป็นเจ้าของ (ownership) ไว้ดังนี้

  • ค่าแต่ละตัวภายใน Rust จะต้องมีเจ้าของ
  • ในการทำงานแต่ละครั้งค่าเหล่านั้นจะต้องมีเจ้าของแค่ตัวเดียว
  • เมื่อออกจาก scope แล้ว ค่าหรือตัวแปรที่อยู่ใน scope จะถูกลบทิ้ง

กฏ 3 ข้อนี้ จะถูกนำเอามาใช้ในการเขียนโค๊ดของเรา ทีนี้เรามาดูตัวอย่างของ กฏ 3 ทั้งข้อนี้ กัน

Move

เริ่มต้นที่ทำความเข้าใจเรื่องของ ค่าแต่ละค่าจะต้องมีเจ้าของ และ ต้องมีเข้าของเพียงตัวเดียวเท่านั้น

fn main() {
    let x = String::from("Nutshell");
    let y = x;
    println!("x: {:?}", x); // Error: value borrowed here after move
    println!("y: {:?}", y);
}

value borrowed here after move

การ assign ค่าไปยังอีกตัวแปรหนึ่งที่ เก็บบน heap นั่น เราจะเรียกมันว่า การ Move ซึ่งต่างจากตัวแปรที่เก็บลงบน stack ที่ใช้ใช้การ copy ข้อมูลแทน (แทนการเปลี่ยนเจ้าของ)

ดังนั้น เมื่อเราทำการ move ค่าจากตัวแปร x ไปยังตัวแปร y แล้วนั้น เจ้าของของค่านั้นเปลี่ยนไปเป็น y ในทันที ทำให้เราไม่สามารถเรียกใช้งาน x ได้ ดังภาพ

Move ownership

ดังนั้นถ้าหากเราสั่ง cargo build จะแสดงข้อความ error ขึ้นมา และแนะนำให้งาน clone แทนการ move ค่า

error[E0382]: borrow of moved value: `x`
  --> src\main.rs:13:22
   |
11 |     let x = String::from("Nutshell"); // vec!["Nutshell".to_string()];
   |         - move occurs because `x` has type `String`, which does not implement the `Copy` trait
12 |     let y = x;
   |             - value moved here
13 |     println!("{:?}", x); // Error: value borrowed here after move
   |                      ^ value borrowed here after move
   |
   = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
help: consider cloning the value if the performance cost is acceptable
   |
12 |     let y = x.clone();
   |              ++++++++

error message

Move to Function

ในกรณีเดียวกัน หากเราส่งค่าไปยัง function อื่นแล้ว เราจะไม่สามารถใช้งานตัวแปรนั้นได้อีก ในตัวอย่าง ถ้าหากเราเรียกใช้งาน msg หลังจากที่ส่งค่าไปยัง take_ownership แล้ว มันจะแสดง error ขึ้นมาเหมือนกัน

fn main() {
    let msg = String::from("Hello"); // create a variable with a string "Hello"
    takes_ownership(msg); // Give ownership to the function
    println!("msg: {:?}", msg); // Error: value borrowed here after move
}

fn takes_ownership(some_string: String) {
    println!("takes_ownership: {:?}", some_string);
}

move ownership to function

ดังนั้น หากยังต้องการใช้งาน msg ต่อให้ใช้วิธีการ clone หรือ Reference and borrow แทน

เช่นเดียวกันกับการส่งค่ากลับมาจาก function อื่น ก็เป็นการเปลี่ยนเจ้าของ (ownership) จากตัวแปรหนึ่งไปอีกตัวแปรหนึ่ง

fn main() {
    let msg: String = give_ownership();
    println!("msg: {:?}", msg);
}

fn give_ownership() -> String {
    let some_string = String::from("Given");
    some_string
}

give ownership to msg

Clone

หากเราต้องการที่จะใช้งานตัวแปร x อยู่ ให้ใช้วิธีการ clone ข้อมูลนั้นให้แก่ตัวแปร y แทน

fn main() {
    let x = String::from("Nutshell");
    let z = x.clone();
    println!("x: {:?}", x);
    println!("y: {:?}", z);
}

clone value from x to y

วิธีนี้เป็นการคัดลองข้อมูลอีกชุดนึงขึ้นมาแล้วให้ y เก็บค่า pointer ที่ชี้ไปที่ตัวใหม่ ดังภาพ

clone value

Reference and borrow

ถ้าหากเราต้องการส่งข้อมูลไปยังตัวแปรอื่น เพื่อทำงานบางอย่าง แต่ยังอยากใช้งานตัวแปรนั้นได้เหมือนเดิม ให้ใช้วิธีการอ้างอิง Reference ไปยังตัวแปรนั้นแทน ถ้าใครเคยเขียนภาษา C มันคือ pass by reference นั่นเอง

Reference and borrow

ซึ่งการใช้งานจะใช้ & ในการอ้างอิงค่า pointer ถ้าเป็นตัวแปรที่ส่งให้ใช้ & หน้าตัวแปร (&s1) และตัวแปรที่ใช้รับค่าจะใช้ & หน้าประเภทแทน (s: &String)

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{:?}' is {:?}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

Reference value

หาต้องการแก้ไขค่าในตัวแปรนั้นให้เพิ่ม mut เข้าไปด้วย ทั้งในตัวแปรที่ส่ง ใส่ไว้หน้า & แบบนี้ &mut s และตัวแปรที่รับ ใส่ไว้หน้า type some_string: &mut String และอย่าลืมใส่ mut ตอนประกาศตัวแปรด้วยนะ

fn main() {
    let mut s = String::from("Hello");
    change_string(&mut s); // &s is a reference to s
    println!("s: {}", s);
}

fn change_string(some_string: &mut String) { // add mut if you want to change the string
    some_string.push_str(", world"); // `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
}

if change value in variable, add mut after &

Slices

ส่วนอีกวิธีในการแก้ไขค่าในตัวแปร มีอีกวิธีหนึ่งที่นิยมใช้งานกัน คือ การ slices ข้อมูล อธิบายง่ายๆ วิธีนี้แทนที่เราจะ refer ไปยังข้อมูลทั้งหมด แต่เราจะเลือก refer ไปที่ข้อมูลบางส่วนแทน ซึ่งจะทำให้การเข้าถึงและการแก้ไขข้อมูลนั้นเร็วขึ้นไปอีก

Slices

โดยเราจะใช้ &s เพื่ออ้างอิงตำแหน่ง address ของข้อมูลที่เก็บค่าไว้ พร้อมทั่งระบุ range ที่ต้องการ slices ด้วย [0..5]

fn main() {
    let s = String::from("hello world");

    // Slices
    let hello = &s[0..5];
    let world = &s[6..11];
    println!("{} {}", hello, world);
}

String Slices

เราสามารถระบุ range ได้หลายแบบ ดังนี้

  • ระบุเริ่มต้นที่ 0 และ n: [0..5] หรือ [..2]
  • จุดเริ่มต้นที่ n ถึงจุดที่สิ้นสุด (m) แบบ fixed: [2..5]
  • จุดเริ่มต้นที่ n ถึงจุดที่สิ้นสุด (m) แบบไม่ dynamic : let len = s.len(); -> [3..len] หรือ [3..]
  • ระบุทั้งหมด: [0..len] หรือ [..]