Biến (variable)
Bài viết này sẽ hướng dẫn bạn làm quen với biến (variable) trong Dart. Cách khai báo khởi tạo biến, giá trị mặc định của chúng, các kiểu dữ liệu; tìm hiểu sơ qua về null safety, type safety; phân biệt được final và const, var và dynamic.
Biến đơn giản là một cái tên chúng ta đặt cho một vùng bộ nhớ nơi mà ta lưu trữ dữ liệu. Mọi thứ bạn có thể gán cho một biến đều là một đối tượng (object). Ngay cả số, hàm và null cũng đều là là các object. Tất cả các chúng đều kế thừa từ lớp Object ngoại trừ null (nếu bạn bật Sound null safety).
Nói sơ qua về Null safety
Null safety nói nôm na là: biến không thể gán giá trị null trừ
khi chúng ta chỉ định rõ rằng biến đó được phép null. Trong Dart để chỉ định rằng biến được phép
null (nullable variable) chúng ta thêm dấu ?
vào ngay sau kiểu dữ
liệu
khi khai báo biến. Ví dụ: int? age
. Tại thời điểm này bạn chỉ cần
lưu
ý một điều là biến sẽ có 2 loại: nullable (cho phép null) và non-nullable (không cho phép null).
Cụ thể cách khai báo chúng ta sẽ tìm hiểu ở phần dưới.
Mục đích của Null safety là nhằm ngăn ngừa các lỗi do truy cập không chủ ý vào các biến được đặt
thành null. Ví dụ, bạn code chung với đồng nghiệp, mọi người thỏa thuận với nhau rằng đối tượng
công dân bắt buộc phải có chứng minh nhân dân (cmnd), nên là trường cmnd sẽ không được có giá
trị null. Tuy nhiên một ai đó trong lúc nhầm lẫn đã gán giá trị null cho trường cmnd (vì không
có null safety nên mọi biến đều có thể chứa giá trị null, lúc biên dịch sẽ không thấy báo lỗi
gì, hoàn toàn hợp lệ). Bây giờ
nếu bạn thao tác gì đó với trường cmnd này (vd: you.cmnd.length
),
bạn
sẽ dính lỗi Null Pointer. Với Null safety,
trường cmnd ngay từ đầu sẽ được khai báo là một biến non-nullable (không chứa giá trị null).
Ngay khi đồng nghiệp của bạn vô tình gán cmnd = null thì người đó sẽ nhận được thông báo lỗi
ngay lập tức khi biên dịch rằng trường này không cho phép null. Vậy là lỗi sẽ không xảy ra.
Ban đầu Dart không có Null safety, cho đến tháng 3 năm 2021, với Dart 2.12 thì Null safety mới được hỗ trợ. Và cũng bắt đầu từ đây, cụm từ "nếu bạn bật Null safety" được đưa vào rất nhiều trong các tài liệu hướng dẫn mà bạn thấy trên mạng. Lý do đơn giản là vì nếu bạn bật Null safety thì cách khai báo biến, cách sử dụng sẽ khác với việc không bật Null safety. Và nếu bạn chưa biết trước điều này, mình cam đoan là nhiều bạn sẽ bị "loạn não", sao chỗ này bày thế này, chỗ kia bày thế kia, ai đúng? ai sai?
Bạn nên bật Null safety (thực ra là từ Dart 2.12 Null safety đã mặc định được bật, nên là bạn không nên tắt Null safety). Cho dù bạn có tắt đi nữa thì tương lai bạn cũng phải bật, vì đã có thông báo là từ Dart 3 (2023), Null safety là bắt buộc. Và cũng vì vậy, trong tất cả bài viết của mình, mặc định là luôn có Null safety (khi hướng dẫn cách khai báo biến chẳng hạn), mình không quan tâm đến những thứ không có null safety, cái gì nên loại bỏ cứ loại bỏ, nhét nhiều vô đầu đau não.
Mình có một bài viết khác về Null safety cụ thể hơn ở đây: Null safety.
Nói sơ qua về Type safety
Lại thêm một cái nói qua, nhưng khá quan trọng và cũng có liên quan ở phần tiếp theo, nên là bạn chịu khó đọc.
Dart là ngôn ngữ type safe / strong type. Có nghĩa là nó sẽ luôn kiểm tra để đảm bảo rằng giá trị bạn gán cho biến đúng với kiểu dữ liệu của biến đó. Ví dụ: bạn không thể gán một chuỗi cho một biến kiểu int. Cũng vì vậy, về lý thuyết khi khai báo một biến bạn phải luôn khai báo kiểu dữ liệu của biến đó.
Tuy nhiên, trên thực tế bạn không khai báo kiểu cũng được. Lý do là vì Dart có thể suy luận ra kiểu dữ liệu (Type inference). Trong phần tiếp theo, khi bạn khai báo biến với từ khóa var, đó là ví dụ cho việc Dart sẽ tự suy luận ra kiểu dữ liệu.
Ngoài ra, Dart còn có một loại kiểu dữ liệu là dynamic, dynamic là kiểu dữ liệu có thể
chấp nhận mọi loại dữ liệu. Bạn có thể sử dụng một biến kiểu dynamic để chứa chuỗi, số,
null
...
Các loại biến trong Dart
Trong Dart có 4 loại biến sau:
Lý do mình nhắc đến các loại biến trước khi tìm hiểu cách khai báo biến là vì có một số lưu ý liên quan đến loại biến khi khai báo biến.
Khai báo & khởi tạo
Trong Dart, để khai báo một biến ta có 2 cách như bên dưới.
1. var tênBiến = giáTrịKhởiTạo;
2. kiểuDữLiệu tênBiến = giáTrịKhởiTạo;
Lưu ý quan trọng là: bạn phải khởi tạo giá trị cho tất cả các biến non-nullable trước khi sử dụng. Nói cách khác là ngay khi bạn khai báo một biến non-nullable, bạn phải gán giá trị khởi tạo cho nó ngay. Rule này áp dụng cho tất cả các loại biến, ngoại trừ biến cục bộ.Cũng có những trường hợp mà bạn không muốn gán giá trị ngay khi khai báo, lúc đó chúng ta cần tới từ khóa late, sẽ tìm hiểu ở những đoạn tiếp theo.
1. var tênBiến = giáTrịKhởiTạo;
Với cách khai báo này, bạn không cần phải chỉ định kiểu dữ liệu của biến. Dart sẽ tự suy luận ra kiểu dữ liệu dựa vào giá trị khởi tạo. (à, từ bây giờ bạn có thể bắt đầu vọc Dartpad được rồi đấy) Ví dụ:
var name = "hla";
var age = 12;
var score = 9.6;
void main() {
print("name is => ${name.runtimeType}"); // String
print("age is => ${age.runtimeType}"); // int
print("score is => ${score.runtimeType}"); // double
}
// Bấm run để xem kết quả nhé
2. kiểuDữLiệu tênBiến = giáTrịKhởiTạo;
Với cách này bạn sẽ chỉ định rõ kiểu dữ liệu của biến (int, String, num...).
String name = "hla";
int age = 12;
double score = 9.6;
void main() {
print("name is => $name");
print("age is => $age");
print("score is => $score");
}
// Bấm run để xem kết quả nhé
Khai báo biến nullable
Như đã nhắc qua ở những đoạn trước, để khai báo một biến có thể chứa giá trị null (nullable),
bạn chỉ cần thêm dấu ?
vào sau kiểu dữ liệu khi khai báo. Đối với
biến nullable, sẽ
có một số toán tử đặc biệt khi thao tác với chúng. Bạn sẽ tìm hiểu sau khi chúng ta nói kỹ hơn
về Null safety.
String? name;
int? age;
double? score = 9.6;
void main() {
print("name is => $name");
print("age is => $age");
print("score is => $score");
score = null; // biến score là nullable nên ta có thể gán null
print("score is => $score");
}
// Bấm run để xem kết quả nhé
Với biến nullable, bạn có thể bỏ qua không cần phải gán giá trị khởi tạo cho nó, vì mặc
định nó sẽ là null.
Lưu ý quan trọng
Ý kiến cá nhân
Cá nhân mình thì mình không khuyến khích bạn khai báo biến bằng từ khóa var vì: thứ nhất nó không tường minh. Thứ 2, khi xài var kết hợp với việc không gán giá trị khởi tạo biến của bạn sẽ là biến kiểu dynamic. Hãy hạn chế dùng dynamic ở mức tối đa.
Khi một biến là dynamic, static type-checking sẽ bị disable, dẫn đến nhiều lỗi tiềm tàng. Ví dụ như trường hợp này, trình biên dịch không hề cảnh báo bạn điều gì cả, nhưng khi runtime bạn sẽ gặp lỗi vì int không có làm length.
var anything;
void main() {
anything = 18;
print("Length of anything => ${anything.length}");
}
// Bấm run để xem kết quả nhé
Từ khóa late
Đặt từ khóa late trước kiểu dữ liệu khi khai báo biến như thế này: late String name;
Từ khóa late được dùng trong 2 trường hợp:
Với trường hợp thứ 2, ví dụ bạn viết thế này late int totalAmount = calcTotalAmount();, thì hàm calcTotalAmount sẽ chỉ được gọi khi lần đầu tiên bạn sử dụng biến totalAmount chứ không phải ngay lúc bạn khai báo. Điều này có thể giúp tối ưu chương trình nếu biến totalAmount có thể sẽ không dùng tới, hoặc hàm calcTotalAmount sử dụng nhiều tài nguyên.
Giá trị mặc định
Các biến nullable thì giá trị mặc định ban đầu là null. Còn các biến non-nullable thì giá trị ban đầu là do bạn đặt. Nhắc lại quy tắc là bạn phải khởi tạo giá trị cho tất cả các biến non-nullable trước khi sử dụng.
Biến toàn cục (top level) và biến thành viên (instance) được khởi tạo trễ (lazy); việc khởi tạo chỉ được thực hiện khi lần đầu tiên bạn sử dụng biến.
Kiểu dữ liệu
Bên dưới là một số kiểu dữ liệu cơ bản mà Dart hỗ trợ, mình sẽ có một bài viết khác đi vào chi tiết từng kiểu dữ liệu sau.
Var và dynamic
Trên Stackoverflow, một câu hỏi vẫn hay được đặt lại từ 10 năm trước đến tận bây giờ là: var và dynamic khác gì nhau? Cá nhân mình nghĩ rằng việc vẫn lăn tăn giữa var và dynamic là vì không hiểu bản chất.
Đầu tiên, dynamic là một kiểu dữ liệu. Nó cũng như int, string, bool,... là một kiểu dữ liệu. Có điều nó được xem là một kiểu đặc biệt, bởi vì nó có thể chứa bất kỳ giá trị kiểu nào. Bạn có thể gán một số, một chuỗi, một danh sách... cho một biến dynamic mà không gặp lỗi gì.
var đơn giản chỉ là một từ khóa (keyword) dùng để khai báo biến. Và khi dùng var thì chúng ta không chỉ rõ kiểu dữ liệu, trình biên dịch sẽ tự xác định kiểu của biến dựa vào giá trị khởi tạo. Và nếu chúng ta không gán giá trị khởi tạo (ví dụ: var aVariable;) thì lúc này trình biên dịch không suy ra được kiểu dữ liệu của biến và nó gán biến đó là kiểu dynamic. Có thể đây là điểm mà nhiều bạn hay lăn tăn, bởi vì lúc này var aVariable; sẽ tương đương với dynamic aVariable;. Hãy thoát khỏi lối suy nghĩ này, không có chuyện var aVariable; = dynamic aVariable;, chẳng qua là complier không đủ thông tin để xác định kiểu của biến nên aVariable sẽ được gắn là kiểu dynamic mà thôi.
Hãy nhớ, var chỉ là từ khóa để khai báo biến, dynamic là một kiểu dữ liệu, hai đứa chúng nó chả liên quan gì nhau.
Const và final
Cả final và const đều được dùng để khai báo các hằng số (các biến có giá trị cố định không thay đổi trong suốt quá trình sử dụng). Tuy vậy, đó cũng là điểm chung duy nhất giữa chúng, và ngay cả trong điểm chung này cũng có chút khác biệt.
Final
Final có nghĩa là chỉ định một lần: biến hoặc trường final sau khi được gán một giá trị, giá trị đó sẽ không thể thay đổi. Cố gắng gán lại giá trị mới cho biến/trường đó sẽ gây lỗi.
Biến final, cũng như biến bình thường, bạn phải gán giá trị ngay khi khai báo (ngoại trừ biến cục bộ), nếu muốn gán sau thì bạn có thể dùng từ khóa late.
Lưu ý rằng mặc dù một object final (Lists, Maps, Classs) là không thể thay đổi nhưng các trường của nó vẫn có thể thay đổi giá trị được.
final colors = ["red", "blue", "yellow"];
void main() {
// colors = ["green", "white", "brown"]; // gán lại lỗi
colors[1] = "green"; // thay đổi field được
print("$colors");
}
// Bỏ comment dòng 4 và bấm run để xem kết quả nhé
Const
Const là hằng số lúc compile, tại thời điểm biên dịch giá trị phải được xác định. Vì vậy, bạn bắt buộc phải gán giá trị cho biến const ngay khi khai báo (không dùng late được), và tất nhiên giá trị đó sẽ không thể thay đổi. Cố gắng gán lại giá trị mới cho biến/trường đó sẽ gây lỗi.
Các biến const là "canonicalized". Với bất kỳ một "giá trị" const nào, chỉ duy nhất một đối tượng được tạo ra và tái sử dụng. Điều này giúp cải thiện hiệu năng.
class Cake {
final String type;
final double price;
const Cake({required this.type, required this.price});
}
void main() {
var banhCuaTui = const Cake(type: "Birthday", price: 100);
var banhCuaBan = const Cake(type: "Birthday", price: 100);
var banhCuaCoAy = const Cake(type: "Birthday", price: 100);
print("banhCuaTui => ${identityHashCode(banhCuaTui)}");
print("banhCuaBan => ${identityHashCode(banhCuaBan)}");
print("banhCuaCoAy => ${identityHashCode(banhCuaCoAy)}");
}
// Cả 3 cái bánh đều là 1 object, bấm Run để xem
Không giống như một object final, một object là const thì mọi thứ trong nó đều là const, bạn không thể cập nhật giá trị.
const colors = ["red", "blue", "yellow"];
void main() {
colors = ["green", "white", "brown"]; // gán lại sẽ lỗi
colors[1] = "green"; // thay đổi field cũng không được
print("$colors");
}
// Bấm run để xem kết quả nhé