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 ว่าเป็น ข้อมูลแบบไหน เพื่อให้รู้วิธีทำงานกับข้อมูลนั้น เราจะดู subset ของชนิด ข้อมูลสองกลุ่ม: scalar และ compound

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

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

ถ้าเราไม่เพิ่ม type annotation : u32 ที่แสดงในโค้ดข้างต้น Rust จะแสดง error ต่อไปนี้ ซึ่งหมายความว่า compiler ต้องการข้อมูลจากเราเพิ่มเพื่อรู้ ว่าจะใช้ type ไหน:

$ 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

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

Scalar Type

Type แบบ scalar แทนค่าเดียว Rust มี scalar type หลัก 4 แบบ: integer, floating-point number, Boolean และ character คุณอาจคุ้นเคยกับสิ่งเหล่านี้ จากภาษาโปรแกรมอื่น มาเริ่มดูว่าพวกมันทำงานยังไงใน Rust

Integer Type

integer คือตัวเลขที่ไม่มีส่วนทศนิยม เราใช้ integer type ตัวหนึ่งในบทที่ 2 คือ type u32 การประกาศ type นี้บ่งบอกว่าค่าที่มันผูกอยู่ควรเป็น unsigned integer (signed integer type ขึ้นต้นด้วย i แทน u) ที่กิน พื้นที่ 32 bit Table 3-1 แสดง integer type built-in ใน Rust เราใช้ variant ใด ๆ เหล่านี้ประกาศ type ของค่า integer ได้

Table 3-1: Integer Type ใน Rust

ความยาวSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
ขึ้นกับ architectureisizeusize

แต่ละ variant เป็น signed หรือ unsigned ก็ได้ และมีขนาดชัดเจน Signed และ unsigned อ้างถึงว่าตัวเลขเป็นค่าลบได้หรือไม่ — พูดอีกอย่าง ตัวเลข ต้องมีเครื่องหมายไหม (signed) หรือจะเป็นค่าบวกตลอด และสามารถแทนได้โดยไม่มี เครื่องหมาย (unsigned) เหมือนการเขียนตัวเลขลงกระดาษ — เมื่อเครื่องหมาย สำคัญ ตัวเลขแสดงด้วยเครื่องหมายบวกหรือลบ แต่เมื่อปลอดภัยที่จะสมมติว่า ตัวเลขเป็นบวก ก็แสดงโดยไม่มีเครื่องหมาย Signed number ถูกเก็บโดยใช้รูปแบบ two’s complement

แต่ละ signed variant เก็บตัวเลขจาก −(2n − 1) ถึง 2n − 1 − 1 inclusive ได้ โดยที่ n คือจำนวน bit ที่ variant นั้นใช้ ดังนั้น i8 เก็บตัวเลขจาก −(27) ถึง 27 − 1 ได้ ซึ่งเท่ากับ −128 ถึง 127 Unsigned variant เก็บตัวเลขจาก 0 ถึง 2n − 1 ได้ ดังนั้น u8 เก็บตัวเลขจาก 0 ถึง 28 − 1 ได้ ซึ่งเท่ากับ 0 ถึง 255

นอกจากนี้ type isize และ usize ขึ้นกับ architecture ของคอมพิวเตอร์ที่ โปรแกรมรัน: 64 bit ถ้าอยู่บน architecture 64-bit และ 32 bit ถ้าอยู่บน architecture 32-bit

คุณเขียน integer literal ในรูปแบบใด ๆ ที่แสดงใน Table 3-2 ได้ หมายเหตุว่า literal ตัวเลขที่เป็นได้หลาย type ตัวเลข อนุญาตให้ใส่ type suffix เช่น 57u8 เพื่อกำหนด type literal ตัวเลขยังใช้ _ เป็นตัวคั่นเชิง visual เพื่อทำให้ตัวเลขอ่านง่ายขึ้นได้ เช่น 1_000 ซึ่งมีค่าเดียวกับการระบุ 1000

Table 3-2: Integer Literal ใน Rust

รูปแบบ literalตัวอย่าง
Decimal98_222
Hex0xff
Octal0o77
Binary0b1111_0000
Byte (u8 เท่านั้น)b'A'

แล้วจะรู้ได้ยังไงว่าใช้ integer type ไหน? ถ้าไม่แน่ใจ default ของ Rust เป็นจุดเริ่มต้นที่ดีโดยทั่วไป — integer type default เป็น i32 สถานการณ์ หลักที่คุณจะใช้ isize หรือ usize คือเมื่อ index collection บางแบบ

Integer Overflow

สมมติว่าคุณมีตัวแปร type u8 ที่เก็บค่าระหว่าง 0 ถึง 255 ได้ ถ้าคุณ พยายามเปลี่ยนตัวแปรเป็นค่านอก range นั้น เช่น 256 integer overflow จะเกิดขึ้น ซึ่งให้ผลเป็นพฤติกรรมหนึ่งในสองอย่าง เมื่อคุณ compile ใน debug mode Rust รวมการเช็ค integer overflow ที่ทำให้โปรแกรม panic ตอน runtime ถ้าพฤติกรรมนี้เกิดขึ้น Rust ใช้คำว่า panicking เมื่อ โปรแกรมออกพร้อม error เราจะพูดถึง panic ในรายละเอียดเพิ่มเติมในส่วน “Unrecoverable Errors with panic! ในบทที่ 9

เมื่อคุณ compile ใน release mode ด้วย flag --release Rust ไม่ รวม การเช็ค integer overflow ที่ทำให้ panic แต่ถ้า overflow เกิดขึ้น Rust จะทำ two’s complement wrapping พูดสั้น ๆ ค่าที่มากกว่าค่าสูงสุดที่ type เก็บได้ จะ “wrap around” ไปเป็นค่าต่ำสุดที่ type เก็บได้ ในกรณีของ u8 ค่า 256 กลายเป็น 0, ค่า 257 กลายเป็น 1 และต่อ ๆ ไป โปรแกรมจะไม่ panic แต่ตัวแปรจะมีค่าที่อาจไม่ใช่สิ่งที่คุณคาดหวัง การพึ่งพฤติกรรม wrapping ของ integer overflow ถือว่าเป็น error

ในการจัดการความเป็นไปได้ของ overflow แบบ explicit คุณใช้ตระกูลเมธอด เหล่านี้ที่ standard library มีให้สำหรับ primitive numeric type:

  • Wrap ในทุก mode ด้วยเมธอด wrapping_* เช่น wrapping_add
  • Return ค่า None ถ้ามี overflow ด้วยเมธอด checked_*
  • Return ค่าและ Boolean บ่งบอกว่ามี overflow หรือไม่ ด้วยเมธอด overflowing_*
  • Saturate ที่ค่าต่ำสุดหรือสูงสุดของ type ด้วยเมธอด saturating_*

Floating-Point Type

Rust ยังมี primitive type สำหรับ floating-point number สองแบบ ซึ่งเป็น ตัวเลขที่มีจุดทศนิยม Floating-point type ของ Rust คือ f32 และ f64 ซึ่ง มีขนาด 32 bit และ 64 bit ตามลำดับ Default type คือ f64 เพราะบน CPU สมัยใหม่ มันเร็วพอ ๆ กับ f32 แต่ให้ความแม่นยำมากกว่า Floating-point type ทั้งหมดเป็น signed

นี่คือตัวอย่างที่แสดง floating-point number ในการใช้งาน:

Filename: src/main.rs

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

    let y: f32 = 3.0; // f32
}

Floating-point number ถูกแทนตามมาตรฐาน IEEE-754

Operation ทางตัวเลข

Rust รองรับ operation ทางคณิตศาสตร์พื้นฐานที่คุณคาดหวังสำหรับ type ตัวเลข ทั้งหมด: บวก, ลบ, คูณ, หาร และ remainder การหาร integer ตัดเศษไปทาง 0 จน ถึงจำนวนเต็มที่ใกล้ที่สุด โค้ดต่อไปนี้แสดงวิธีใช้ operation ทางตัวเลขแต่ละ อย่างใน statement 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;
}

แต่ละ expression ใน statement เหล่านี้ใช้ operator คณิตศาสตร์และประเมิน เป็นค่าเดียว ซึ่งจากนั้น bind กับตัวแปร ภาคผนวก B มีรายการ operator ทั้งหมดที่ Rust มี

Boolean Type

เช่นเดียวกับภาษาโปรแกรมอื่นส่วนใหญ่ Boolean type ใน Rust มีค่าที่เป็นไปได้ สองค่า: true และ false Boolean มีขนาด 1 byte Boolean type ใน Rust ระบุด้วย bool เช่น:

Filename: src/main.rs

fn main() {
    let t = true;

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

วิธีหลักในการใช้ค่า Boolean คือผ่าน conditional เช่น if expression เรา จะครอบคลุมวิธีที่ if expression ทำงานใน Rust ในส่วน “Control Flow”

Character Type

Type char ของ Rust เป็น primitive alphabetic type ที่สุดของภาษา นี่คือ ตัวอย่างการประกาศค่า char:

Filename: src/main.rs

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

หมายเหตุว่าเราระบุ literal char ด้วย single quotation mark ต่างจาก string literal ที่ใช้ double quotation mark Type char ของ Rust มีขนาด 4 byte และแทน Unicode scalar value ซึ่งหมายความว่ามันแทนได้มากกว่า ASCII เยอะ ตัวอักษรที่มีเครื่องหมาย, อักขระจีน-ญี่ปุ่น-เกาหลี, emoji และ zero- width space เป็นค่า char ที่ valid ใน Rust ทั้งหมด Unicode scalar value อยู่ในช่วง U+0000 ถึง U+D7FF และ U+E000 ถึง U+10FFFF inclusive อย่างไรก็ตาม “character” ไม่ได้เป็นแนวคิดใน Unicode จริง ๆ ดังนั้น สัญชาตญาณของคุณเรื่อง “character” คืออะไร อาจไม่ตรงกับ char ใน Rust เรา จะพูดถึงหัวข้อนี้ในรายละเอียดใน “เก็บข้อความ UTF-8 ด้วย String” ในบทที่ 8

Compound Type

Compound type จัดกลุ่มหลายค่าเป็น type เดียวได้ Rust มี primitive compound type สองแบบ: tuple และ array

Tuple Type

tuple คือวิธีทั่วไปในการจัดกลุ่มตัวเลขของค่าหลายค่าที่มี type หลากหลาย ให้เป็น compound type เดียว tuple มีความยาวคงที่ — เมื่อประกาศแล้ว ไม่ สามารถเติบโตหรือหดขนาดได้

เราสร้าง tuple โดยเขียนรายการค่าคั่นด้วย comma ภายในวงเล็บ แต่ละตำแหน่งใน tuple มี type และ type ของค่าต่าง ๆ ใน tuple ไม่จำเป็นต้องเหมือนกัน เรา เพิ่ม type annotation แบบ optional ในตัวอย่างนี้:

Filename: src/main.rs

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

ตัวแปร tup bind กับ tuple ทั้งหมด เพราะ tuple ถือเป็น compound element เดียว ในการดึงค่าแต่ละค่าออกจาก tuple เราใช้ pattern matching เพื่อ destructure ค่า tuple ได้ แบบนี้:

Filename: src/main.rs

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

    let (x, y, z) = tup;

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

โปรแกรมนี้สร้าง tuple ก่อน แล้ว bind มันกับตัวแปร tup จากนั้นใช้ pattern กับ let เพื่อเอา tup มาเปลี่ยนเป็นสามตัวแปรแยก: x, y และ z นี่ เรียกว่า destructuring เพราะแยก tuple เดียวออกเป็นสามส่วน สุดท้าย โปรแกรมพิมพ์ค่าของ y ซึ่งคือ 6.4

เรายังเข้าถึง element ของ tuple ตรง ๆ ได้ โดยใช้ period (.) ตามด้วย 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;
}

โปรแกรมนี้สร้าง tuple x แล้วเข้าถึงแต่ละ element ของ tuple ด้วย index ของพวกมัน เช่นเดียวกับภาษาโปรแกรมส่วนใหญ่ index แรกใน tuple คือ 0

tuple ที่ไม่มีค่าใด ๆ มีชื่อพิเศษว่า unit ค่านี้และ type ของมันเขียนเป็น () ทั้งคู่ และแทนค่าว่างหรือ return type ว่าง Expression จะ implicitly return ค่า unit ถ้าไม่ return ค่าอื่นใด

Array Type

อีกวิธีในการมี collection ของหลายค่าคือใช้ array ต่างจาก tuple ทุก element ของ array ต้องมี type เดียวกัน ต่างจาก array ในภาษาอื่นบางภาษา array ใน Rust มีความยาวคงที่

เราเขียนค่าใน array เป็นรายการคั่นด้วย comma ภายใน square bracket:

Filename: src/main.rs

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

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

อย่างไรก็ตาม array มีประโยชน์มากกว่า เมื่อคุณรู้ว่าจำนวน element ไม่ต้อง เปลี่ยน เช่น ถ้าคุณกำลังใช้ชื่อเดือนในโปรแกรม คุณคงใช้ array แทน vector เพราะคุณรู้ว่ามันจะมี 12 element เสมอ:

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

คุณเขียน type ของ array ด้วย square bracket พร้อม type ของแต่ละ element, semicolon และจำนวน element ใน array ดังนี้:

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

ที่นี่ i32 คือ type ของแต่ละ element หลัง semicolon เลข 5 บ่งบอกว่า array มี 5 element

คุณยัง initialize array ให้มีค่าเดียวกันสำหรับแต่ละ element ได้ โดยระบุ ค่าเริ่มต้น ตามด้วย semicolon และความยาวของ array ใน square bracket ดัง ที่แสดงที่นี่:

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

Array ชื่อ a จะมี 5 element ที่จะถูกตั้งเป็นค่า 3 ทั้งหมดในตอนเริ่มต้น ซึ่งเหมือนเขียน let a = [3, 3, 3, 3, 3]; แต่กระชับกว่า

เข้าถึง element ของ array

Array คือ chunk เดียวของหน่วยความจำที่มีขนาดรู้และคงที่ ที่ allocate บน stack ได้ คุณเข้าถึง element ของ array ด้วย indexing แบบนี้:

Filename: src/main.rs

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

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

ในตัวอย่างนี้ ตัวแปรชื่อ first จะได้ค่า 1 เพราะนั่นคือค่าที่ index [0] ใน array ตัวแปรชื่อ second จะได้ค่า 2 จาก index [1] ใน array

เข้าถึง element ของ array แบบ invalid

มาดูว่าเกิดอะไรขึ้นถ้าคุณพยายามเข้าถึง element ของ array ที่อยู่หลังท้าย array สมมติว่าคุณรันโค้ดนี้ คล้ายเกมทายตัวเลขในบทที่ 2 เพื่อรับ index ของ array จากผู้ใช้:

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

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

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 ในจุดที่ใช้ค่า invalid ใน operation indexing โปรแกรมออกพร้อม error message และไม่ execute statement println! สุดท้าย เมื่อคุณพยายามเข้าถึง element ด้วย indexing Rust จะ เช็คว่า index ที่คุณระบุน้อยกว่าความยาว array ถ้า index มากกว่าหรือเท่า กับความยาว Rust จะ panic การเช็คนี้ต้องเกิดตอน runtime โดยเฉพาะในกรณี นี้ เพราะ compiler ไม่มีทางรู้ว่าผู้ใช้จะป้อนค่าอะไรเมื่อรันโค้ดทีหลัง

นี่คือตัวอย่างของหลักการ memory safety ของ Rust ในการใช้งาน ในภาษา ระดับต่ำหลายภาษา การเช็คแบบนี้ไม่ได้ทำ และเมื่อคุณให้ index ที่ไม่ถูกต้อง หน่วยความจำ invalid ก็ถูกเข้าถึงได้ Rust ปกป้องคุณจาก error แบบนี้โดย ออกทันที แทนที่จะอนุญาตการเข้าถึงหน่วยความจำและทำงานต่อ บทที่ 9 พูดถึง การจัดการ error ของ Rust เพิ่มเติม และวิธีเขียนโค้ดที่อ่านได้และปลอดภัย ที่ไม่ panic และไม่อนุญาตการเข้าถึงหน่วยความจำ invalid