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

ชนิดข้อมูลส่วนแบ่งข้อมูล (The Slice Type)

ส่วนแบ่งข้อมูล (Slices) ช่วยให้คุณสามารถสร้างตัวอ้างอิงชี้ไปยังลำดับสมาชิกที่จัดวางเรียงต่อกันเป็นพืดในโครงสร้าง คอลเลกชัน (collection) ได้อย่างสะดวก เนื่องจากส่วนแบ่งข้อมูลจัดเป็นประเภทตัวแปรอ้างอิงรูปแบบหนึ่ง มันจึงไม่มีสิทธิ์ครอบครองสิทธิ์ขาดความเป็นเจ้าของหน่วยความจำของข้อมูลนั้น

ต่อไปนี้เป็นโจทย์ปัญหาการเขียนโปรแกรมขนาดเล็ก: ให้ทดลองเขียนฟังก์ชันดึงค่าคำตอบรับข้อมูลชนิดข้อความ (string) ของคำศัพท์ต่างๆ ที่แบ่งแยกกันด้วยสัญลักษณ์เว้นวรรค และส่งผลลัพธ์คืนค่าออกมาเป็นคำแรกสุดที่ระบบสแกนตรวจพบในข้อความนั้น หากภายในประโยคอักขระดังกล่าวไม่มีสัญลักษณ์เว้นวรรคปรากฏอยู่เลย แสดงว่าข้อความทั้งหมดนั้นนับเป็นคำเดียว ให้ระบบทำการส่งคืนข้อความเต็มทั้งหมดนั้นกลับออกมา

หมายเหตุ: เพื่อวัตถุประสงค์ในการแนะนำความรู้เรื่องส่วนแบ่งข้อมูล (slices) เบื้องต้น เราจะทึกทักสมมติให้ข้อความทั้งหมดรองรับเฉพาะมาตรฐานอักขระ ASCII เท่านั้นในหัวข้อนี้ คู่มือคำอธิบายการจัดการระบบคำเข้ารหัส UTF-8 อย่างลึกซึ้งจะได้รับการอธิบายในบทที่ 8 หัวข้อ “การจัดเก็บข้อความเข้ารหัส UTF-8 ด้วย Strings”

ลองมาจัดทำวางโครงสร้างและเขียนระบุลายเซ็นของฟังก์ชันนี้โดยปราศจากการเรียกใช้งานส่วนแบ่งข้อมูลกันดูก่อน เพื่อทำความเข้าใจถึงปัญหาเชิงระบบที่จะมีเครื่องมือส่วนแบ่งข้อมูลเข้ามาช่วยปลดล็อกในภายหลัง:

fn first_word(s: &String) -> ?

ฟังก์ชัน first_word จะมีพารามิเตอร์ชนิด &String ซึ่งเราไม่มีความจำเป็นต้องดึงสิทธิ์การครอบครองหน่วยความจำของข้อมูลนั้นเข้ามา จึงประกาศพารามิเตอร์รูปนี้ได้ปกติ (ตามแนวปฏิบัติทั่วไปของ Rust ฟังก์ชันต่างๆ จะหลีกเลี่ยงการยึดถือสิทธิ์ครอบครองของอาร์กิวเมนต์ที่ส่งเข้ามา ยกเว้นในกรณีที่มีความจำเป็นจริงๆ ซึ่งเหตุผลความปลอดภัยจะกระจ่างแจ้งยิ่งขึ้นเมื่อเราศึกษาต่อไป) แต่ประเด็นคือเราควรจะระบุสเปกขากลับเป็นชนิดข้อมูลใดดี? เนื่องจากเดิมเราปราศจากเครื่องมือระบุพิกัดย่าน ชิ้นส่วนย่อย (part) ของข้อความได้โดยสะดวก แต่อย่างไรก็ตามเราอาจใช้วิธีคืนค่ากลับมาเป็นตำแหน่งหมายเลขดัชนีจุดสิ้นสุดของคำคำนั้น (ระบุพิกัดโดยตรวจสอบจากเว้นวรรคช่องแรกสุดที่พบ) ลองมาประยุกต์ใช้วิธีการนี้ดูกันในตารางที่ 4-7:

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() {}

และเนื่องจากระบบจำเป็นต้องประมวลผลไล่แสกนดูชิ้นส่วนตัวแักษรภายใน String ทีละตำแหน่งเพื่อตรวจเช็คว่าจุดใดคือเว้นวรรค เราจึงสั่งแปลงร่างอ็อบเจกต์ String ให้ไปเป็นแถวอาร์เรย์ของข้อมูลชนิดไบต์ (array of bytes) ผ่านการเรียกใช้เมธอด 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) บนอาร์เรย์ของข้อมูลไบต์นั้นผ่านการเรียกใช้เมธอด 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() {}

เราจะมาพูดคุยรายละเอียดลึกซึ้งเรื่องตัววนซ้ำประมวลผลนี้ใน บทที่ 13 สำหรับในตอนนี้ ให้จดจำเพียงว่า iter คือเมธอดที่คอยอำนวยความสะดวกส่งคืนสมาชิกแต่ละตัวของคอลเลกชันออกมาให้ และตัวเมธอด enumerate จะทำหน้าที่ครอบสรุปมูลค่าของ iter ดังกล่าวมาส่งกลับคืนมาในรูปกลุ่มข้อมูลทูเพิล (tuple) สองตำแหน่งแทน โดยสมาชิกตำแหน่งแรกสุดของทูเพิลที่ได้จาก enumerate คือหมายเลขลำดับดัชนี (index) และสมาชิกตำแหน่งที่สองคือข้อมูลตัวอ้างอิงชี้ไปยังอักขระตัวนั้น ซึ่งวิธีการนี้ช่วยเพิ่มความสะดวกแก่นักพัฒนามากกว่าการที่ต้องมาคอยเขียนสูตรคำนวณหมายเลขลำดับดัชนีขึ้นมาบวกเพิ่มเอง

เนื่องจากโครงสร้างขากลับของเมธอด enumerate รายงานผลเป็นทูเพิล เราจึงสามารถเลือกประยุกต์ใช้งานนิพจน์เงื่อนไขรูปแบบ Pattern เพื่อกระจายโครงสร้างตัวแปร (destructure) ของทูเพิลนั้นออกมาได้ทันที เราจะร่วมพูดคุยถึงหัวเรื่อง patterns เพิ่มเติมใน บทที่ 6 โดยในโครงสร้างลูป for นี้เราเขียนเงื่อนไขระบุรูปแบบจัดตั้งตัวแปร i มารับตำแหน่งดัชนี และสร้างตัวแปรอ้างอิง &item มารองรับชิ้นข้อมูลไบต์เดี่ยว และเนื่องจากผลรายงานสมาชิกจากกลไกคำสั่ง .iter().enumerate() ส่งคืนผลลัพธ์มาเป็นแบบการอ้างอิงสิทธิ์ เราจึงจำเป็นต้องเติมสัญลักษณ์เครื่องหมาย & ไว้เคียงหน้าชื่อตัวแปรในนิพจน์รูปแบบ Pattern ดักจับ

ภายในขอบเขตการรันวนลูป for เราดำเนินการสแกนค้นหาสัญลักษณ์ไบต์ที่เป็นตัวแทนของช่องว่างเว้นวรรคผ่านหลักไวยากรณ์ระบุตัวสัญลักษณ์ข้อมูลไบต์ (byte literal syntax) หากเราตรวจจับพบพิกัดเว้นวรรคดังกล่าวสำเร็จ เราจะส่งคืนตำแหน่งดัชนีชี้ตำแหน่งนั้นออกมาทันที และในทิศทางตรงกันข้ามหากประมวลผลจนจบลูปแล้วไม่พบตำแหน่งเว้นวรรคเลย เราส่งคืนความยาวเต็มข้อความออกไปด้วยประโยค 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() {}

ณ ตอนนี้เรามีวิธีกระบวนการสืบค้นพิกัดจุดสิ้นสุดของคำศัพท์แรกในข้อความสำเร็จแล้ว แต่ทว่าระบบการจัดสรรแบบนี้จะอุบัติปัญหาสำคัญขึ้นตามมา: นั่นคือการที่เราส่งคืนเฉพาะข้อมูลตัวเลขสเกลาร์ชนิด usize ออกมาเดี่ยวๆ โดยข้อมูลตัวเลขนี้จะไม่มีนัยสำคัญที่สมบูรณ์หรือถูกผูกสิทธิ์ค้ำประกันความปลอดภัยใดๆ ร่วมกับตัวแปรหลัก &String เลย กล่าวคือเนื่องจากตัวเลขนี้ลอยตัวเป็นอิสระต่างหาก จึงไม่มีความปลอดภัยใดๆ รับรองว่าข้อมูลประวัติเนื้อความข้อความของโปรแกรมจะไม่ผันแปรไปในระหว่างทาง ลองพิจารณาตัวอย่างซอร์สโค้ดในรายการที่ 4-8 ที่หยิบเอาฟังก์ชัน first_word จากรายการที่ 4-7 มาประยุกต์ใช้งานดู:

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!
}

โปรแกรมตัวอย่างนี้จะสามารถผ่านขั้นตอนการประกอบคอมไพล์ได้สำเร็จโดยปราศจากข้อผิดพลาดแจ้งเตือนใดๆ และจะยอมรันงานปกติแม้ว่าเราจะเขียนคำสั่งหยิบตัวแปร word ไปใช้งานต่อที่ด้านล่างหลังจากบรรทัดคำสั่งเคลียร์ข้อความ s.clear() ประมวลผลไปแล้ว เนื่องจากตัวแปรเก็บเลข word ไม่ได้เชื่อมโยงค้ำสิทธิ์ความปลอดภัยหรือรับรู้ถึงประวัติความเปลี่ยนแปลงของตัวแปรข้อความ s เลย ตัวแปร word จึงยังคงถือเก็บตัวเลข 5 ไว้ปกติ และหากเรานำเอาค่าข้อมูลเลข 5 นี้สลับกลับมาคีย์เพื่อพยายามสั่งให้ตัวแปร s แตกแยกคำข้อความแรกสุดออกมา จะเกิดความล้มเหลวข้อผิดพลาดในการประมวลผล (bug) ทันที เนื่องจากอักขระประวัติที่แท้จริงภายใน s ได้ถูกสลัดแปรสภาพไปหมดสิ้นแล้วตั้งแต่ก่อนจังหวะเวลาดึงค่าตัวเลข 5 จาก word มาใช้งาน

การที่นักพัฒนาโปรแกรมต้องคอยมานั่งพะวงระวังประเด็นตัวแปรเก็บพิกัดดัชนีอย่าง word หลุดวงรอบไม่สัมพันธ์กับค่าเนื้อความประวัติของ s ถือเป็นเรื่องที่น่าเบื่อหน่ายและเปิดช่องว่างให้เกิดบั๊กได้ง่ายมาก! และทิศทางการเขียนรหัสแบบจดหมายจำดัชนีนี้จะยิ่งเพิ่มความเปราะบางอย่างมากหากเราต้องการเขียนฟังก์ชันดึงคำที่สองเพิ่มขึ้นมาอย่างฟังก์ชัน second_word ลายเซ็นการระบุสเปกขากลับของฟังก์ชันคงต้องเขียนสเปกหน้าตาออกมาในรูปทำนองนี้:

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

ในรอบนี้เราจำเป็นต้องมาคอยจดจำพิกัดจุดตั้งต้น และ ดัชนีจุดสิ้นสุดไปพร้อมๆ กัน และส่งผลให้เรามีปริมาณตัวแปรเก็บตัวเลขจำนวนคงที่ที่เกิดจากการคำนวณของโครงสร้างประโยคในช่วงเวลานั้นๆ ปลิวว่อนอยู่ในระบบมากขึ้นโดยไม่มีสิ่งใดไปช่วยควบคุมดูแลความสอดคล้องกันเลย เราลงเอยด้วยการมีตัวแปรลอยตัวต่างหาก 3 ตัวแปรในขอบเขตงานซึ่งนักเขียนโค้ดต้องรับภาระควบคุมไม่ให้เกิดบั๊กขึ้นมาเอง

โชคดีมากที่ Rust ได้ตระเตรียมหนทางช่วยปิดประตูข้อบกพร่องข้อกังวลนี้ไว้เรียบร้อยแล้ว: ส่วนแบ่งข้อความ (string slices)

ส่วนแบ่งข้อความ (String Slices)

ส่วนแบ่งข้อความ (string slice) คือตัวแปรอ้างอิงประเภทระบุชี้ช่วงขอบเขตข้อมูลลำดับอักขระอักษรที่จัดเก็บเรียงเป็นพืดต่อกันภายใน String โดยมีรูปแบบไวยากรณ์การเขียนดังนี้:

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

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

แทนการอ้างอิงครอบคลุมพื้นที่ทั้งหมดของ String ตัวแปร hello จะทำหน้าที่เป็นตัวแทนเข้าอ้างอิงสิทธิ์เฉพาะบริเวณย่อยส่วนหนึ่งของ String โดยระบุขอบเขตช่วงไว้ด้วยชุดอักขระ [0..5] เพิ่มเติมที่ด้านท้ายวงเล็บเหลี่ยม เราสร้างส่วนแบ่งข้อมูลได้โดยใส่ชุดคำสั่งช่วงขอบเขต (range) ภายในเครื่องหมายวงเล็บเหลี่ยมในรูป [ดัชนีเริ่มต้น..ดัชนีสิ้นสุด] โดยที่ ดัชนีเริ่มต้น คือพิกัดตำแหน่งแรกสุดที่ต้องการหยิบใช้ภายในส่วนแบ่งข้อมูล และ ดัชนีสิ้นสุด คือพิกัดตัวเลขถัดถัดไปจากจุดปิดท้ายช่วง (บวกเพิ่มขึ้น 1 จากตำแหน่งสุดท้ายจริง) โดยโครงสร้างภายในของระบบข้อมูลส่วนแบ่งนี้จะดำเนินการจัดเก็บเฉพาะเบาะแสข้อมูลพิกัดจุดเริ่มต้นและขนาดความยาว (length) ของส่วนแบ่งข้อมูล ซึ่งมีขนาดสัมพันธ์สอดคล้องกับสมการนำเอา ดัชนีสิ้นสุด หักลบด้วย ดัชนีเริ่มต้น ดังนั้นกรณีตัวอย่างคำสั่ง let world = &s[6..11]; ตัวแปร world จะกลายโฉมเป็นข้อมูลส่วนแบ่งที่บรรจุข้อมูล pointer ชี้ไปยังตัวไบนารีไบต์ที่ดัชนี 6 ของ s และพ่วงด้วยมูลค่าความยาวเท่ากับ 5

รูปภาพที่ 4-7 แสดงทิศทางความสัมพันธ์นี้อย่างเห็นภาพชัดเจน:

Three tables: a table representing the stack data of s, which points to the byte at index 0 in a table of the string data "hello world" on the heap. The third table represents the stack data of the slice world, which has a length value of 5 and points to byte 6 of the heap data table.

รูปภาพที่ 4-7: ส่วนแบ่งข้อความ (string slice) อ้างอิงถึงบางส่วนของ String

ด้วยคุณสมบัติหลักไวยากรณ์อธิบายขอบเขตช่วง .. ของ Rust หากคุณประสงค์ต้องการระบุให้ขอบเขตเริ่มต้นนับตั้งแต่ตำแหน่งดัชนีที่ 0 เป็นต้นไป คุณสามารถละเว้นการพิมพ์ตัวเลขขอบเขตก่อนหน้าจุดสองจุดออกไปได้ กล่าวคือรูปประโยคทั้งคู่นี้ให้ผลลัพธ์ตรงกันเสมือนตัวเดียวกัน:

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

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

ทำนองเดียวกัน หากเป้าหมายการแยกส่วนแบ่งข้อมูลอ้างอิงของคุณประสงค์ต้องการครอบคลุมไปจนถึงพิกัดปิดท้ายไบต์สุดท้ายของ String คุณสามารถเขียนโดยละเว้นข้อมูลระบุตัวเลขขอบเขตด้านหลังเครื่องหมายจุดสองจุดทิ้งได้ ซึ่งรูปประโยคคู่นี้ส่งผลลัพธ์เทียบเท่ากันเช่นกัน:

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

let len = s.len();

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

คุณยังสามารถระบุละเว้นละทิ้งสัญลักษณ์ตัวเลขขอบเขตออกไปทั้งหมดพร้อมกันทั้งคู่เพื่อสื่อถึงการสร้างส่วนแบ่งอ้างอิงครอบคลุมเนื้อความของข้อความทั้งหมดตั้งแต่ต้นจนจบได้ ซึ่งรูปประโยคเหล่านี้ให้ผลลัพธ์เดียวกันประดุจตัวเดียวกัน:

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

let len = s.len();

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

หมายเหตุ: หมายเลขลำดับดัชนีช่วงขอบเขตของส่วนแบ่งข้อความ (String slice range indices) จะต้องได้รับการประกาศวางตำแหน่งไว้ตรงพิกัดช่วงรอยต่อรหัสตัวอักษรของ UTF-8 ที่สมบูรณ์และถูกต้อง (valid UTF-8 boundaries) เท่านั้น หากคุณพยายามจะเขียนโค้ดผ่ารอยแยกส่วนแบ่งข้อมูลครึ่งกลางตัวอักษรที่มีโครงสร้างแบบหลายไบต์ (multibyte character) ตัวโปรแกรมจะทำงานแครชและสั่งปิดการทำงานแสดงข้อผิดพลาดทันที

เมื่อเรารวบรวมข้อมูลความรู้พื้นฐานได้พร้อมสรรพแล้ว มาลองร่วมมือกันปรับแก้ปรับปรุงเนื้อโค้ดฟังก์ชัน first_word เพื่อให้ระบบดำเนินงานในรูปแบบส่งคืนผลลัพธ์กลับมาเป็นส่วนแบ่งข้อมูลอ้างอิงกันเลย โดยชนิดประเภทตัวแปรที่ใช้เป็นตัวแทนของความหมายคำว่า “ส่วนแบ่งข้อความ” จะเขียนระบุแสดงแทนด้วยชุดสัญลักษณ์ชนิดข้อมูล &str:

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() {}

เราใช้วิธีค้นหาหมายเลขลำดับพิกัดดัชนีปลายคำศัพท์ผ่านกระบวนการเดิมตามที่เราสร้างไว้ในตารางที่ 4-7 นั่นคือค้นหาดัชนีของเว้นวรรคตัวแรกสุดที่แสกนตรวจพบ เมื่อพิจารณาช่องว่างสำเร็จเรียบร้อย เราทำส่งคืนผลลัพธ์กลับมาเป็นส่วนแบ่งข้อความโดยระบุขอบเขตเริ่มต้นตั้งแต่ตำแหน่งเริ่มแรกของประโยคไปจนถึงตำแหน่งดัชนีของเว้นวรรคที่เป็นขอบเขตปิดท้าย

ในรอบนี้เมื่อเราดำเนินการส่งสัญญาณเรียกใช้งานฟังก์ชัน first_word ผลลัพธ์ที่เราได้รับตอบขากลับมาคือค่ามูลค่าตัวแปรเดี่ยวชิ้นเดียวซึ่งผูกสิทธิ์ค้ำความปลอดภัยโยงตรงไปยังชิ้นข้อมูลหลัก โดยภายในออบเจกต์ส่วนแบ่งนี้จะบรรจุตัวชี้อ้างอิงพิกัดตำแหน่งเริ่มต้นและข้อมูลระบุจำนวนสมาชิกขอบเขตของส่วนแบ่งข้อมูล

กระบวนการส่งคืนผลลัพธ์กลับเป็นส่วนแบ่งข้อมูลนี้ยังสามารถนำไปเขียนประยุกต์ใช้งานออกแบบฟังก์ชัน second_word ได้อย่างมีเสถียรภาพสอดคล้องกันด้วยเช่นกัน:

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

ตอนนี้นักพัฒนาจะได้มีโครงสร้างสเปกการเชื่อมต่อเรียกใช้งานโปรแกรม (API) ที่ชัดเจน สะอาดตา และยากที่จะเกิดข้อผิดพลาดในการเขียนโปรแกรมขึ้นมาอย่างมาก เนื่องจากคอมไพเลอร์ Rust จะเพิ่มมาตรการช่วยคุ้มกันความปลอดภัยคอยตรวจตราไม่ให้ตัวแปรอ้างอิงชิ้นย่อยใดๆ ใน String เกิดสภาวะลอยเคว้งสูญเสียสภาพ จำเรื่องบั๊กร้ายแรงในโปรเจกต์รายการที่ 4-8 ได้ใช่ไหมครับ ที่เราดึงเลขตำแหน่งดัชนีคำแรกออกมาจัดเก็บสำเร็จแล้วดันสั่งลบเนื้อหาข้อความต้นขั้วทิ้งจนเลขดัชนีเสียหายลอยเคว้ง? โค้ดส่วนนั้นหากพิจารณาทางตรรกศาสตร์ถือว่าบกพร่องผิดเพี้ยนเต็มๆ แต่โปรแกรมในตอนนั้นยอมปล่อยผ่านไร้การแจ้งเตือนความล้มเหลวทันที และปัญหาบั๊กประหลาดจะแอบโผล่มาสร้างผลกระทบต่อเราในภายหลังเมื่อเรานำพิกัดเลขดัชนีตกค้างนั้นไปอ้างอิงใช้งานกับข้อความว่างเปล่า การเปลี่ยนรูปแบบมาประยุกต์ใช้งานส่วนแบ่งข้อมูล (slices) จะสกัดและช่วยตัดสิทธิ์การเกิดข้อบกพร่องลักษณะดังกล่าวออกจากระบบอย่างถาวร และช่วยชี้เป้าปัญหารายงานความผิดพลาดให้เราทราบทันควันตั้งแต่ช่วงเขียนโค้ด การเรียกใช้งานฟังก์ชัน first_word เวอร์ชันส่วนแบ่งข้อมูลนี้จะส่งผลให้คอมไพเลอร์ปัดตกแสดงรายงานข้อผิดพลาดในขั้นตอนคอมไพล์ทันที:

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}");
}

นี่คือข้อความแสดงข้อผิดพลาดจากคอมไพเลอร์:

$ 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

จำกฎเหล็กกติกายืมใช้ข้อมูลได้ไหมครับว่า หากขอบเขตงานยังมีการเรียกใช้ตัวแปรอ้างอิงแบบแก้ไขไม่ได้ (immutable reference) คาไว้อยู่ ระบบจะไม่อนุญาตให้ประกาศแทรกตั้งตัวแปรอ้างอิงแบบแก้ไขได้ (mutable reference) ขอยืมซ้อนเข้ามาในเวลาเดียวกัน และเนื่องจากการทำคำสั่ง clear มีความจำเป็นต้องเข้ามาล้างปรับหดเนื้อความของ String ให้สั้นลง มันจึงต้องส่งคำขอรับสิทธิ์เป็นตัวอ้างอิงแก้ไขแบบ mutable และบรรทัดคำสั่ง println! ที่เขียนตามหลังคำสั่ง clear มีความพยายามที่จะดึงเอาค่าตัวแปรอ้างอิงใน word มาใช้งานต่อ ส่งผลให้ตัวแปรอ้างอิงแบบแก้ไขไม่ได้ตัวนั้นยังมีผลค้ำสิทธิ์ทำงานอยูอย่างต่อเนื่องจนถึงบรรทัดดังกล่าว Rust ปฏิเสธสิทธิ์การซ้อนทับกันของสิทธิ์แก้ไข mutable ใน clear และสิทธิ์แก้ไขไม่ได้ใน word ในเสี้ยวจังหวะเดียวกัน ส่งผลให้ขั้นตอนคอมไพล์ทำงานแครชและคอมไพเลอร์ปฏิเสธรันงาน การทำเช่นนี้ Rust ไม่เพียงแต่ช่วยออกแบบโครงสร้าง API ของเราให้อ่านเข้าใจง่ายขึ้น แต่ยังช่วยกำจัดขุดรากถอนโคนประเภทกลุ่มข้อบกพร่องบั๊กคลาสสิกต่างๆ ออกไปตั้งแต่ก่อนที่โปรแกรมจะถูกรันทำงาน!

ค่าคงที่ข้อความเป็นส่วนแบ่งข้อความ (String Literals as Slices)

จำได้ไหมที่เราเคยร่วมพูดคุยถึงลักษณะพิเศษของค่าคงที่ข้อความ (string literals) ว่าข้อมูลจริงจะได้รับการเขียนบันทึกเก็บจองพื้นที่ฝังไว้ในไฟล์ไบนารีหลักของโปรแกรม เมื่อเรามีความรู้พื้นฐานเรื่องส่วนแบ่งข้อมูล slices ครบถ้วนแล้ว เราจะเข้าใจโฉมหน้าแท้จริงของค่าคงที่ข้อความได้ชัดแจ้งยิ่งขึ้นดังนี้:

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

ชนิดประเภทข้อมูลที่แท้จริงของตัวแปร s ตรงจุดนี้คือ &str นั่นคือมันสวมบทบาทเป็นข้อมูลส่วนแบ่ง slice ที่ประทับตราตัวชี้ pointer โยงเจาะจงไประบุพิกัดที่อยู่ของหน่วยความจำผืนนั้นภายในไฟล์ไบนารีระบบ และนี่เป็นเหตุผลสำคัญที่อธิบายว่าทำไมอักขระประเภท literals จึงแก้ไขมูลค่าข้อมูลไม่ได้ เนื่องจากตัวแปรชนิด &str มีกติกาล็อกสถานะเป็นตัวแปรอ้างอิงแก้ไขค่าไม่ได้ (immutable reference) นั่นเอง

ส่วนแบ่งข้อความในฐานะตัวแปรพารามิเตอร์

เมื่อนักพัฒนาตระหนักดีแล้วว่าเราสามารถเขียนรหัสดึงชิ้นส่วนแบ่ง slices ออกมาจากกลุ่มค่าคงที่ข้อความ literals และอ็อบเจกต์ชนิด String ออกมาใช้งานได้อย่างอิสระ สิ่งนี้ช่วยนำทางชี้แนวทางการปรับแต่งเขียนสเปกลายเซ็นของฟังก์ชัน first_word ให้มีความยืดหยุ่นและน่าประยุกต์ใช้งานขึ้นอีกระดับ ดังนี้:

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

โปรแกรมเมอร์ชาว Rustacean ที่มีความเชี่ยวชาญส่วนใหญ่จะเลือกดีไซน์ระบุลักษณะลายเซ็นของฟังก์ชันปลายทางให้มีรูปแบบดังที่แสดงในรายการที่ 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);
}

หากเรามีส่วนแบ่งข้อความอยู่แล้ว เราสามารถส่งผ่านค่าตัวแปรนั้นป้อนเข้าไปได้โดยตรง หรือถ้าหากข้อมูลต้นทางของเราจัดเก็บอยู่ในรูปแบบอ็อบเจกต์ชนิด String เราสามารถส่งผ่านตัวอ้างอิงส่วนย่อยของ String นั้นหรือเขียนระบุอ้างอิงชี้ตัวแปร String เข้าไปประมวลผลได้เช่นกัน ความคล่องตัวยืดหยุ่นในการเขียนตรงจุดนี้จะทำงานเบื้องหลังโดยดึงฟีเจอร์เด่นเรื่องระบบแปลงสิทธิ์ความสัมพันธ์อ้างอิง (deref coercions) มาประมวลผลร่วม ซึ่งเราจะได้พูดคุยอธิบายรายละเอียดฟีเจอร์นี้ในบทที่ 15 หัวข้อ “การประยุกต์ใช้ประโยชน์กลไกแปลงสิทธิ์อ้างอิง Deref Coercions ในฟังก์ชันและเมธอด”

การออกแบบฟังก์ชันให้ยินยอมรับพารามิเตอร์เป็นประเภทส่วนแบ่งข้อความ (string slice) แทนที่การระบุผูกมัดเข้มงวดไว้กับสิทธิ์ตัวแปรอ้างอิงข้อความ String จะส่งผลให้โครงสร้าง API ของซอฟต์แวร์ที่เราสร้างมีรูปแบบที่เป็นสากลและเอื้ออำนวยการหยิบไปประยุกต์ใช้ในกรณีต่าง ๆ ได้สมบูรณ์ โดยไม่สูญเสียความสามารถดั้งเดิมใดๆ เลย:

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);
}

ส่วนแบ่งข้อมูลประเภทชนิดอื่นๆ (Other Slices)

ส่วนแบ่งข้อความ (String slices) นั้น ดังที่คุณน่าจะคาดเดาได้ว่าเป็นเครื่องมือที่ออกแบบขึ้นมาเฉพาะเจาะจงสำหรับประยุกต์ใช้จัดการกับอักขระอักษรเท่านั้น แต่ความจริงแล้วภาษา Rust ได้เตรียมโครงสร้างส่วนแบ่งทั่วไปสากล (general slice type) สำหรับนำมาประยุกต์ใช้กับโครงสร้างข้อมูลชนิดอื่นๆ ไว้ให้ด้วยเช่นกัน ลองพิจารณาอาร์เรย์ตัวนี้กัน:

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

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

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

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

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

ข้อมูลส่วนแบ่งชิ้นนี้จะจัดรายงานชนิดข้อมูลประเภท &[i32] ซึ่งจะมีพฤติกรรมหลักการรันคอมไพล์สอดคล้องเป็นพิมพ์เขียวเดียวกับการจัดการของส่วนแบ่งข้อความทุกประการ นั่นคือกักเก็บเบาะแสข้อมูล pointer ชี้ไปยังพิกัดเริ่มต้นย่อยของสมาชิกในอาร์เรย์พ่วงด้วยขนาดความยาวของส่วนแบ่ง โดยคุณจะได้หยิบยกเอาโครงสร้างส่วนแบ่งลักษณะอเนกประสงค์นี้ไปเขียนโค้ดจัดการกับระบบโครงสร้างกลุ่มข้อมูลสารสนเทศอื่นๆ อีกมากมาย เราจะกลับมาพูดคุยเจาะลึกเรื่องกลุ่มข้อมูลและเวกเตอร์อย่างเป็นระบบในบทที่ 8

สรุป

แนวคิดพื้นฐานเรื่องสิทธิ์การครอบครองหน่วยความจำ (ownership), การยืมใช้ข้อมูล (borrowing) ตลอดจนส่วนแบ่งข้อมูล (slices) ล้วนมีบทบาทสำคัญเป็นอย่างยิ่งคอยประสานงานปกป้องรักษาความปลอดภัยของหน่วยความจำของโปรแกรมในภาษา Rust ตั้งแต่จังหวะเวลาขั้นตอนคอมไพล์ ตัวภาษา Rust มอบสิทธิ์ในการควบคุมการจัดสรรใช้งานหน่วยความจำเครื่องให้แก่นักพัฒนาได้อย่างอิสระเทียบเคียงได้ดีเฉกเช่นวิถีของภาษาเขียนระบบอื่น ๆ แต่ด้วยการที่เรามีระบบบริหารจัดการที่ตัวแปรผู้ครอบครองสิทธิ์จะคอยทำลายกวาดล้างพื้นที่จัดเก็บข้อมูลให้อัตโนมัติเมื่อสิ้นสุดช่วงเวลาขอบเขตงาน ส่งผลให้นักพัฒนาโปรแกรมหมดภาระความกังวลที่จะต้องมาเขียนโค้ดปลดปล่อยหน่วยความจำและนั่งสืบหาไล่เช็คแก้ไขข้อผิดพลาดทางหน่วยความจำด้วยตนเอง

สิทธิ์การครอบครองหน่วยความจำส่งผลกระทบและกำหนดทิศทางวิธีการทำงานคุณสมบัติเด่นจุดอื่นๆ ของภาษา Rust แทบทุกมิติ ดังนั้นเราจะยังคงหยิบยกเอาแนวคิดพื้นฐานนี้ขึ้นมาอธิบายพ่วงขยายความต่อเนื่องกันไปตลอดซอร์สโค้ดของหนังสือเล่มนี้ มาเริ่มก้าวเดินทางต่อเข้าสู่บทที่ 5 เพื่อศึกษาแนวคิดการรวมกลุ่มกลุ่มข้อมูลเข้าด้วยกันภายในโครงสร้างวัตถุ struct กันเลยครับ