Tour of Rust - Ch.5 - Ownership & Borrowing Data

Published on

เขียนวันที่ : Jun 27, 2022

tour-of-rust/chapter5
Discord

บันทึกการเรียน Tour Of Rust บทเรียนที่ 5 - Tour of Rust - Chapter 5 - Ownership & Borrowing Data

Borrowing กับ Ownership เป็นเรื่องเกี่ยวกับการจัดการหน่วยความจำของข้อมูล เอาจริงๆ เป็นเรื่องที่ผมก็ยังไม่แม่น และต้องอ่าน Dcos และค้นข้อมูลอ่านเพิ่มเติมอยู่ตลอดเวลา แต่ถ้าเอาแบบไม่ลงลึกมาก ถ้าเข้าใจ Pass by value กับ pass by reference ก็น่าจะพอเข้าใจได้

Chapter 5 - Ownership & Borrowing Data 🦀

วันนี้จดบันทึกเรื่อง Ownership และ Borrowing ถึงแม้ส่วนใหญ่ จะก็อปโค๊ดจาก Tour of Rust มาเป็นตัวอย่าง ก็เถอะ 🤣

Ownership

เรื่องของ Ownership หรือความเป็นเจ้าของ (เรียกแบบภาษาไทย) ส่วนตัวผมขอทับศัพท์เป็น Ownership ละกัน

เวลาเราสร้าง instance ขึ้นมาซัก type นึง และกำหนดค่า variable ให้มัน มันจะทำการสร้างพื้นที่หน่วยความจำ ที่ตัว Rust Compiler มันจะเช็คตลอดการทำงาน (Lifetime) ตัวแปรที่ถูก assign (binding) เราเรียกว่า owner

struct Foo {
    x: i32,
}

fn main() {
    // สร้าง instance structs และผูกกับตัวแปร `foo`
    // เพื่อสร้างพื้นที่หน่วยความจำ
    // `foo` คือ owner
    let foo = Foo { x: 42 };
    // foo is the owner
}

Scope-Based Resource Management

ในภาษา Rust ใช้ end of scope (หรือจุดสิ้นสุดขอบเขต) เป็นที่ไว้สำหรับทำลายโครงสร้าง(deconstruct) และ คืนหน่วยความจำ(deallocate) เราเรียกขั้นตอนนี้ว่า drop

รายละเอียดหน่วยความจำ:

  • Rust ไม่มี garbage collection
  • เราเรียกสิ่งนี้ว่า Resource Acquisition Is Initialization ( RAII ) ในภาษา C++
struct Foo {
    x: i32,
}

fn main() {
    let foo_a = Foo { x: 42 };
    let foo_b = Foo { x: 13 };

    println!("{}", foo_a.x);

    println!("{}", foo_b.x);
    // foo_b drop
    // foo_a drop
}

Dropping is Hierarchical

เวลาตัว struct ถูก drop มันจะ drop จากตัวมันเองก่อน แล้วก็ค่อยๆไปที่ตัวลูกของมัน และต่อไปเป็นทอดๆเรื่อยๆ

รายละเอียดหน่วยความจำ:

  • ตัว Rust จะคืนหน่วยความจำให้อัตโนมัติ ทำให้เรามั่นใจว่าจะเกิด memory leak น้อยลง
  • การจัดการ memory ทำการ drop ได้เพียงแค่ครั้งเดียว
struct Bar {
    x: i32,
}

struct Foo {
    bar: Bar,
}

fn main() {
    let foo = Foo { bar: Bar { x: 42 } };
    println!("{}", foo.bar.x);
    // `foo` drop ก่อน จากนั้นต่อด้วย `foo.bar`
}

Moving Ownership

เมื่อ Ownership ถูกส่งไปเป็น argument ของ function ตัว Ownership จะถูกย้ายไปเป็น paremeter ของ function แทน เมื่อย้ายไปแล้ว ตัวแปรเดิมจะไม่สามารถใช้งานได้อีกต่อไป

struct Foo {
    x: i32,
}

fn do_something(f: Foo) {
    println!("{}", f.x);
    // 2. f drop ตรงนี้
}

fn main() {
    let foo = Foo { x: 42 };
    // 1. `foo` ถูกส่งไปใน do_something
    do_something(foo);
    // 3. ถูก drop ไปหลังจาก do_something แล้ว
}

Returning Ownership

Ownership สามารถได้คืนจาก function

Borrowing Ownership with References

การอ้างอิง (References) ช่วยให้เราเข้าถึง resource โดยใช้ตัวแปร & ตัว reference ก็สามารถ drop ได้เหมือนกันกับ resources อื่นๆ

struct Foo {
    x: i32,
}

fn main() {
    let foo = Foo { x: 42 };
    let f = &foo;
    println!("{}", f.x);
    // f is dropped here
    // foo is dropped here
}

Borrowing Mutable Ownership with References

นอกจากนี้เรายังสามารถใช้เครื่องหมาย &mut เพื่อทำการยืม (borrow) การเข้าถึง resource ที่เปลี่ยนค่าได้

โดย Ownership ไ่ม่สามารถย้ายหรือแก้ไขอะไรได้ในขณะที่ถูก borrow อยู่

Dereferencing

เราใช้ &mut สำหรับการอ้างถึง (Reference) และใช้ * เพื่อกำหนด owner ใหม่ให้มัน

ในทางกลับกัน เราก็สามารถที่จะ copy ค่านั้นออกมาด้วยการใช้ *

fn main() {
    let mut foo = 42;
    let f = &mut foo;
    let bar = *f; // copy ค่า value ของ owner
    *f = 13;      // set the reference's owner's value
    println!("{}", bar);
    println!("{}", foo);
}

Passing Around Borrowed Data

สรุป กฎที่ใช้ในการอ้างอิง (Reference) ได้ดังนี้:

  • Rust ยอมให้มีการอ้างอิงถึง mutable reference ได้ค่าเดียว หรือ หลายตัวที่เป็นแบบเปลี่ยนแปลงค่าไม่ได้ แต่ไม่สามารถทำทั้งสองอย่างพร้อมกันได้
  • Reference จะต้องไม่มี lifetime นานกว่า owner
struct Foo {
    x: i32,
}

fn do_something(f: &mut Foo) {
    f.x += 1;
    // mutable reference f drop ตรงนี้
}

fn main() {
    let mut foo = Foo { x: 42 };
    do_something(&mut foo);
    // เพราะว่า mutable references ทั้งหมดถูก drop ใน function
    // do_something แล้ว ทำให้เราสามารถสร้างตัวใหม่ได้
    do_something(&mut foo);
    // foo is dropped here
}

References Of References

Reference ก็สามารถใช้อ้างอิงถึง References อื่นได้เช่นกัน

Explicit Lifetimes

ถึงแม้ว่าเราจะไม่รู้ หรือไม่เห็น Lifetime ของตัวแปร แต่ว่า Compiler ของ Rust รู้ว่า lifetime เป็นยังไง และมีการตรวจสอบตลอด ทำให้มั่นใจได้ว่า จะไม่มี reference ใดที่มี lifetime นานกว่า owner ได้

การระบุ lifetime ให้เริ่มต้นด้วย เครื่อง ' เช่น 'a, 'b เป็นต้น

struct Foo {
    x: i32,
}

// the parameter foo and return value share the same lifetime
fn do_something<'a>(foo: &'a Foo) -> &'a i32 {
    return &foo.x;
}

fn main() {
    let mut foo = Foo { x: 42 };
    let x = &mut foo.x;
    *x = 13;
    // x is dropped here, allowing us to create a non-mutable reference
    let y = do_something(&foo);
    println!("{}", y);
    // y is dropped here
    // foo is dropped here
}

Multiple Lifetimes

การมี Lifetimes หลายตัว ทำได้ก็จริง แต่บางที Compiler ก็ยังไม่สามารถแยกแยะ lifetime ของทุกตัวได้ทั้งหมด

Static Lifetimes

ตัวแปรแบบ static เป็น memory resource ที่ถูกสร้าง ณ​ เวลา compile และจะคงอยู่ตั้งแต่โปรแกรมเริ่มทำงานจบจบโปรแกรม โดยต้องระบุ type ให้มันชัดเจน

static lifetime สามารถเกิดตอน runtime ก็ได้ ไม่เฉพาะแค่ตอน compile

ตัว static lifetime เราจะใช้ 'static เป็นตัวกำหนด และตัว static lifetime จะไม่มีวันโดน drop

 let msg: &'static str = "Hello World!";

Lifetimes in Data Types

เหมือนกับ function ตัว Data Types เราก็สามารถใส่ lifetime ให้กับสมาชิกแต่ละตัวได้ โดย Rust จะทำการตรวจอยู่เสมอว่า Reference มันจะไม่มี lifetime มากกว่า Owner

    i:&'a i32
}

fn main() {
    let x = 42;
    let foo = Foo {
        i: &x
    };
    println!("{}",foo.i);
}

🎉 จบแล้ว ยาวมากและมึนมากๆ 🤣

สรุปบทที่ 5

เอาจริงๆ ส่วนตัวผมพบว่า บทนี้เป็นเรื่องที่เข้าใจยากที่สุดสำหรับผมเลย ขนาดแปลมา บางทีผมก็ยังไม่เข้าใจเท่าไหร่ ทั้ง Eng ทั้งไทยนะ ก็ยังงๆ สุดท้าย ยังไงก็ต้องไป research อ่านเพิ่มเติมอยู่ดี

Buy Me A Coffee
Discord