ชนิดข้อมูล
ทุกค่าใน 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
| ความยาว | Signed | Unsigned |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| ขึ้นกับ architecture | isize | usize |
แต่ละ 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 | ตัวอย่าง |
|---|---|
| Decimal | 98_222 |
| Hex | 0xff |
| Octal | 0o77 |
| Binary | 0b1111_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