Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Slice Type

Slice ให้คุณอ้างถึงลำดับ element ที่ติดกันใน collection slice เป็น reference ชนิดหนึ่ง ดังนั้นไม่มี ownership

นี่คือปัญหา programming เล็ก ๆ — เขียนฟังก์ชันที่รับ string ของคำที่คั่น ด้วย space แล้ว return คำแรกที่เจอใน string นั้น ถ้าฟังก์ชันไม่เจอ space ใน string ทั้ง string ต้องเป็นหนึ่งคำ ดังนั้นทั้ง string ควรถูก return

หมายเหตุ: เพื่อจุดประสงค์ของการแนะนำ slice เราสมมติแค่ ASCII ในส่วนนี้ การพูดถึง UTF-8 handling อย่างละเอียดอยู่ในส่วน “เก็บข้อความ UTF-8 ด้วย String” ของบทที่ 8

มาทำงานผ่านวิธีเขียน signature ของฟังก์ชันนี้โดยไม่ใช้ slice เพื่อเข้าใจ ปัญหาที่ slice จะแก้:

fn first_word(s: &String) -> ?

ฟังก์ชัน first_word มี parameter type &String เราไม่ต้องการ ownership ดังนั้นนี่ก็ดี (ใน Rust ที่ idiomatic ฟังก์ชันไม่รับ ownership ของ argument เว้นแต่ต้องการ และเหตุผลของเรื่องนั้นจะชัดเจนขึ้นเมื่อเราไปต่อ) แต่เรา ควร return อะไร? เราไม่มีวิธีพูดถึง ส่วน ของ string จริง ๆ อย่างไรก็ตาม เรา return index ของท้ายคำได้ บ่งบอกด้วย space ลองดู ตามที่แสดงใน Listing 4-7

Filename: src/main.rs
fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}
Listing 4-7: ฟังก์ชัน first_word ที่ return ค่า byte index เข้า parameter String

เพราะเราต้องไปผ่าน String ทีละ element และเช็คว่าค่าเป็น space หรือไม่ เราจะแปลง String เป็น array ของ byte โดยใช้เมธอด as_bytes

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

ถัดไป เราสร้าง iterator บน array ของ byte โดยใช้เมธอด iter:

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

เราจะพูดถึง iterator ในรายละเอียดใน บทที่ 13 ตอนนี้ รู้ว่า iter เป็นเมธอดที่ return แต่ละ element ใน collection และ enumerate ห่อผลของ iter แล้ว return แต่ละ element เป็นส่วนหนึ่งของ tuple แทน element แรกของ tuple ที่ return จาก enumerate คือ index และ element ที่สองคือ reference ของ element นี้สะดวกกว่าการคำนวณ index เองนิด หน่อย

เพราะเมธอด enumerate return tuple เราใช้ pattern destructure tuple นั้น ได้ เราจะพูดถึง pattern มากขึ้นใน บทที่ 6 ใน for loop เราระบุ pattern ที่มี i สำหรับ index ใน tuple และ &item สำหรับ single byte ใน tuple เพราะเราได้ reference ของ element จาก .iter().enumerate() เราใช้ & ใน pattern

ภายใน for loop เราค้นหา byte ที่แทน space โดยใช้ byte literal syntax ถ้าเราเจอ space เรา return ตำแหน่ง ไม่อย่างนั้น เรา return ความยาวของ string โดยใช้ s.len()

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

fn main() {}

ตอนนี้เรามีวิธีหา index ของท้ายคำแรกใน string แต่มีปัญหา เรา return usize เดี่ยว ๆ แต่มันเป็นตัวเลขที่มีความหมายเฉพาะในบริบทของ &String พูดอีกอย่าง เพราะมันเป็นค่าที่แยกจาก String ไม่มีการรับประกันว่ามันจะ ยัง valid ในอนาคต พิจารณาโปรแกรมใน Listing 4-8 ที่ใช้ฟังก์ชัน first_word จาก Listing 4-7

Filename: src/main.rs
fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}

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

    let word = first_word(&s); // word will get the value 5

    s.clear(); // this empties the String, making it equal to ""

    // word still has the value 5 here, but s no longer has any content that we
    // could meaningfully use with the value 5, so word is now totally invalid!
}
Listing 4-8: เก็บผลจากการเรียกฟังก์ชัน first_word แล้วเปลี่ยนเนื้อหา String

โปรแกรมนี้ compile โดยไม่มี error และจะทำเหมือนกันถ้าเราใช้ word หลังการ เรียก s.clear() เพราะ word ไม่ได้เชื่อมกับ state ของ s เลย word ยังมีค่า 5 เราใช้ค่า 5 กับตัวแปร s พยายามดึงคำแรกออกได้ แต่นี่จะเป็น bug เพราะเนื้อหาของ s เปลี่ยนตั้งแต่เราเซฟ 5 ใน word

การต้องห่วงว่า index ใน word จะหลุดจาก sync กับข้อมูลใน s น่าเบื่อและ เสี่ยง error! การจัดการ index เหล่านี้ยิ่งเปราะบางถ้าเราเขียนฟังก์ชัน second_word signature ของมันจะต้องดูเป็นแบบนี้:

fn second_word(s: &String) -> (usize, usize) {

ตอนนี้เรากำลังติดตาม index เริ่ม และ จบ และเรามีค่ามากขึ้นที่คำนวณจาก ข้อมูลใน state เฉพาะ แต่ไม่ผูกกับ state นั้นเลย เรามีตัวแปรไม่เกี่ยวข้อง สามตัวลอยอยู่ที่ต้องเก็บให้ sync กัน

โชคดี Rust มีคำตอบสำหรับปัญหานี้ — string slice

String Slice

string slice คือ reference ของลำดับ element ที่ติดกันของ String และ หน้าตาเป็นแบบนี้:

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

    let hello = &s[0..5];
    let world = &s[6..11];
}

แทนที่จะเป็น reference ของทั้ง String hello เป็น reference ของส่วน หนึ่งของ String ระบุใน [0..5] ที่เพิ่ม เราสร้าง slice โดยใช้ range ภายใน square bracket ระบุ [starting_index..ending_index] โดย starting_index คือตำแหน่งแรกใน slice และ ending_index คือหนึ่ง มากกว่าตำแหน่งสุดท้ายใน slice ภายใน โครงสร้างข้อมูล slice เก็บตำแหน่งเริ่ม และความยาวของ slice ซึ่งสอดคล้องกับ ending_index ลบ starting_index ดังนั้นในกรณีของ let world = &s[6..11]; world จะเป็น slice ที่มี pointer ไปยัง byte ที่ index 6 ของ s พร้อมค่าความยาว 5

Figure 4-7 แสดงเรื่องนี้ในไดอะแกรม

สามตาราง: ตารางแทนข้อมูล stack ของ s ซึ่งชี้ไปยัง byte ที่ index
0 ในตารางของข้อมูล string "hello world" บน heap ตารางที่สาม
แทนข้อมูล stack ของ slice world ซึ่งมีค่าความยาว 5 และชี้ไปยัง byte 6
ของตารางข้อมูล heap

Figure 4-7: string slice ที่อ้างถึงส่วนหนึ่งของ String

ด้วย syntax range .. ของ Rust ถ้าคุณอยากเริ่มที่ index 0 คุณ drop ค่า ก่อน period สองตัวได้ พูดอีกอย่าง เหล่านี้เท่ากัน:

#![allow(unused)]
fn main() {
let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];
}

ในทำนองเดียวกัน ถ้า slice ของคุณรวม byte สุดท้ายของ String คุณ drop ตัว เลขท้ายได้ นั่นหมายความว่าเหล่านี้เท่ากัน:

#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];
}

คุณยัง drop ทั้งสองค่าได้เพื่อรับ slice ของทั้ง string ดังนั้นเหล่านี้ เท่ากัน:

#![allow(unused)]
fn main() {
let s = String::from("hello");

let len = s.len();

let slice = &s[0..len];
let slice = &s[..];
}

หมายเหตุ: index range ของ string slice ต้องเกิดที่ขอบเขตอักขระ UTF-8 ที่ valid ถ้าคุณพยายามสร้าง string slice ในกลางของอักขระ multibyte โปรแกรม จะออกพร้อม error

ด้วยข้อมูลทั้งหมดนี้ในใจ มาเขียน first_word ใหม่เพื่อ return slice type ที่บ่งบอก “string slice” เขียนเป็น &str:

Filename: src/main.rs
fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

fn main() {}

เราได้ index สำหรับท้ายคำแบบเดียวกับที่เราทำใน Listing 4-7 โดยมองหาการ เกิดครั้งแรกของ space เมื่อเราเจอ space เรา return string slice โดยใช้ ต้น string และ index ของ space เป็น index เริ่มและจบ

ตอนนี้เมื่อเราเรียก first_word เราได้ค่าเดียวกลับมาที่ผูกกับข้อมูล ต้นทาง ค่าประกอบด้วย reference ของจุดเริ่มของ slice และจำนวน element ใน slice

การ return slice ก็ใช้ได้สำหรับฟังก์ชัน second_word:

fn second_word(s: &String) -> &str {

ตอนนี้เรามี API ที่ตรงไปตรงมาที่ทำพังยากกว่ามาก เพราะ compiler จะรับ ประกันว่า reference เข้า String ยัง valid จำ bug ในโปรแกรมใน Listing 4-8 ได้ไหม ตอนที่เราได้ index ท้ายคำแรก แต่แล้ว clear string จน index invalid? โค้ดนั้นเชิง logic ผิด แต่ไม่แสดง error ใด ๆ ทันที ปัญหาจะแสดงทีหลังถ้าเรายังพยายามใช้ index คำแรกกับ string ที่ว่างเปล่า Slice ทำให้ bug นี้เป็นไปไม่ได้ และให้เรารู้เร็วกว่ามากว่าเรามีปัญหากับ โค้ด การใช้ version slice ของ first_word จะโยน compile-time error:

Filename: src/main.rs
fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

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

    let word = first_word(&s);

    s.clear(); // error!

    println!("the first word is: {word}");
}

นี่คือ compiler error:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:18:5
   |
16 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
17 |
18 |     s.clear(); // error!
   |     ^^^^^^^^^ mutable borrow occurs here
19 |
20 |     println!("the first word is: {word}");
   |                                   ---- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` (bin "ownership") due to 1 previous error

จำจากกฎการ borrow ได้ว่าถ้าเรามี immutable reference ของบางอย่าง เราก็จะ รับ mutable reference เพิ่มไม่ได้ เพราะ clear ต้อง truncate String มันต้องรับ mutable reference println! หลังการเรียก clear ใช้ reference ใน word ดังนั้น immutable reference ต้องยัง active ณ จุดนั้น Rust ไม่อนุญาตให้ mutable reference ใน clear และ immutable reference ใน word มีอยู่ในเวลาเดียวกัน และ compile ล้มเหลว ไม่ใช่แค่ Rust ทำให้ API ของเราใช้ง่ายขึ้น มันยังกำจัด error ทั้งกลุ่มที่ compile time ด้วย!

String Literal คือ Slice

จำได้ว่าเราพูดถึง string literal ถูกเก็บภายใน binary ตอนนี้เรารู้เรื่อง slice เราเข้าใจ string literal ได้อย่างถูกต้อง:

#![allow(unused)]
fn main() {
let s = "Hello, world!";
}

type ของ s ที่นี่คือ &str — มันเป็น slice ที่ชี้ไปยังจุดเฉพาะของ binary นี่ก็เป็นเหตุผลที่ string literal เป็น immutable — &str เป็น immutable reference

String Slice เป็น Parameter

การรู้ว่าคุณรับ slice ของ literal และค่า String ได้ นำเราไปสู่การปรับ ปรุงอีกอย่างใน first_word คือ signature ของมัน:

fn first_word(s: &String) -> &str {

Rustacean ที่มีประสบการณ์มากกว่าจะเขียน signature ที่แสดงใน Listing 4-9 แทน เพราะมันให้เราใช้ฟังก์ชันเดียวกันกับทั้งค่า &String และค่า &str

fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

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

    // `first_word` works on slices of `String`s, whether partial or whole.
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s.
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or
    // whole.
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}
Listing 4-9: ปรับปรุงฟังก์ชัน first_word โดยใช้ string slice เป็น type ของ parameter s

ถ้าเรามี string slice เราส่งตรง ๆ ได้ ถ้าเรามี String เราส่ง slice ของ String หรือ reference ของ String ได้ ความยืดหยุ่นนี้ใช้ประโยชน์จาก deref coercion ฟีเจอร์ที่เราจะครอบคลุมในส่วน “ใช้ Deref Coercion ในฟังก์ชันและเมธอด” ของบทที่ 15

การประกาศฟังก์ชันให้รับ string slice แทน reference ของ String ทำให้ API ของเราทั่วไปและมีประโยชน์มากขึ้นโดยไม่เสียฟังก์ชันใด ๆ:

Filename: src/main.rs
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

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

    // `first_word` works on slices of `String`s, whether partial or whole.
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s.
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or
    // whole.
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

Slice อื่น ๆ

String slice อย่างที่คุณนึกได้ เฉพาะกับ string แต่ยังมี slice type ที่ ทั่วไปกว่าด้วย พิจารณา array นี้:

#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];
}

เหมือนที่เราอาจอยากอ้างถึงส่วนหนึ่งของ string เราอาจอยากอ้างถึงส่วนหนึ่ง ของ array เราทำได้แบบนี้:

#![allow(unused)]
fn main() {
let a = [1, 2, 3, 4, 5];

let slice = &a[1..3];

assert_eq!(slice, &[2, 3]);
}

slice นี้มี type &[i32] มันทำงานแบบเดียวกับ string slice ทำ โดยเก็บ reference ของ element แรกและความยาว คุณจะใช้ slice ชนิดนี้สำหรับ collection อื่น ๆ ทุกประเภท เราจะพูดถึง collection เหล่านี้ในรายละเอียด เมื่อเราพูดถึง vector ในบทที่ 8

สรุป

แนวคิดของ ownership, borrowing และ slice รับประกัน memory safety ใน โปรแกรม Rust ที่ compile time ภาษา Rust ให้คุณควบคุมการใช้หน่วยความจำใน แบบเดียวกับภาษา systems programming อื่น แต่การมี owner ของข้อมูล cleanup ข้อมูลอัตโนมัติเมื่อ owner ออกจาก scope หมายความว่าคุณไม่ต้องเขียนและ debug โค้ดเพิ่มเพื่อได้การควบคุมนี้

Ownership ส่งผลต่อวิธีที่หลายส่วนอื่นของ Rust ทำงาน เราจะพูดถึงแนวคิดเหล่า นี้ต่อตลอดส่วนที่เหลือของหนังสือ มาไปบทที่ 5 และดูการรวมชิ้นข้อมูลเข้าด้วย กันใน struct