Null safety Dart / Null safety Flutter là gì?
Null safety Dart / Null safety Flutter là gì? Tại sao Google lại hỗ trợ null safety
? Bắt đầu từ phiên bản 2.0 thì Dart/Flutter đã chính thức hỗ trợ tính năng an toàn null safety
.
Xem thêm🎯GIÁO TRÌNH DART/FLUTTER
1️⃣Null safety Dart / Null safety Flutter là gì?
Khi bạn chọn sử dụng null safety
, các biến trong chương trình của bạn là không thể nhận giá trị mặc định null
, có nghĩa là các biến không thể nhận giá trị null
trừ khi bạn chỉ rõ chúng có thể.
Ví dụ, trong các phiên bản trước, khi bạn khai báo biến trong Dart/Flutter như sau:
var bien_a; int so_nguyen_x; String ho_va_ten;
Thì do bạn chưa cung cấp cho các biến a
, so_nguyen_x
, ho_va_ten
một giá trị mặc định nên Dart sẽ gán giá trị mặc định của chúng là null
. Điều này có thể gây ra lỗi khi chương trình chạy, ví dụ biến so_nguyen_x
chưa được cung cấp giá trị nào mà bạn lại đem đi tính toán!
Do đó, từ phiên bản Dart/Flutter 2.0 trở đi, bạn bắt buộc phải cung cấp một giá trị mặc định trước khi sử dụng biến đó (thường là ngay khi khai báo biến).
Trong trường hợp bạn chưa muốn cung cấp giá trị cho biến ngay, hoặc biến của bạn có thể chấp nhận giá trị null
thì bạn phải thêm kí tự ?
vào sau kiểu của biến khi khai báo hoặc gán giá trị mặc định cho biến là null
. Ví dụ, đoạn chương trình trên phải sửa thành:
var bien_a = null; int? so_nguyen_x; String? ho_va_ten;
Với việc hỗ trợ null safety
thì các lỗi null-dereference
trong thời gian chạy của bạn sẽ chuyển thành lỗi phân tích cú pháp ngay trong khi bạn gõ code, do đó giúp việc debug chương trình trở nên thuận tiện hơn.
Bạn không nhất thiết phải cung cấp giá trị cho biến ngay, mà chỉ cần cung cấp trước khi biến đó được sử dụng. Ví dụ, đoạn mã sau là hoàn toàn hợp lệ.
void main() { int x; print('Blah blah blah...'); x = 10; print(x); }
Như vậy, cách để sửa lỗi
null safety
của các mã nguồn cũ (phiên bản Dart 1.x) chỉ đơn giản là thêm kí tự?
vào sau kiểu của biến khi khai báo nếu biến đó có thể nhận giá trịnull
(dĩ nhiên sau đó, trước khi sử dụng đến biến đó bạn phải cung cấp giá trị cho nó); hoặc gán cho biến một giá trị mặc định nếu biến đó không thể nhận giá trịnull
. Bạn cũng có thể tham khảo chi tiết ở bài viết https://dart.dev/null-safety/migration-guide
Trước phiên bản 2.0, Dart là ngôn ngữ nullability
. Theo đó, null
sẽ là kiểu con (subtype) của mọi kiểu. Tức là tất cả các kiểu số nguyên int, số thực double, danh sách List… đều chấp nhận giá trị null
.
Tuy nhiên, từ phiên bản 2.0 thì Dart là ngôn ngữ hỗ trợ null safety
. Theo đó, kiểu Null đã tách ra, không còn là kiểu con của các kiểu khác nữa. Do đó, khi bạn khai báo một biến là non-nullable (ví dụ kiểu int, kiểu String) mà lại gán cho chúng giá trị null thì chương trình sẽ báo lỗi.
2️⃣Nguyên tắc thiết kế của Null safety Dart/Flutter
Null safety Dart (Null safety Flutter) dựa trên 3 nguyên tắc thiết kế cốt lõi sau:
- Mặc định khi khai báo các biến là non-nullable (không thể nhận giá trị
null
), trừ khi bạn chỉ rõ rằng một biến có thể lànull
. Mặc định này được chọn sau khi Google nghiên cứu phát hiện ra rằngnon-null
là lựa chọn phổ biến nhất trong các API. - Incrementally adoptable. Bạn quyết định những phần nào của các dự án hiện tại để chuyển sang
null safety
vào bất cứ khi nào. Bạn có thể di chuyển từng bước, sử dụng đồng thời các mãnull- safe
và khôngnull- safe
trong cùng một dự án. Google cung cấp các công cụ để giúp bạn di chuyển (migration). - Fully sound. Dart cho phép tối ưu hóa trình biên dịch . Nếu hệ thống kiểu dữ liệu của Dart xác định rằng một cái gì đó không phải là
null
, thì cái đó không bao giờ có thể lànull
. Một khi bạn di chuyển toàn bộ dự án của mình và các thành phần phụ thuộc của nó sangnull safety
, bạn sẽ thu được toàn bộ lợi ích của sự ổn định – không chỉ ít lỗi hơn mà còn có các tệp nhị phân nhỏ hơn và việc thực thi dự án của bạn sẽ nhanh hơn.
3️⃣Kí tự !
Bạn sử dụng kí tự !
ở phía sau một biểu thức để thực thi biểu thức đó và chuyển (cast) giá trị nhận được sang kiểu non-nullable
// Using null safety, incorrectly: class HttpResponse { final int code; final String? error; HttpResponse.ok() : code = 200; HttpResponse.notFound() : code = 404, error = 'Not found'; String toString() { if (code == 200) return 'OK'; return 'ERROR ${error!.toUpperCase()}'; } }
4️⃣Late variables (khai báo biến trễ, biến muộn)
Đôi khi các biến – các trường trong một lớp class
hoặc các biến cấp cao nhất – phải là kiểu non-nullable
, nhưng chúng không thể được gán giá trị ngay lập tức. Đối với những trường hợp như vậy, hãy sử dụng từ khóa late
.
class MonAn {
late String ten;
void setTen(String str) {
ten = str;
}
}
void main() {
final buaSang = MonAn();
buaSang.setTen('Chim To Dần');
print(buaSang.ten);
}
Trong ví dụ trên, biến ten
rõ ràng là kiểu non-nullable
nhưng chúng ta không thể gán giá trị mặc định, vì class MonAn
này chưa có đối tượng nào được tạo ra. Khi chúng ta tạo ra đối tượng buaSang
thuộc class MonAn
thì chúng ta mới biết (và cần) phải cung cấp giá trị cho trường ten
trước khi sử dụng nó.
Khi bạn sử dụng late
trước một khai báo biến, điều đó sẽ giúp cho Dart biết rằng:
- Biến này không cần kiểm tra phải có giá trị mặc định ngay.
- Bạn sẽ gán cho nó một giá trị sau đó.
- Bạn sẽ đảm bảo rằng biến có một giá trị trước khi biến được sử dụng.
Nếu bạn khai báo một biến late
và biến đó được sử dụng trước khi nó được gán giá trị, thì Dart vẫn sẽ báo lỗi.
Ví dụ trên, nhiều bạn có thể sẽ sửa lại như sau:
class MonAn {
String? ten;
void setTen(String str) {
ten = str;
}
}
void main() {
final buaSang = MonAn();
buaSang.setTen('Chim To Dần');
print(buaSang.ten);
}
Chương trình vẫn hoạt động! Nhưng sẽ gây bối rối cho việc khởi tạo lớp MonAn
bởi vì trường ten
rõ ràng là sẽ không bao giờ được nhận giá trị null
nên việc sử dụng từ khóa ?
để chấp nhận cho nó có thể nhận giá trị null
sẽ tiềm ẩn những lỗi logic sau này.
Việc sử dụng từ khóa late này còn được gọi là trì hoãn khởi tạo hoặc khởi tạo muộn, khởi tạo trễ (lazy initialization)
5️⃣Late final variables
Bạn có thể kết hợp từ khóalate
với final
:
// Using null safety:
class Coffee {
late final String _temperature;
void heat() {
_temperature = 'hot';
}
void chill() {
_temperature = 'iced';
}
String serve() => _temperature + ' coffee';
}
Không giống như cách khai báo biến/trường final
thông thường, bạn không phải khởi tạo giá trị cho trường/biến đó trong phần mô tả hoặc phần xây dựng lớp (constructor). Bạn có thể gán giá trị cho nó trong lúc chương trình chạy. Nhưng vì sử dụng từ khóa final
, nên bạn chỉ được phép gán một lần!
Required named parameters
To guarantee that you never see a null
parameter with a non-nullable type, the type checker requires all optional parameters to either have a nullable type or a default value. What if you want to have a named parameter with a nullable type and no default value? That would imply that you want to require the caller to always pass it. In other words, you want a parameter that is named but not optional.
I visualize the various kinds of Dart parameters with this table:
mandatory optional
+------------+------------+
positional | f(int x) | f([int x]) |
+------------+------------+
named | ??? | f({int x}) |
+------------+------------+
For unclear reasons, Dart has long supported three corners of this table but left the combination of named+mandatory empty. With null safety, we filled that in. You declare a required named parameter by placing required
before the parameter:
// Using null safety:
function({int? a, required int? b, int? c, required int? d}) {}
Here, all the parameters must be passed by name. The parameters a
and c
are optional and can be omitted. The parameters b
and d
are required and must be passed. Note that required-ness is independent of nullability. You can have required named parameters of nullable types, and optional named parameters of non-nullable types (if they have a default value).
This is another one of those features that I think makes Dart better regardless of null safety. It simply makes the language feel more complete to me