ข้ามไปยังเนื้อหา

Data Types

Scalar type แทนค่าเดียว Rust มี scalar type หลักสี่ประเภท

Integer type ฝัง signedness และขนาดบิตไว้ในชื่อ

SignedUnsignedขนาด
i8u88-bit
i16u1616-bit
i32u3232-bit (ค่าเริ่มต้น)
i64u6464-bit
i128u128128-bit
isizeusizeขนาด pointer

Integer type เริ่มต้นคือ i32 ใช้ usize สำหรับ index เข้าถึง array และ slice

Index ของ array และ slice ต้องเป็น usize เท่านั้น — ไม่ใช่ i32 ไม่ใช่ integer type อื่นใด นี่ไม่ใช่แค่ style ที่ชอบ คอมไพเลอร์ปฏิเสธ index type อื่นทันที

// @expect-error E0277
fn main() {
let v: [i32; 3] = [10, 20, 30];
let i: i32 = 1;
println!("{}", v[i]);
}
error[E0277]: the type `[i32]` cannot be indexed by `i32`
--> src/main.rs:5:22
|
5 | println!("{}", v[i]);
| ^ slice indices are of type `usize` or ranges of `usize`
|
= help: the trait `SliceIndex<[i32]>` is not implemented for `i32`
= note: required for `[i32]` to implement `Index<i32>`
= note: required for `[i32; 3]` to implement `Index<i32>`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.

แก้ด้วย v[i as usize] usize ถูกนิยามว่า “กว้างเท่า pointer บน platform นี้” เพราะมันมีไว้ index หน่วยความจำและวัดขนาดโดยเฉพาะ (.len() ก็ return usize เช่นกัน) — มันไม่ได้มีไว้แทนปริมาณที่อาจติดลบได้อย่างมีเหตุผล ดังนั้น Rust จึงไม่อนุญาตให้อะไรมาแทนมันแบบเงียบๆ

Rust มี f32 (single precision) และ f64 (double precision เป็นค่าเริ่มต้น) ควรใช้ f64 เสมอ เว้นแต่จะมีเหตุผลเฉพาะด้านหน่วยความจำหรือ precision ให้ใช้ f32

bool มีค่าเป็น true หรือ false char แทน Unicode scalar value เดียวและมีขนาด สี่ byte — รองรับ emoji, ตัวอักษร CJK และ Unicode codepoint อื่นๆ ทั้งหมด

fn main() {
let flag: bool = true;
let letter: char = 'R';
let emoji: char = '\u{1F980}'; // crab emoji
println!("flag={}, letter={}, emoji={}", flag, letter, emoji);
}

Tuple จัดกลุ่มค่าที่อาจมี type ต่างกันเข้าเป็นค่าเดียว มีความยาวคงที่ เข้าถึงแต่ละ element ด้วย dot notation และ index ที่นับจาก 0

fn main() {
let point: (i32, f64, bool) = (1, 2.5, false);
println!("{} {} {}", point.0, point.1, point.2);
// Destructuring a tuple
let (a, b, c) = point;
println!("a={}, b={}, c={}", a, b, c);
}

Array เก็บค่าจำนวนคงที่ที่มี type เดียวกัน annotation ของ type คือ [T; N] โดย T คือ element type และ N คือความยาว ทั้งสองรู้ที่ compile time ใช้ slice (&[T]) เมื่อต้องการ view ที่มีขนาดแบบ dynamic

fn main() {
let arr: [i32; 4] = [10, 20, 30, 40];
println!("first={}, last={}, len={}", arr[0], arr[3], arr.len());
// Repeat initializer: [value; count]
let zeros = [0_i32; 5];
println!("zeros len = {}", zeros.len());
}

ระบบ type ของ Rust เป็น static — ทุกค่ามี type ที่รู้ที่ compile time — แต่คุณแทบไม่ต้องเขียน type ออกมาเอง คอมไพเลอร์อนุมาน type จาก context คุณต้องใส่ annotation เมื่อการอนุมานมีความคลุมเครือเท่านั้น

fn main() {
let n = 42; // inferred as i32
let f = 1.5; // inferred as f64
let s = "hello"; // inferred as &str
// Suffix syntax forces a specific numeric type
let byte = 255_u8;
println!("{} {} {} {}", n, f, s, byte);
}

ต่างจาก C หรือ JavaScript, Rust ไม่เคยขยายหรือย่อ numeric type ให้คุณแบบเงียบๆ เลย — แม้แต่จาก type เล็กไป type ใหญ่กว่าที่เก็บทุกค่าที่เป็นไปได้ของ type เล็กได้แน่นอน i32 กับ i64 ผสมกันโดยไม่มี conversion ที่ชัดเจนไม่ได้:

// @expect-error E0308
fn main() {
let a: i32 = 5;
let b: i64 = 10;
let c: i32 = b;
println!("{}", a + c);
}
error[E0308]: mismatched types
--> src/main.rs:5:18
|
5 | let c: i32 = b;
| --- ^ expected `i32`, found `i64`
| |
| expected due to this
|
help: you can convert an `i64` to an `i32` and panic if the converted value doesn't fit
|
5 | let c: i32 = b.try_into().unwrap();
| ++++++++++++++++++++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.

Numeric conversion ทุกครั้งใน Rust คือการตัดสินใจที่คุณทำอย่างชัดเจน ด้วย as (truncate แบบเงียบๆ อธิบายถัดไป) หรือด้วย TryFrom/TryInto (fallible, return Result ดังนั้นค่าที่ out-of-range จะถูกจับได้แทนที่จะถูก truncate) ไม่มี automatic promotion ในทิศทางไหนเลย

as แปลงระหว่าง numeric type ต่างๆ การ cast ไปยัง integer type ที่เล็กกว่าจะ truncate: เก็บเฉพาะ bit ลำดับต่ำ ดังนั้นค่าที่ใส่ไม่พอดีจะกลายเป็นตัวเลขอื่นแบบเงียบๆ แทนที่จะ error

fn main() {
let big: i32 = 300;
let truncated = big as u8; // keeps only the low 8 bits
println!("truncated = {truncated}"); // 44, not 300
}

การคำนวณปกติ (+, -, *) เป็นคนละเรื่อง: มัน ไม่ truncate แต่จะตรวจสอบแทน ใน debug build การคำนวณที่ผลลัพธ์จริงไม่พอดีกับ type จะ panic แทนที่จะ wrap แบบเงียบๆ:

// @expect-panic
fn main() {
let x: u8 = std::hint::black_box(250);
let sum = x + 10; // 260 does not fit in a u8
println!("sum = {sum}");
}
thread 'main' (<id>) panicked at src/main.rs:4:15:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

release build (cargo build --release) ปิด overflow check เป็นค่าเริ่มต้นและ wrap แบบเงียบๆ แทนที่จะ panic — การพึ่งพาความแตกต่างนี้คือบั๊กที่รอวันโผล่ตอน production เท่านั้น ใช้ method family ที่ชัดเจนแทนด้านล่าง ซึ่งมีให้ทุก integer type:

Familyเมื่อ overflowReturn type
checked_add/checked_sub/checked_mulreturn None แทนที่จะ panic หรือ wrapOption<T>
wrapping_add/wrapping_sub/wrapping_mulwrap โดยตั้งใจ (bit เดียวกับ + แบบ release-mode)T
saturating_add/saturating_sub/saturating_mulclamp ไปที่ MIN/MAX ของ typeT
overflowing_add/overflowing_sub/overflowing_mulwrap และบอกด้วยว่า wrap หรือไม่(T, bool)
fn main() {
let x: u8 = std::hint::black_box(250);
println!("wrapping = {}", x.wrapping_add(10)); // 4
println!("checked = {:?}", x.checked_add(10)); // None
println!("saturating = {}", x.saturating_add(10)); // 255
println!("overflowing = {:?}", x.overflowing_add(10)); // (4, true)
}
flowchart TD
  Q{"x + y might overflow -
which do you want?"}
  Q -->|"crash if it happens
(a bug, catch it)"| Debug["plain + in a debug build
(panics on overflow)"]
  Q -->|"treat it as absent"| Checked["checked_add -> Option::None"]
  Q -->|"wrap on purpose"| Wrapping["wrapping_add -> wraps"]
  Q -->|"clamp to the edge"| Saturating["saturating_add -> MAX/MIN"]
  Q -->|"wrap, but tell me if it did"| Overflowing["overflowing_add -> (value, bool)"]
เลือก overflow strategy
fn main() {
// Scalar types
let n: i32 = -42;
let f: f64 = 3.14;
let b: bool = true;
let c: char = 'R';
println!("i32: {}, f64: {}, bool: {}, char: {}", n, f, b, c);
// Tuple: fixed-length, mixed types
let tup: (i32, f64, bool) = (1, 2.5, false);
println!("tuple.0 = {}, tuple.1 = {}, tuple.2 = {}", tup.0, tup.1, tup.2);
// Array: fixed-length, same type
let arr: [i32; 4] = [10, 20, 30, 40];
println!("arr[0] = {}, arr[3] = {}, len = {}", arr[0], arr[3], arr.len());
// Type inference
let inferred = 99_u8;
println!("inferred u8: {}", inferred);
}

ตัวเลขทุกตัวใน Rust พก width และ signedness ไว้เป็นส่วนหนึ่งของ type ไม่ใช่ property ตอน runtime ที่คุณ query แล้วแยก branch ได้ นั่นคือเหตุผลที่ไม่มี implicit conversion: i32 กับ i64 ต่างกันสำหรับ type checker พอๆ กับ i32 กับ bool — มันไม่ใช่ type เดียวกันเลย แม้ตัวหนึ่งจะเก็บทุกค่าของอีกตัวได้เสมอ การ cast ด้วย as คือคุณบอกคอมไพเลอร์ว่า “ฉันรู้ว่าอาจเสียข้อมูล ทำไปเลย”; ส่วน family checked_/wrapping_/saturating_/overflowing_ คือคุณบอกมันตรงๆ ว่าต้องการ behavior ไหนในสี่แบบ เมื่อ arithmetic — ไม่ใช่ cast — ให้ค่าที่ไม่พอดี

การบังคับให้ต้องมี usize ชัดเจนสำหรับ indexing และ conversion ชัดเจนระหว่าง i32 กับ i64 มีต้นทุนเป็นตัวอักษรเพิ่มขึ้นเล็กน้อยแทบทุกครั้งที่เข้าถึง array หรือคำนวณข้าม type แลกกับการที่บั๊กหมวดหมู่ “มันทำงานบนเครื่องฉันเพราะตัวเลขบังเอิญเล็ก” ทั้งหมดคอมไพล์ไม่ผ่านเลย type checker บังคับให้คุณตัดสินใจ ณ จุด conversion ว่าค่าที่ out-of-range ควร panic, wrap, saturate หรือรายงานกลับเป็น None แทนที่การตัดสินใจนั้นจะถูกทำแบบ implicit (และไม่คงเส้นคงวา) โดย CPU

  • “as เป็น conversion ที่ปลอดภัยและใช้ได้ทั่วไป” as ระหว่าง integer type truncate แบบเงียบๆ เมื่อ narrowing — มันจะไม่ panic เลย ซึ่งหมายความว่ามันเป็นเครื่องมือที่ผิดพอดีเมื่อค่า out-of-range บ่งบอกถึงบั๊กที่คุณอยากจับ ใช้ try_into() (return Result) แทนเมื่อ input อาจ out of range ได้
  • “Debug กับ release build ทำงานเหมือนกัน แค่ release เร็วกว่า” Integer overflow คือตัวอย่างที่ขัดแย้งชัดเจน: debug build panic เมื่อ overflow, release build wrap แบบเงียบๆ เป็นค่าเริ่มต้น โค้ดที่ “ทำงานได้” เพราะทดสอบใน debug mode ตลอดอาจพังทันทีที่ ship เป็น release
  • “usize ก็แค่ u32 บนเครื่อง 64-bit ใช้แทนกันได้” usize นิยามว่ากว้างเท่า pointer — 64 bit บน target desktop/server ทั่วไปวันนี้ แต่คอมไพเลอร์ยังถือว่ามันเป็น type แยกจาก u32 และ u64 และมันอาจเป็น 32 bit หรือ 16 bit บน target อื่นได้ การ cast ระหว่างกันยังต้องใช้ as หรือ try_into() ไม่ว่าจะคอมไพล์บน platform ไหน

โค้ดที่ AI เขียนมักจะ index Vec หรือ array ด้วย loop variable ที่ประกาศเป็น i32 (นิสัยที่ติดมาจากภาษาสไตล์ C) แล้วเติม cast as usize ตอนใช้งานแทนที่จะประกาศ loop variable เป็น usize ตั้งแต่แรก — หรือแย่กว่านั้นคือ cast ค่าที่อาจติดลบตรงไปเป็น usize เลย ซึ่งเปลี่ยนมันเป็นตัวเลขบวกมหาศาลแบบเงียบๆ แทนที่จะ panic

🐛 หา bug ในโค้ดนี้

fn get_last_n(items: &[i32], n: i32) -> &[i32] {
let start = items.len() as i32 - n;
&items[start as usize..]
}
เฉลย

ถ้า n มากกว่า items.len() แล้ว items.len() as i32 - n จะติดลบ — และการ cast i32 ที่ติดลบไปเป็น usize ไม่ panic แต่ตีความ bit ใหม่เป็นตัวเลขบวกมหาศาล (-1i32 as usize คือ usize::MAX บน target 64-bit) การเข้าถึง slice ที่ตามมาจึง panic ห่างจากบั๊กจริงมาก ด้วยข้อความที่งงว่า “range end index out of range for slice” แทนที่จะบอกชัดว่า n ใหญ่เกินไป ทางแก้คือเก็บการคำนวณความยาวไว้ใน usize และใช้ checked_sub เพื่อจับ underflow อย่างชัดเจน: let start = items.len().saturating_sub(n as usize);

เมื่อเขียน let x = 42 integer type เริ่มต้นคืออะไร?
ค่า char ใน Rust มีขนาดเท่าไร?
annotation ใดอธิบาย array ของค่า i32 จำนวนห้าค่า?
suffix ใน 255_u8 บอกอะไรแก่คอมไพเลอร์?
โค้ดนี้ compile ไหม?
fn main() {
    let v: [i32; 3] = [10, 20, 30];
    let i: i32 = 1;
    println!("{}", v[i]);
}
โค้ดนี้ print อะไร?
fn main() {
    let x: u8 = 250;
    println!("{:?}", x.checked_add(10));
    println!("{}", x.saturating_add(10));
}