[Dart&Flutter][Bài4]Biến và kiểu dữ liệu trong Dart (P.3)

Tiếp tục series về biến và kiểu dữ liệu trong Dart, hôm nay mình sẽ nói về kiểu Nullable, Non-nullable và các toán tử.

3. Kiểu Nullable và Non-nullable:

Bắt đầu từ Dart 2.10, giá trị mặc định là không thể null (Non-nullable), có nghĩa là chúng không được phép có giá trị null.

// tiếp cận một biến trước khi nó được gán sẽ gây lên lỗi biên dịch

int value;
print("$value");// sai quy tắc, không thể biên dịch

Nếu bạn không khởi tạo một biến, nó tự động được gán giá trị null nhưng điều này sẽ gây ra lỗi bởi vì mặc định của Dart là không thể null. Để có thể biên dịch  thành công, bạn phải khởi tạo biến khi được khai báo hoặc gán giá trị trước khi biến ấy được dùng.

//1
int value = 0;
print("$value");


//2
int value;
value = 0;
print("$value");

Nó sẽ báo lỗi khi bạn dùng như ở bên dưới do giá trị được gán sau khi được tiếp cận.

int value;
print("$value");
value = 0;

Thuộc tính không thể null của biến rất mạnh mẽ vì nó thêm một mức độ an toàn của kiểu  cho ngôn ngữ , do đó, khả năng nhà phát triển gặp phải ngoại lệ thời gian chạy (runtime exceptions) liên quan đến null thấp hơn. Ví dụ: bạn không cần phải làm điều này:

String name = "Alberto";
void main() {
	if (name != null) {
	print(name);
	}
}

Trình biên dịch sẽ đảm bảo rằng nó không thể null và do đó không cần kiểm tra null. Tóm lại, chúng ta cần nhớ đến một số lưu ý sau:

  • Theo mặc định, các biến không được null và chúng phải luôn được khởi tạo trước khi dùng. Sẽ tốt hơn nếu bạn khởi tạo chúng ngay lập tức, nhưng bạn cũng có thể làm điều đó trước khi chúng được sử dụng.
  • Không kiểm tra null đối với các biến không thể null “chuẩn” vì nó vô ích.

Cũng nói thêm, kể từ bản Flutter 2.5 được ra mắt cùng với Sound Null Safety, thì khi bạn khai báo một biến tường minh kiểu giá trị nguyên thuỷ như int, double... thì sẽ luôn yêu cầu lập trình phải gán cho nó một giá trị ngay khi khởi tạo. Để lách luật và muốn gán giá trị trước khi dùng biến, bạn có thể khai báo biến đó với var. Nhưng nhớ, bản phải luôn đảm bảo biến này khác null trước khi được dùng.

Trong Dart, bạn cũng có thể khai báo kiểu có thể null. Kiểu dữ liệu này không yêu cầu khởi tạo trước khi dùng và cho phép biến có thể null.

int? value;
print("$value"); // hợp lệ, nó in ra 'null'

Nếu bạn thêm một dấu chấm hỏi ở cuối kiểu dữ liệu, bạn sẽ có một kiểu có thể null. Để đảm bảo an toàn, chúng sẽ yêu cầu kiểm tra null thủ công để tránh các trường hợp ngoại lệ không mong muốn. Tuy vậy, trong hầu hết các trường hợp, sử dụng biến không thể null thì tốt hơn nhiều.

Các kiểu có thể null hỗ trợ toán tử chỉ mục [] cần được gọi bằng cú pháp ?[]. Null được trả về nếu biến cũng null.

String? name = "Anh";
String? first = name?[0]; // first = 'A';

String? name;
String? first = name?[0]; // first = 'null';

Dù sao thì các bạn nên chú ý rằng, tốt nhất vẫn nên dùng theo mặc định là biến không thể null, nó an toàn hơn cho việc code. Tránh hoặc hạn chế dùng các biến không thể null, trong những tình huống bắt buộc. Cuối cùng nhưng không kém phần quan trọng, cách chuyển đổi duy nhất có thể có giữa giá trị không thể null và có thể null:

  • Khi bạn chắc chắn rằng giá trị trả về không null, bạn có thể thêm ! ở cuối để chuyển nó sang dạng không thể null:
int? nullable = 0;
int notNullable = nullable!;

Toán tử ! (toán tử bang) chuyển giá trị có thể null (int?) thành không thể null (int) của cùng kiểu dữ liệu. Một ngoại lệ (exception) được ném ra nếu giá trị có thể null thực sự null:

int? nullable;

// một ngoại được trả ra
int notNullable = nullable!;
  • Nếu bạn cần ép kiểu giá trị null thành kiểu con (subtype) không thể null, dùng toán tử as:
num? value = 5;

int otherValue = value as int;

Bạn sẽ không thể thực hiện int otherValue = value! vì toán tử ! hoạt động chỉ khi cùng một kiểu dữ liệu. Trong ví dụ này, chúng ta có một num và một int vì vậy chúng ta cần chuyển kiểu (cast) nó với as.

  • Toán tử ?? có thể được dùng để tạo ra biến không thể null từ biến có thể null.
int? nullable = 10;
int nonNullable = nullable ?? 0;

Nếu biến nullable khác null, nó sẽ gán giá trị vào biến nonNullable. Nếu nó null, sẽ gán nonNullable = 0.

Hãy nhớ rằng khi bạn đang làm việc với các giá trị có thể null, toán tử (.) không có sẵn. Thay vào đó, bạn phải sử dụng (?.).

double? pi = 3.14;

final round1 = pi.round(); // không
final round2 = pi?.round(); // Ok

4. Các toán tử:

4.1. Toán tử số học:

Các toán tử số học thường được sử dụng trên int và double để xây dựng các biểu thức. Như  các bạn đã biết biết, toán tử + cũng có thể được sử dụng để nối các chuỗi.

Việc tăng hoặc giảm tiền tố và hậu tố hoạt động như các bạn thường thấy trong nhiều ngôn ngữ.

int a = 10;
++a; // a = 11
a++; // a = 12

int b = 5;
--b; // b = 4;
b--; // b = 3;

int c = 6;
c += 6 // c = 12

Mặc dù, cả tăng / giảm tiền tố hậu tố và tiền tố đều có cùng kết quả nhưng chúng hoạt động theo một cách khác.

Cụ thể:

  • Trong phiên bản tiền tố (++ x), giá trị được tăng đầu tiên và sau đó được "trả về";
  • Trong phiên bản hậu tố (x ++), giá trị đầu tiên được "trả lại" và sau đó tăng dần

4.2. Toán tử quan hệ:

Kiểm tra sự bằng nhau của hai đối tượng a và b luôn dùng với toán tử == bởi vì, không giống như trong Java, không có phương thức nào equal (). Nói chung, đây là cách == works:  

+  Nếu a hoặc b là null, trả về true nếu cả hai là null hoặc false nếu chỉ có một là null. Ngược lại

+ trả về kết quả là == theo logic mà bạn đã xác định trong phương thức ghi đè (method override). Tất nhiên, == chỉ hoạt động với các đối tượng cùng kiểu.

4.3. Toán tử kiểm tra kiểu:

Giả sử bạn đã xác định một loại mới như class Fruit {}. Bạn có thể truyền một đối tượng tới Fruit bằng cách sử dụng toán tử as như thế này:

Code sẽ biên dịch nhưng nó không an toàn: nếu nho là rỗng hoặc nếu nó không phải là Trái cây, bạn sẽ nhận được ngoại lệ. Nó luôn luôn là cần thiết để kiểm tra xem quá trình ép kiểu (cast) có khả thi hay không trước khi thực hiện:

if (grapes is Fruit) {
	(grapes as Fruit).color = "Green";
}

Bây giờ, bạn được đảm bảo rằng quá trình ép kiểu (cast) sẽ chỉ xảy ra nếu nó có thể và không có ngoại lệ thời gian chạy nào xảy ra. Trên thực tế, trình biên dịch đủ thông minh để hiểu rằng bạn đang thực hiện kiểm tra kiểu với is và nó có thể thực hiện smart cast.

if (grapes is Fruit) {
	grapes.color = "Green";
}

Bạn có thể tránh viết cast rõ ràng (grapes as Fruit) bởi vì, bên trong phạm vi của điều kiện, grapes  tự động ép kiểu (cast) thành Fruit.

4.4. Toán tử logic:

Khi bạn phải tạo các biểu thức điều kiện phức tạp, bạn có thể sử dụng các toán tử logic:

4.5. Toán tử Bitwise và shift:

Bạn sẽ không bao giờ sử dụng các toán tử này trừ khi bạn đang thực hiện một số thao tác dữ liệu cấp thấp nhưng trong Flutter điều này không bao giờ xảy ra.