Kiểu dữ liệu
Mọi giá trị trong Rust đều thuộc về một kiểu dữ liệu cố định, cho Rust biết loại dữ liệu được gán để nó biết cách làm việc với dữ liệu đó. Chúng ta sẽ xem xét hai tập kiểu dữ liệu: vô hướng (scalar) và phức hợp (compound).
Hãy nhớ rằng Rust là một ngôn ngữ có kiểu tĩnh (statically typed), có nghĩa là
nó phải biết kiểu của tất cả các biến lúc biên dịch. Trình biên dịch có thể suy
luận ra kiểu chúng ta muốn dựa trên giá trị và cách chúng ta dùng nó. Trong
trường hợp có thể có nhiều kiểu, ví dụ như khi chúng ta chuyển đổi một String
sang một kiểu số bằng parse
trong phần [“So sánh số đoán với số bí mật”]
(comparing-the-guess-to-the-secret-number) ở Chương 2, chúng ta
phải khai báo kiểu, giống như sau:
let guess: u32 = "42".parse().expect("Not a number!");
Nếu chúng ta không khai báo kiểu ở đây, Rust sẽ hiển thị lỗi bên dưới, có nghĩa trình biên dịch cần nhiều thông tin hơn từ chúng ta để biết kiểu chúng ta muốn dùng:
$ cargo build
Compiling no_type_annotations v0.1.0 (file:///projects/no_type_annotations)
error[E0282]: type annotations needed
--> src/main.rs:2:9
|
2 | let guess = "42".parse().expect("Not a number!");
| ^^^^^ consider giving `guess` a type
For more information about this error, try `rustc --explain E0282`.
error: could not compile `no_type_annotations` due to previous error
Bạn sẽ thấy những khai báo kiểu khác nhau cho những kiểu dữ liệu khác nhau.
Kiểu vô hướng (Scalar Types)
Một kiểu scalar biểu thị một giá trị đơn lẻ. Rust có bốn kiểu scalar chính: số nguyên (integer), số thực, Booleans và ký tự. Bạn có lẽ đã nhận ra những kiểu này từ những ngôn ngữ lập trình khác. Hãy cùng xem chúng hoạt động thế nào trong Rust.
Kiểu số nguyên (Integer Types)
Một integer là một số không có phần phân số. Chúng ta đã sử dụng một kiểu
integer trong Chương 2, kiểu u32
. Khai báo kiểu này chỉ ra rằng giá trị nó
được liên kết là một số nguyên không dấu (unsigned integer) (kiểu số nguyên có
dấu bắt đầu với i
thay vì u
) chiếm 32 bit. Bảng 3-1 cho bạn thấy những kiểu
integer có sẵn trong Rust. Chúng ta có thể sử dụng bất kỳ variant nào để khai báo
kiểu của một giá trị integer.
Độ dài | Có dấu | Không dấu |
---|---|---|
8-bit | i8 |
u8 |
16-bit | i16 |
u16 |
32-bit | i32 |
u32 |
64-bit | i64 |
u64 |
128-bit | i128 |
u128 |
arch | isize |
usize |
Mỗi loại có thể là có dấu (signed) cũng như không dấu (unsigned) và có kích thước riêng. Signed và Unsiged nói đến việc liệu số có thể là số âm hay không, nói cách khác, liệu số có cần có dấu (signed) hay không, hay nó luôn dương và không cần dấu (unsigned). Giống như viết số trên giấy: khi việc có dấu là quan trọng, số 1 sẽ được viết với một dấu cộng hoặc một dấu trừ; tuy nhiên, khi nó an toàn để giả định rằng số là số dương, nó sẽ không cần dấu. Số có dấu được lưu trữ bằng biểu diễn bù hai.
Mỗi loại có dấu có thể lưu số từ -(2n - 1) tới 2n - 1 - 1,
n là số bit loại đó dùng. Vì vậy i8
có thể chứa các số từ
-(27) tới 27 - 1, tương đương từ -128 to 127. Những loại
không dấu có thể lưu những số từ 0 tới 2n - 1, vì vậy u8
có thể lưu
những số từ 0 đến 28 - 1, tương đương 0 tới 255.
Thêm vào đó, kiểu isize
và usize
phụ thuộc vào máy tính bạn đang chạy: 64
bit nếu nó là kiến trúc 64-bit và 32 bit nếu nó là kiến trúc 32-bit.
Bạn có thể viết chữ số integer dưới các dạng như trong Bảng 3-2. Lưu ý rằng những số mà có
thể thuộc về nhiều loại số khác nhau cho phép sử dụng hậu tố khai báo kiểu, ví dụ 57u8
.
Số cũng có thể sử dụng _
như dấu phân cách để dễ đọc hơn, ví dụ viết 1_000
cũng có giá
trị như 1000
.
Number literals | Example |
---|---|
Decimal | 98_222 |
Hex | 0xff |
Octal | 0o77 |
Binary | 0b1111_0000 |
Byte (u8 only) |
b'A' |
Vậy làm thế nào bạn biết được nên dùng kiểu integer nào? Nếu bạn không chắc
chắn, bạn có thể để mặc định của Rust là i32
. Tình huống chủ yếu khi phân vân giữa
isize
hay usize
là khi đánh chỉ mục một số tập hợp.
Tràn số integer (Integer Overflow)
Giả sử bạn đang có một biến
ở Chương 9.u8
với khoảng giá trị từ 0 đến 255. Nếu bạn cố gán cho biến một giá trị ngoài khoảng đó, 256 chẳng hạn, integer overflow sẽ xảy ra. Rust có một vài quy tắc khá thú vị liên quan đến hành vi này. Khi bạn biên dịch ở chế độ gỡ rối (debug mode), Rust bao gồm cả việc kiểm tra tràn số, thứ có thể làm cho chương trình của bạn panic lúc chạy. Rust dùng cụm từ panicking khi một chương trình do lỗi mà thoát; chúng ta sẽ thảo luận về panic sâu hơn ở phần “Lỗi không thể hồi phục vớipanic!
”Khi bạn biên dịch ở chế độ phát hành (release mode) với cờ
--release
, Rust không kiểm tra việc tràn số. Thay vào đó, nếu tràn số xảy ra, Rust thực hiện phép bù hai (two’s complement wrapping). Nói ngắn gọn, giá trị lớn hơn giá trị lớn nhất mà kiểu đó có thể biểu diễn được “làm tròn” về giá trị nhỏ nhất của kiểu đó. Trong trường hợpu8
, 256 trở thành 0, 256 trở thành 1 và cứ thế. Chương trình sẽ không panic, nhưng biến sẽ có giá trị không như bạn mong muốn. Tràn số được coi là một lỗi.Để xử lý nguy cơ tràn số, bạn có thể dùng những phương pháp standard library đã cung cấp cho những kiểu số cơ bản (primitive numeric type):
- Wrap trong tất cả các chế độ với các phương thức
wrapping_*
, ví dụwrapping_add
- Trả về giá trị
None
nếu có tràn số với các phương thứcchecked_*
- Trả về giá trị và một giá trị boolean biểu thị việc có bị tràn số hay không > với các phương thức
overflowing_*
- Bão hòa tại giá trị cực tiểu hoặc cực đại với các phương thức
saturating_*
Kiểu số thực dấu phẩy động (Floating-Point Types)
Rust cũng có hai kiểu cơ bản cho số thực, cụ thể hơn là số với phần thập phân.
Những kiểu số thực của Rust là f32
và f64
, tương ứng là 32 bit và 64 bit.
Kiểu mặc định là f64
vì trên những CPU hiện đại nó có tốc độ tương đương như
f32
nhưng lại có khả năng biểu diễn chính xác hơn. Tất cả kiểu số thực đều có dấu (signed).
Đây là một ví dụ về số thực trong thực tế:
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
Số thực dấu phẩy động được biểu diễn theo chuẩn IEEE-754. Kiểu f32
là một
số thực có độ chính xác đơn (single-precision) còn f64
có độ chính xác kép
(double precision).
Phép toán số học
Rust hỗ trợ những phép toán số học cơ bản cho tất cả các kiểu số: cộng, trừ,
nhân, chia và chia lấy dư. Phép chia số nguyên sẽ làm tròn kết quả xuống số nguyên gần
nhất. Đoạn code sau cho bạn thấy cách dùng từng phép toán trong statement let
.
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 floored = 2 / 3; // Results in 0
// remainder
let remainder = 43 % 5;
}
Mỗi biểu thức trong những statement này sử dụng một phép toán số học và tính ra một giá trị rồi gán vào một biến. Appendix B có chứa danh sách những phép toán mà Rust cung cấp.
Kiểu Boolean
Như hầu hết các ngôn ngữ lập trình khác, một kiểu Boolean trong Rust có hai giá
trị khả dĩ: true
và false
. Những biến Boolean có kích thước là một byte.
Kiểu Boolean trong Rust là bool
. Ví dụ:
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
}
Các giá trị Boolean thường được sử dụng thông qua các điều kiện, ví dụ như if
.
Chúng ta sẽ nói về cách hoạt động của if
trong Rust ở phần
“Luồng điều khiển”.
Kiểu ký tự
Kiểu char
của Rust là kiểu alphabet cơ bản nhất của ngôn ngữ, đoạn code sau
cho bạn thấy một cách để sử dụng nó.
Filename: src/main.rs
fn main() {
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '😻';
}
Chú ý ký tự kiểu char
được đặt trong nháy đơn, trái với chuỗi được đặt trong nháy kép.
Kiểu char
của Rust có kích thước bốn byte và biểu diễn một giá trị Unicode
Scalar, có nghĩa là nó có thể biểu diễn nhiều hơn là chỉ ASCII. Ký tự có dấu,
chữ Trung, Nhật, Hàn; emoji; và zero-width space đều là những giá trị char
hợp
lệ trong Rust. Những giá trị Unicode Scalar có biên từ U+0000
tới U+D7FF
và
bao gồm U+E000
tới U+10FFFF
. Tuy nhiên, một “character” không thực sự là một
khái niệm trong Unicode, nên cảm nhận của bạn về việc một “character” là gì có
thể không giống với một char
là gì trong Rust. Chúng ta sẽ thảo luận chủ đề
này ở “Lưu trữ văn bản mã hóa UTF-8 với Strings” trong
Chương 8.
Kiểu phức hợp (Compound Types)
Kiểu phức hợp có thể nhóm nhiều giá trị vào một kiểu. Rust có hai kiểu phức hợp cơ bản: tuple và array.
Kiểu Tuple
Một tuple là một cách chung để nhóm một số các giá trị thuộc những kiểu khác nhau vào một kiểu phức hợp. Tuple có độ dài cố định: một khi đã khai báo, bạn không thể tăng hay giảm kích thước của nó.
Chúng ta tạo một tuple bằng cách viết một danh sách phân cách bởi dấu phẩy trong dấu ngoặc đơn. Mỗi vị trí trong tuple có một kiểu riêng, và kiểu của các giá trị khác nhau trong tuple không cần phải giống nhau. Chúng ta đã thêm những chú thích về kiểu (type annotation) tùy chọn trong ví dụ sau:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
Biến tup
liên kết cả tuple, bởi vì một tuple được coi là một phần tử phức hợp
đơn. Để lấy những giá trị riêng biệt ra khỏi một tuple, chúng ta có thể sử dụng
pattern matching, như này:
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");
}
Chương trình này tạo một tuple và gán nó vào biến tup
. Nó sau đó sử dụng một
pattern với let
để lấy tup
và biến nó thành ba biến riêng rẽ, x
, y
và
z
. Điều này gọi là destructuring, bởi vì nó phá vỡ một tuple thành ba phần.
Cuối cùng, chương trình in ra giá trị của y
, 6.4
.
Chúng ta có thể truy cập trực tiếp một phần tử tuple bằng cách sử dụng dấu chấm (.
) theo
sau là chỉ số của giá trị mà chúng ta muốn truy cập. Ví dụ:
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;
}
Chương trình này tạo tuple x
, và truy cập vào mỗi phần tử trong tuple
bằng cách sử dụng các chỉ số (index) tương ứng của chúng. Như hầu hết các ngôn ngữ lập
trình, chỉ số đầu tiên trong một tuple là 0.
Tuple mà không chứa giá trị nào có một cái tên đặc biệt, unit. Giá trị và kiểu tương ứng của nó
đều được viết là ()
và đại diện cho một giá trị trống hoặc một kiểu trả về trống.
này được gọi là unit type và giá trị của nó được gọi là unit value. Expression trả về
unit value nếu nó không trả về bất kì giá trị nào khác.
Mảng (Array)
Cách khác để có một tập hợp nhiều giá trị là mảng. Không giống như tuple, mọi phần tử của một mảng phải cùng kiểu. Mảng trong Rust khác với mảng trong một số ngôn ngữ khác bởi vì mảng trong Rust có một độ dài cố định, giống như tuple.
Trong Rust, những giá trị trong một mảng được viết như một danh sách phân cách bởi dấu phẩy nằm trong ngoặc vuông:
fn main() {
let a = [1, 2, 3, 4, 5];
}
Mảng sẽ hữu dụng khi bạn muốn dữ liệu của bạn phân bổ trên ngăn xếp (stack) thay vì heap (chúng ta sẽ bàn stack và heap trong Chương 4) hoặc khi bạn muốn chắc chắn bạn luôn có một số cố định các phần tử. Mảng không linh động như kiểu vector. Một vector là một kiểu tập hợp tương tự cung cấp bởi standard library nhưng nó được phép thay đổi kích thước. Nếu bạn không chắc cần sử dụng mảng hay vector, bạn có thể nên dùng vector. Chương 8 sẽ nói chi tiết hơn về vector.
Tuy nhiên, mảng sẽ hữu ích hơn nếu bạn biết rằng số phần tử sẽ không thay đổi. Ví dụ nếu bạn đang dùng tên các tháng trong năm, bạn có thể dùng mảng thay vì vector vì bạn biết rằng nó sẽ luôn có 12 phần tử:
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
Bạn có thể viết kiểu của một mảng bằng dấu ngoặc vuông, trong dấu ngoặc vuông chứa kiểu của các phần tử, một dấu chấm phẩy và sau đó là số phần tử của mảng, như sau:
let a: [i32; 5] = [1, 2, 3, 4, 5];
Ở đây, i32
là kiểu của các phần tử. Sau dấu chấm phẩy, số 5
chỉ ra rằng mảng
chứa năm phần tử.
Nếu bạn muốn tạo một mảng với các phần tử có cùng giá trị, bạn có thể chỉ định giá trị khởi tạo, tiếp theo là một dấu chấm phẩy, và theo sau là độ dài của mảng trong ngoặc vuông:
let a = [3; 5];
Mảng có tên a
sẽ chứa 5
phần tử và đều được đặt giá trị khởi tạo là 3
.
Điều này tương tự như viết let a = [3, 3, 3, 3, 3];
nhưng theo cách ngăn gọn
hơn.
Truy cập phần tử của mảng
Một mảng là một đoạn bộ nhớ với kích thước cố định được biết trước có thể phân bổ trên stack. Bạn có thể truy cập các phần tử của một mảng bằng chỉ số (index), như thế này:
fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}
Trong ví dụ này, biến có tên first
sẽ nhận giá trị 1
, bởi vì đó là giá trị
tại chỉ số [0]
trong mảng. Biến có tên second
sẽ nhận giá trị 2
từ chỉ số
[1]
trong mảng.
Truy cập phần tử không hợp lệ trong mảng
Điều gì sẽ xảy ra nếu bạn cố truy cập một phần tử của một mảng mà nó đã đi quá kết thúc của mảng đó? Giả sử bạn sửa ví dụ như sau, sử dụng đoạn code tương tự như trò chơi đoán số trong Chương 2 để lấy một chỉ số mảng từ người dùng:
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}");
}
Đoạn code này biên dịch thành công. Nếu bạn chạy đoạn code này với cargo run
và nhập
0, 1, 2, 3 hoặc 4, chương trình sẽ in ra giá trị tương ứng của chỉ số đó trong mảng. Nhưng
nếu bạn nhập vào một số vượt quá kết thúc mảng, ví dụ 10, bạn sẽ thấy output như sau:
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:19:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Chương trình dẫn đến một lỗi thực thi (runtime error) tại thời điểm sử dụng một giá trị không hợp lệ
trong phép toán lấy chỉ số. Chương trình kết thúc với một tin nhắn lỗi và không thực thi
lệnh println!
cuối cùng. Khi bạn định truy cập một phần tử sử dụng chỉ số, Rust sẽ kiểm tra
liệu chỉ số bạn dùng có nhỏ hơn độ dài của mảng hay không. Nếu chỉ số lớn hơn
hoặc bằng chiều dài của mảng, Rust sẽ panic. Phép kiểm tra này phải được xảy ra lúc thực thi
(runtime), đặc biệt trong trường hợp này, bởi vì trình biên dịch không thể biết giá trị nào
người dùng sẽ nhập vào khi họ chạy code.
Đây là một ví dụ trong những nguyên tắc an toàn của Rust. Trong nhiều ngôn ngữ bậc thấp, loại kiểm tra này không được thực hiện, và khi bạn cung cấp một chỉ số không đúng, bộ nhớ không hợp lệ có thể bị truy cập. Rust bảo vệ bạn khỏi loại lỗi này bằng cách thoát ngay lập tức thay vì cho phép truy cập bộ nhớ và tiếp tục chạy. Chương 9 sẽ nói nhiều hơn về việc xử lý lỗi của Rust.
Source
https://doc.rust-lang.org/stable/book/ch03-02-data-types.html