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

ชนิดข้อมูล

ทุกๆ ค่าในภาษา Rust จะต้องมี ชนิดข้อมูล (data type) ที่แน่นอน ซึ่งช่วยระบุให้ Rust รับทราบว่าเป็นข้อมูลประเภทใด เพื่อที่จะสามารถจัดการและทำงานกับข้อมูลนั้นๆ ได้อย่างถูกต้อง ในที่นี้เราจะมาดูชนิดข้อมูลย่อย 2 กลุ่มหลัก ได้แก่: ชนิดข้อมูลสเกลาร์ (scalar) และชนิดข้อมูลเชิงประกอบ (compound)

โปรดจำไว้ว่า Rust เป็นภาษาโปรแกรมที่ใช้ระบบตรวจชนิดข้อมูลแบบคงที่ (statically typed) ซึ่งหมายความว่ามันจำเป็นต้องทราบชนิดข้อมูลของตัวแปรทั้งหมดตั้งแต่ขั้นตอนคอมไพล์ โดยทั่วไปแล้วคอมไพเลอร์จะสามารถคาดเดาชนิดข้อมูลที่เราต้องการเลือกใช้งานได้โดยอิงตามค่าข้อมูลและรูปแบบการเรียกใช้งานของเรา แต่ในกรณีที่มีชนิดข้อมูลที่เป็นไปได้อยู่หลากหลายแบบ เช่น ตอนที่เราแปลงค่า String ไปเป็นประเภทตัวเลขผ่านคำสั่ง parse ในหัวข้อ “การเปรียบเทียบค่าที่ทายกับตัวเลขปริศนา” ในบทที่ 2 เราจำเป็นต้องระบุป้ายระบุชนิดข้อมูลกำกับ (type annotation) เพิ่มเติม ดังตัวอย่างนี้:

#![allow(unused)]
fn main() {
let guess: u32 = "42".parse().expect("Not a number!");
}

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

$ cargo build
   Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0284]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^        ----- type must be known at this point
  |
  = note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `guess` an explicit type
  |
2 |     let guess: /* Type */ = "42".parse().expect("Not a number!");
  |              ++++++++++++

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

คุณจะได้เห็นรูปแบบการระบุชนิดข้อมูลที่แตกต่างกันออกไปสำหรับข้อมูลชนิดอื่นๆ

ชนิดข้อมูลสเกลาร์ (Scalar Types)

ชนิดข้อมูลสเกลาร์ (scalar type) เป็นตัวแทนของมูลค่าข้อมูลชิ้นเดี่ยวๆ ภาษา Rust มีชนิดข้อมูลสเกลาร์หลักอยู่ 4 ประเภท ได้แก่: เลขจำนวนเต็ม (integers), เลขทศนิยม (floating-point numbers), ค่าความจริงทางตรรกศาสตร์ (Booleans) และตัวอักษร (characters) คุณอาจจะคุ้นเคยกับคำศัพท์เหล่านี้จากภาษาโปรแกรมอื่นๆ มาลองดูกันเลยว่าพวกมันทำงานอย่างไรใน Rust

ชนิดข้อมูลเลขจำนวนเต็ม (Integer Types)

เลขจำนวนเต็ม (integer) คือตัวเลขที่ไม่มีส่วนประกอบของเศษส่วนหรือจุดทศนิยม เราเคยเรียกใช้งานข้อมูลประเภทจำนวนเต็มชนิดหนึ่งไปแล้วในบทที่ 2 นั่นคือประเภทข้อมูล u32 การประกาศประเภทชนิดแบบนี้บ่งชี้ว่าค่าที่ผูกอยู่นั้นคือจำนวนเต็มแบบไม่มีเครื่องหมายลบ (unsigned integer) โดยที่ชนิดจำนวนเต็มแบบมีเครื่องหมาย (signed integer) จะขึ้นต้นด้วยสัญลักษณ์อักษร i แทนอักษร u และข้อมูลดังกล่าวมีขนาดพื้นที่หน่วยความจำ 32 บิต ตารางที่ 3-1 แสดงรายชื่อประเภทข้อมูลเลขจำนวนเต็มพื้นฐานภายในตัวของภาษา Rust เราสามารถเลือกใช้ประเภทใดในกลุ่มเหล่านี้ไปประกาศชนิดตัวแปรตัวเลขจำนวนเต็มได้ตามต้องการ

ตารางที่ 3-1: ชนิดข้อมูลเลขจำนวนเต็มใน Rust

ขนาดมีเครื่องหมายไม่มีเครื่องหมาย
8 บิตi8u8
16 บิตi16u16
32 บิตi32u32
64 บิตi64u64
128 บิตi128u128
ขึ้นกับสถาปัตยกรรมisizeusize

จำนวนเต็มในแต่ละประเภทย่อยนั้นสามารถเป็นแบบมีเครื่องหมายหรือไม่มีเครื่องหมายได้ และขนาดจะได้รับการระบุชัดเจน โดยคำว่า มีเครื่องหมาย (signed) และ ไม่มีเครื่องหมาย (unsigned) จะใช้เรียกอ้างอิงถึงความเป็นไปได้ที่จำนวนนั้นจะติดลบได้หรือไม่ กล่าวคือ ตัวเลขนั้นจำเป็นต้องมีเครื่องหมายประกอบ (+/-) หรือไม่ (มีเครื่องหมาย) หรือว่าค่าของมันจะเป็นบวกเสมอไปตลอดทำให้ไม่จำเป็นต้องระบุสัญลักษณ์เครื่องหมายไว้เคียงข้าง (ไม่มีเครื่องหมาย) มันคล้ายกับเวลาที่เราเขียนตัวเลขลงบนกระดาษ เมื่อสัญลักษณ์มีความสำคัญ ตัวเลขจะถูกเขียนโดยมีเครื่องหมายบวกรวมถึงลบกำกับอยู่ข้างหน้า แต่เมื่อเราทราบแน่ชัดว่าจำนวนดังกล่าวเป็นบวกแน่นอนอยู่แล้ว เราจะเขียนเฉพาะตัวเลขเปล่าๆ ปราศจากเครื่องหมาย ชนิดข้อมูลตัวเลขแบบมีเครื่องหมายจะถูกจัดเก็บโดยใช้รูปแบบการแทนค่าด้วย ส่วนเติมเต็มสอง (two’s complement)

ชนิดข้อมูลแบบมีเครื่องหมาย (signed) ในแต่ละขนาดย่อยจะเก็บค่าตัวเลขตั้งแต่มูลค่า −(2n − 1) ไปจนถึง 2n − 1 − 1 (นับรวมขอบเขตด้วย) โดยที่ค่า n คือขนาดของจำนวนบิตข้อมูลที่ตัวเลือกชนิดย่อยนั้นๆ เรียกใช้งาน ดังนั้นตัวแปรชนิด i8 จะเก็บตัวเลขได้จากย่าน −(27) ถึง 27 − 1 ซึ่งมีค่าเทียบเท่ากับช่วง −128 ถึง 127 ส่วนประเภทตัวแปรไม่มีเครื่องหมาย (unsigned) จะเก็บมูลค่าตัวเลขสเกลาร์ได้จาก 0 ถึง 2n − 1 ดังนั้นชนิดข้อมูล u8 จึงเก็บข้อมูลได้ในช่วงตั้งแต่ 0 ถึง 28 − 1 ซึ่งเทียบเท่ากับช่วง 0 ถึง 255

นอกจากนี้ ประเภทข้อมูลชนิด isize และ usize จะเปลี่ยนไปตามประเภทสถาปัตยกรรมชิปเซ็ตประมวลผล (architecture) ของเครื่องคอมพิวเตอร์ที่ตัวโปรแกรมของคุณกำลังทำงานอยู่ โดยจะมีขนาด 64 บิตหากรันอยู่บนระบบประมวลผลสถาปัตยกรรมแบบ 64 บิต และมีขนาด 32 บิตหากรันบนระบบ 32 บิต

คุณสามารถเขียนค่าคงที่ตัวเลขจำนวนเต็ม (integer literals) ได้หลากหลายรูปแบบตามตัวอย่างที่ระบุในตารางที่ 3-2 พึงสังเกตว่าเลขจำนวนคงที่ที่มีโอกาสตีความได้เป็นข้อมูลหลายประเภทนั้นอนุญาตให้ผู้เขียนเสริมสัญลักษณ์ต่อท้ายชื่อชนิด (type suffix) เข้าไปได้ เช่น 57u8 เพื่อระบุบ่งบอกชนิดตัวแปรทันที ยิ่งไปกว่านั้น ค่าตัวเลขยังอนุญาตให้เติมสัญลักษณ์ _ คั่นระหว่างตัวเลขตามความเหมาะสมเพื่อช่วยให้อ่านข้อมูลจำแนกตัวเลขได้ง่ายขึ้น เช่น 1_000 ซึ่งจะได้รับการประมวลผลเป็นตัวเลขเดียวกับค่า 1000

ตารางที่ 3-2: รูปแบบการเขียนค่าคงที่ตัวเลขจำนวนเต็มใน Rust

รูปแบบการเขียนตัวเลขตัวอย่าง
Decimal (ฐานสิบ)98_222
Hex (ฐานสิบหก)0xff
Octal (ฐานแปด)0o77
Binary (ฐานสอง)0b1111_0000
Byte (u8 เท่านั้น)b'A'

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

ปัญหาตัวเลขเกินขอบเขตข้อมูล (Integer Overflow)

สมมติว่าคุณมีตัวแปรประเภทข้อมูลชนิด u8 ซึ่งสามารถเก็บมูลค่าขอบเขตตัวเลขอยู่ระหว่างช่วง 0 ถึง 255 หากคุณพยายามป้อนสั่งแก้ไขตัวแปรดังกล่าวให้เป็นค่านอกย่าน เช่น 256 จะเกิดปรากฏการณ์ ตัวเลขเกินขอบเขตข้อมูล (integer overflow) ขึ้น ซึ่งสามารถนำไปสู่ผลลัพธ์พฤติกรรมการทำงานได้ 2 แบบ หากคุณสั่งประกอบโปรแกรม (compiling) ในโหมดดีบั๊ก (debug mode) Rust จะเพิ่มระบบตรวจสอบความปลอดภัยเพื่อป้องกันปัญหาตัวเลขเกินขอบเขตนี้ ซึ่งจะมีผลให้ตัวโปรแกรมยุติทำงานกะทันหันหรือหยุดชะงัก (panic) ในขณะที่โปรแกรมรัน (runtime) หากพบลักษณะพฤติกรรมนี้ ภาษา Rust จะเรียกใช้คำว่า panicking หรือการเกิดตื่นตระหนกเพื่ออธิบายสถานะที่ตัวโปรแกรมปิดการทำงานเนื่องจากเกิดข้อผิดพลาดรุนแรง เราจะได้พูดคุยรายละเอียดเรื่อง panics นี้ลึกขึ้นในหัวข้อ “ข้อผิดพลาดที่ไม่สามารถกู้คืนได้ด้วย panic! ในบทที่ 9

เมื่อคุณดำเนินการคอมไพล์ประกอบโปรแกรมในโหมดใช้งานจริง (release mode) ด้วยการป้อนพ่วงแฟล็ก --release Rust จะ ไม่ได้ เพิ่มกลไกตรวจสอบระบบป้องกันตัวเลขเกินขอบเขตเพื่อสั่งหยุดโปรแกรม (panic) มาให้ ในทางกลับกันหากเกิดปัญหาตัวเลขเกินขอบเขตขึ้น Rust จะทำการคำนวณสลับค่าวนทับสอดคล้องกับหลักการ two’s complement wrapping สรุปอย่างย่นย่อคือ ค่าที่มากกว่าช่วงสูงสุดที่ชนิดข้อมูลจะยอมรับจะถูกนำมาเปรียบเทียบคำนวณวนลูปกลับมาเริ่มต้นจากค่าต่ำสุดที่ชนิดนั้นจะเก็บได้ สำหรับตัวแปรชนิด u8 ค่าตัวเลข 256 จะผันค่าสลับมาเป็น 0 และค่าตัวเลข 257 จะผันค่ามาเป็น 1 ไปเรื่อยๆ เป็นลำดับ แม้ว่าตัวโปรแกรมจะไม่ได้สั่งปิดการทำงานกะทันหัน (panic) แต่ค่าตัวแปรจะเปลี่ยนสภาพไปแสดงข้อมูลประหลาดที่คุณคงไม่ได้คาดหมายไว้ตั้งแต่ตอนเขียนโปรแกรม การปล่อยให้โปรแกรมพึ่งพาพฤติกรรมจำลองคำนวณวนทับลักษณะนี้จะถือเป็นข้อผิดพลาดในการเขียนโปรแกรม

หากต้องการรับมือกับปัญหาความเป็นไปได้ที่จะเกิดสภาวะตัวเลขล้นขอบเขตนี้อย่างเป็นทางการ คุณสามารถเลือกเรียกใช้งานกลุ่มเมธอดเสริมที่เตรียมไว้ให้ในไลบรารีมาตรฐานสำหรับตัวเลขพื้นฐานประเภทย่อยต่าง ๆ ดังนี้:

  • ปรับแก้ในเชิงวนค่าทับกลับมาในทุกๆ โหมดคอมไพล์ด้วยกลุ่มเมธอด wrapping_* เช่น wrapping_add
  • สั่งให้คืนค่าผลลัพธ์เป็น None หากเกิดปัญหาล้นขอบเขตขึ้นด้วยกลุ่มเมธอด checked_*
  • สั่งคืนผลการคำนวณพร้อมค่าความจริง Boolean เพื่อรายงานว่ามีค่าล้นขอบเขตหรือไม่ด้วยกลุ่มเมธอด overflowing_*
  • ล็อกขอบเขตการคำนวณไว้ที่ขอบสูงสุดหรือขอบต่ำสุดเพื่อจำกัดค่าด้วยกลุ่มเมธอด saturating_*

ชนิดข้อมูลเลขทศนิยม (Floating-Point Types)

นอกจากนี้ Rust ยังได้เตรียมชนิดข้อมูลพื้นฐาน 2 ชนิดสำหรับจัดเก็บข้อมูล เลขทศนิยม (floating-point numbers) ซึ่งก็คือตัวเลขที่มีจุดทศนิยมประกบอยู่ ข้อมูลประเภททศนิยมของ Rust คือชนิด f32 และ f64 ซึ่งมีขนาดขอบเขตหน่วยความจำ 32 บิตและ 64 บิตตามลำดับ โดยที่ชนิดเริ่มต้นระบบของ Rust สำหรับเลขทศนิยมคือประเภท f64 เนื่องจากในหน่วยประมวลผล (CPUs) สมัยใหม่ การคำนวณด้วยชนิด f64 จะมีระดับความเร็วใกล้เคียงเทียบเท่ากับการทำงานของชนิด f32 แต่ความแม่นยำของผลลัพธ์ทศนิยมจะดีกว่ามาก และข้อมูลประเภททศนิยมทุกชนิดจะมีเครื่องหมายกำกับ (+/-) เสมอ

ตัวอย่างต่อไปนี้จะแสดงวิธีการประยุกต์ใช้งานเลขทศนิยมในการทำงานจริง:

Filename: src/main.rs

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

ข้อมูลประเภทเลขทศนิยมได้รับการจัดรูปแบบการแสดงผลตามข้อตกลงมาตรฐานอ้างอิงสากล IEEE-754

การคำนวณทางคณิตศาสตร์ (Numeric Operations)

Rust รองรับระบบคำนวณพื้นฐานทางคณิตศาสตร์อย่างครบถ้วนตามความต้องการของชนิดตัวเลข ได้แก่: การบวก, การลบ, การคูณ, การหาร และการหาเศษ (modulo) พึงระลึกว่าขั้นตอนการหารตัวเลขจำนวนเต็ม (integer division) จะทำการตัดทิ้งเศษทศนิยมทั้งหมดและปัดเศษเข้าหาเลขศูนย์เสมอ โค้ดต่อไปนี้แสดงภาพตัวอย่างวิธีกำหนดการคำนวณวิชาคณิตศาสตร์เบื้องต้นในแต่ละแบบด้วยคำสั่ง let:

Filename: src/main.rs

fn main() {
    // addition
    let sum = 5 + 10;

    // subtraction
    let difference = 95.5 - 4.3;

    // multiplication
    let product = 4 * 30;

    // division
    let quotient = 56.7 / 32.2;
    let truncated = -5 / 3; // Results in -1

    // remainder
    let remainder = 43 % 5;
}

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

ชนิดข้อมูลค่าความจริง (Boolean Type)

เช่นเดียวกับแนวทางของภาษาโปรแกรมส่วนใหญ่ ชนิดข้อมูลค่าความจริง (Boolean type) ของ Rust จะประเมินค่าแสดงผลได้เป็น 2 สถานะ ได้แก่: true (จริง) และ false (เท็จ) โดยที่ข้อมูลประเภทนี้มีขนาดพื้นที่จัดเก็บ 1 ไบต์ ชนิดข้อมูลประเภทนี้จะเขียนประกาศด้วยคำสำคัญว่า bool ตัวอย่างเช่น:

Filename: src/main.rs

fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

พฤติกรรมหลักในการประยุกต์นำค่าความจริง Boolean ไปเขียนโค้ดคือส่วนเงื่อนไขการทำงาน เช่น นิพจน์เงื่อนไขตัวกรองอย่าง if เราจะร่วมสำรวจขั้นตอนทำงานนิพจน์ if อย่างเป็นระบบในหัวข้อ “โครงสร้างควบคุมการทำงาน” ถัดไป

ชนิดข้อมูลตัวอักษร (Character Type)

ชนิดข้อมูล char ของ Rust เป็นรูปแบบชนิดข้อมูลอักขระขั้นพื้นฐานที่สุดในตัวภาษา ตัวอย่างการประกาศค่าตัวอักษร char มีดังนี้:

Filename: src/main.rs

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
}

พึงสังเกตว่าการเขียนค่าคงที่ตัวอักษร char (char literals) จะต้องสั่งครอบเขียนด้วยเครื่องหมายอัญประกาศเดี่ยว (single quotation marks) เสมอ ตรงกันข้ามกับการเขียนข้อมูลข้อความ (strings) ที่จะครอบเขียนด้วยเครื่องหมายอัญประกาศคู่ (double quotation marks) โดยที่ข้อมูลประเภทอักขระ char ของ Rust จะมีขนาดหน่วยความจำ 4 ไบต์ และเป็นตัวแทนของรหัส Unicode Scalar Value ซึ่งหมายความว่าจะสามารถรองรับและพิมพ์ตัวอักษรได้กว้างขวางครอบคลุมมากกว่าระบบ ASCII มาตรฐานดั้งเดิม เช่น ตัวอักษรที่มีเครื่องหมายกำกับการออกเสียง (accented letters), ตัวอักษรภาษาจีน ภาษาญี่ปุ่น และภาษาเกาหลี, สัญลักษณ์อิโมจิ ตลอดจนช่องว่างไร้ความกว้าง (zero-width spaces) ก็ล้วนถือเป็นค่าข้อมูล char ที่ถูกต้องของ Rust โดยขอบเขตของ Unicode Scalar Value จะอยู่ในช่วงครอบคลุมตั้งแต่รหัสอ้างอิง U+0000 ถึง U+D7FF และรหัส U+E000 ถึง U+10FFFF (นับรวมขอบเขตด้วย) อย่างไรก็ตามคำว่า “ตัวอักษร” หรือ “อักขระ” ในความรับรู้โดยธรรมชาติของมนุษย์อาจจะไม่ตรงกันพอดิบพอดีกับข้อมูลสเปก char ของ Rust ในเชิงเทคนิคคอมพิวเตอร์ เราจะหยิบยกประเด็นนี้นำมาขยายความลึกซึ้งในบทที่ 8 หัวข้อ “การจัดเก็บข้อความเข้ารหัส UTF-8 ด้วย Strings”

ชนิดข้อมูลเชิงประกอบ (Compound Types)

ชนิดข้อมูลเชิงประกอบ (compound types) ช่วยให้เราสามารถรวบรวมกลุ่มข้อมูลหลากหลายชิ้นเอาไว้รวมกันภายใต้ชนิดข้อมูลหลักชนิดเดียวได้ ภาษา Rust มีชนิดข้อมูลเชิงประกอบพื้นฐานอยู่ 2 รูปแบบ ได้แก่: ทูเพิล (tuples) และอาร์เรย์ (arrays)

ชนิดข้อมูลทูเพิล (Tuple Type)

ทูเพิล (tuple) คือวิธีการมาตรฐานทั่วไปในการรวบรวมกลุ่มข้อมูลสารสนเทศต่างๆ ที่อาจจะมีประเภทชนิดข้อมูลที่แตกต่างกัน ให้เข้ามารวมกลุ่มอยู่ในชุดโครงสร้างข้อมูลเชิงประกอบชิ้นเดียว โดยที่ทูเพิลจะมีขนาดความยาวคงที่ (fixed length) เมื่อได้ประกาศสร้างขึ้นมาแล้ว ขนาดของมันจะไม่สามารถขยายใหญ่ขึ้นหรือหดสั้นลงได้อีก

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

Filename: src/main.rs

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

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

Filename: src/main.rs

fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {y}");
}

โปรแกรมตัวอย่างนี้จะสร้างทูเพิลแล้วสั่งผูกข้อมูลไว้กับตัวแปร tup จากนั้นจึงใช้นิพจน์ควบคุมคำสั่ง let แตกย่อยโครงสร้างตัวแปรของ tup กระจายออกไปจัดเก็บเป็นตัวแปรใหม่จำนวน 3 ตัว ได้แก่ x, y และ z ซึ่งกระบวนการนี้เรียกว่า การกระจายโครงสร้าง (destructuring) เนื่องจากเป็นการแยกชิ้นส่วนของทูเพิลตัวเดี่ยวออกเป็น 3 ส่วนย่อย และขั้นตอนสุดท้ายตัวโปรแกรมจะแสดงผลค่าของ y ซึ่งมีค่าเป็น 6.4

เราสามารถก็ตรงเข้าไปเรียกข้อมูลภายในทูเพิลแต่ละตำแหน่งได้โดยตรงผ่านการเขียนสัญลักษณ์จุดทศนิยม (.) แล้วพ่วงต่อท้ายด้วยหมายเลขดัชนี (index) ของตำแหน่งข้อมูลที่เราต้องการเรียกใช้ ตัวอย่างเช่น:

Filename: src/main.rs

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

โปรแกรมนี้จะสร้างตัวแปรทูเพิล x แล้วดำเนินการเรียกข้อมูลของสมาชิกแต่ละตำแหน่งในทูเพิลด้วยดัชนีระบุตำแหน่งของมัน และเช่นเดียวกับภาษาโปรแกรมทั่วไป ตำแหน่งแรกของโครงสร้างทูเพิลจะเริ่มนับดัชนีตำแหน่งแรกเป็นดัชนีที่ 0 เสมอ

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

ชนิดข้อมูลอาร์เรย์ (Array Type)

อาร์เรย์ (array) เป็นอีกหนึ่งทางเลือกในการจัดกลุ่มเก็บรายการข้อมูลหลากหลายชิ้น แต่จะมีความต่างไปจากทูเพิลคือ สมาชิกทุกตำแหน่งข้อมูลภายในอาร์เรย์จะต้องมีประเภทชนิดข้อมูลที่สอดคล้องตรงกันทุกประการ และสอดคล้องกับพฤติกรรมของทูเพิลคือ โครงสร้างอาร์เรย์ของภาษา Rust จะมีขนาดความยาวคงที่ (fixed length)

เราเขียนสร้างค่าข้อมูลภายในอาร์เรย์ด้วยการป้อนรายการข้อมูลคั่นด้วยสัญลักษณ์จุลภาคไว้ภายในวงเล็บเหลี่ยม (square brackets):

Filename: src/main.rs

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

อาร์เรย์มีประโยชน์มากเมื่อคุณต้องการเก็บจองข้อมูลตัวแปรไว้ในส่วนหน่วยความจำแบบสแตก (stack) สอดคล้องกับชนิดข้อมูลพื้นฐานอื่นๆ ทั้งหมดที่เราเห็นไปก่อนหน้านี้ แทนที่จะนำไปจัดเก็บในหน่วยความจำส่วนฮีป (heap) (เราจะได้ร่วมพูดคุยถึงความแตกต่างระหว่าง stack และ heap ละเอียดใน บทที่ 4) หรือเมื่อคุณต้องการรับประกันอย่างเข้มงวดว่าโปรแกรมจะคงขนาดจำนวนสมาชิกที่แน่นอนไปตลอดการทำงาน แต่อย่างไรก็ดี อาร์เรย์จะไม่มีความยืดหยุ่นในการปรับแต่งขนาดได้เหมือนกับโครงสร้างประเภทเวกเตอร์ (vector) โดยที่เวกเตอร์จัดเป็นกลุ่มประเภทข้อมูลจัดเก็บรายการรูปแบบคล้ายคลึงกันที่ถูกบรรจุไว้ในคลังไลบรารีมาตรฐาน ซึ่ง สามารถ ปรับขยายขนาดเพิ่มหรือหดเล็กลงได้ตามความจำเป็นเนื่องจากโครงสร้างข้อมูลย่อยภายในตัวจะถูกจัดเก็บจองไว้บนพื้นที่ของ heap หากคุณยังไม่มีความแน่ใจว่าจะเลือกใช้โครงสร้างข้อมูลระหว่างอาร์เรย์หรือเวกเตอร์ดีกว่ากัน โดยทั่วไปแล้วแนะนำให้เลือกเรียกใช้เวกเตอร์เป็นหลัก สามารถอ่านข้อมูลเพิ่มเติมของเวกเตอร์ได้ใน บทที่ 8

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

#![allow(unused)]
fn main() {
let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];
}

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

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

ในที่นี้ประเภท i32 บ่งบอกชนิดข้อมูลของสมาชิกแต่ละตำแหน่งในอาร์เรย์ และสัญลักษณ์หลังเครื่องหมายอัฒภาคคือตัวเลข 5 ระบุแสดงว่าอาร์เรย์ตัวนี้มีสมาชิกจัดเก็บอยู่ 5 ตัว

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

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

อาร์เรย์ชื่อ a นี้จะถูกประกอบด้วยสมาชิกจำนวน 5 ตัว โดยสมาชิกทุกตัวจะถูกตั้งค่าคงที่เริ่มแรกให้มีมูลค่าเป็นตัวเลข 3 การทำแบบนี้จะให้ผลลัพธ์เดียวกับการพิมพ์ว่า let a = [3, 3, 3, 3, 3]; แต่เขียนได้สั้นกระชับและสะอาดตากว่ามาก

การเข้าถึงสมาชิกภายในอาร์เรย์

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

Filename: src/main.rs

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

    let first = a[0];
    let second = a[1];
}

ในตัวอย่างนี้ ตัวแปรชื่อ first จะได้รับจัดเก็บมูลค่าเป็น 1 เนื่องจากเป็นข้อมูลที่ถูกจองตำแหน่งไว้ที่ตำแหน่งแรกคือดัชนีชี้ [0] ในอาร์เรย์ และตัวแปรชื่อ second จะได้รับจัดเก็บมูลค่าเป็น 2 จากดัชนีชี้ตำแหน่ง [1]

การเข้าถึงข้อมูลสมาชิกอาร์เรย์ออกนอกขอบเขตที่ไม่ถูกต้อง

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

Filename: src/main.rs

use std::io;

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

    println!("Please enter an array index.");

    let mut index = String::new();

    io::stdin()
        .read_line(&mut index)
        .expect("Failed to read line");

    let index: usize = index
        .trim()
        .parse()
        .expect("Index entered was not a number");

    let element = a[index];

    println!("The value of the element at index {index} is: {element}");
}

โค้ดส่วนนี้จะสามารถผ่านขั้นตอนคอมไพล์ได้ผ่านฉลุย หากคุณสั่งรันโปรแกรมนี้ผ่านคำสั่ง cargo run แล้วกรอกเลขนำเข้าเป็น 0, 1, 2, 3 หรือ 4 ตัวโปรแกรมจะทำงานและแสดงค่าของข้อมูลตำแหน่งนั้นตามปกติ แต่ถ้าคุณลองเปลี่ยนมาป้อนตัวเลขขอบเขตที่เลยออกไปนอกรายการของขนาดอาร์เรย์ เช่น เลข 10 คุณจะได้รับผลลัพธ์ปรากฏดังนี้:

thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

โปรแกรมได้เกิดข้อผิดพลาดขึ้นในขณะรันโปรแกรม (runtime error) ตรงจุดที่ผู้ใช้สั่งดึงดูข้อมูลผ่านเลขดัชนีชี้ตำแหน่งที่ไม่ถูกต้อง ตัวโปรแกรมเลือกที่จะปิดตัวเองและรายงานข้อความความล้มเหลวออกมาทันทีโดยข้ามการทำงานในแถวสุดท้ายที่มีฟังก์ชัน println! ไปเลย เมื่อใดก็ตามที่คุณพยายามดึงข้อมูลสมาชิกผ่านดัชนีชี้ Rust จะสั่งประมวลผลด่านตรวจเพื่อเทียบว่าค่าดัชนีที่คุณป้อนนั้นมีขนาดน้อยกว่าความยาวของอาร์เรย์จริงหรือไม่ และถ้าดัชนีดังกล่าวมีค่ามากกว่าหรือเท่ากับขนาดความยาว Rust จะสั่ง panic หยุดโปรแกรมทันที ซึ่งขั้นตอนการกรองตรวจสอบความปลอดภัยลักษณะนี้จำเป็นต้องดำเนินการตรวจสอบในช่วงโปรแกรมรัน (runtime) โดยเฉพาะอย่างยิ่งกรณีเช่นนี้ เนื่องจากตัวคอมไพเลอร์ไม่มีทางล่วงรู้ได้เลยว่าผู้ใช้งานจะกรอกป้อนตัวเลขใดเข้ามาให้โปรแกรมในตอนประมวลผล

นี่เป็นตัวอย่างเชิงรูปธรรมที่แสดงให้เห็นถึงการรักษาความปลอดภัยของหน่วยความจำ (memory safety) ของภาษา Rust ในภาษาโปรแกรมระดับต่ำ (low-level languages) อื่น ๆ หลายภาษามักจะไม่มีด่านช่วยสกัดกรองตรวจสอบแบบนี้ ซึ่งเมื่อผู้เขียนสั่งดึงดัชนีผิดตำแหน่ง ระบบของภาษาเหล่านั้นอาจจะแอบเข้าไปดึงอ่านข้อมูลที่อยู่ในหน่วยความจำจุดอื่นๆ ที่ไม่พึงประสงค์ได้โดยตรง แต่ Rust ได้จัดเตรียมระบบปกป้องคุณจากสภาวะเสี่ยงภัยลักษณะนี้ด้วยการปิดกระบวนการและปิดตัวลงทันทีแทนการปล่อยผ่านให้โปรแกรมเข้าถึงหน่วยความจำตำแหน่งแปลกปลอมแล้วประมวลผลงานต่อไป ในบทที่ 9 จะอธิบายถึงการจัดการความผิดพลาดของ Rust เพิ่มเติม รวมถึงวิธีการเขียนโค้ดที่ทั้งอ่านง่าย ปลอดภัย ไม่แครช (panic) และป้องกันสิทธิ์เข้าถึงพฤติกรรมพื้นที่หน่วยความจำต้องห้ามอย่างเหมาะสม