- Null Safety là gì?
Null Safety là một cơ chế an toàn kiểu dữ liệu được Dart sử dụng để hạn chế các lỗi liên quan đến giá trị null trong chương trình. Null Safety được đưa vào Dart nhằm giúp lập trình viên xác định rõ một biến có thể chứa null hay không ngay từ khi viết chương trình.
Trong Dart, nguyên tắc mặc định là Non-nullable by default, nghĩa là một biến khi được khai báo sẽ không được phép chứa giá trị null, trừ khi lập trình viên khai báo rõ rằng biến đó có thể null.
Ví dụ, String và String? là hai kiểu có ý nghĩa khác nhau:
String name = "Thương";
String? nickname;
String là kiểu non-nullable, còn String? là kiểu nullable.
Điều này giúp Dart có thể kiểm tra nhiều trường hợp lỗi ngay trong quá trình phân tích code thay vì đợi đến khi chương trình chạy mới phát hiện lỗi.
Có thể hiểu đơn giản rằng Null Safety đặt ra một nguyên tắc:
Nếu một biến không được phép null thì Dart sẽ cố gắng đảm bảo biến đó không nhận null. Nếu một biến có thể null thì lập trình viên phải xử lý khả năng đó trước khi sử dụng.
- Tại sao Dart cần Null Safety?
null là một trạng thái rất phổ biến trong lập trình. Nó thường biểu thị rằng một giá trị chưa tồn tại, chưa được cung cấp, không tìm thấy hoặc không có dữ liệu.
Ví dụ trong một ứng dụng Flutter, dữ liệu lấy từ server có thể không đầy đủ. Một sản phẩm có thể không có mô tả, một người dùng có thể chưa có ảnh đại diện hoặc một API có thể không trả về dữ liệu như dự kiến.
Nếu chương trình không kiểm soát những trường hợp này, việc truy cập vào một giá trị null có thể khiến ứng dụng xảy ra lỗi trong quá trình chạy.
Trước khi có Null Safety, Dart cho phép nhiều biến có thể nhận null mà không cần khai báo rõ ràng. Điều này khiến lập trình viên khó biết được một biến có thể null hay không.
Một đoạn code có thể trông hoàn toàn hợp lệ nhưng đến lúc chạy mới phát sinh lỗi kiểu:
The method was called on null
Đây là một trong những nguyên nhân gây crash ứng dụng.
Null Safety giải quyết vấn đề này bằng cách đưa việc kiểm tra null vào hệ thống kiểu dữ liệu của Dart. Nhờ đó, nhiều lỗi có thể được phát hiện sớm hơn trong quá trình phát triển.
- Non-nullable và Nullable
Đây là khái niệm quan trọng nhất của Null Safety.
Non-nullable
Một kiểu dữ liệu thông thường trong Dart mặc định là non-nullable.
Ví dụ:
String name = "Thương";
Biến name phải chứa một giá trị String hợp lệ và không được phép chứa null.
Nullable
Nếu một biến có khả năng không có giá trị, lập trình viên có thể thêm dấu ? phía sau kiểu dữ liệu.
String? name;
Khi đó name có thể chứa một chuỗi hoặc null.
Cách khai báo này giúp thể hiện rõ ý định của lập trình viên đối với dữ liệu.
Ví dụ:
String→ chắc chắn phải có chuỗi.String?→ có thể có chuỗi hoặc không có giá trị.User→ phải có đối tượng User.User?→ có thể có đối tượng User hoặc null.
Đây là một trong những điều đầu tiên cần hiểu khi học Dart hiện đại.
- Null Checking
Khi một biến được khai báo nullable, Dart không thể mặc định giả định rằng biến đó đang chứa dữ liệu.
Ví dụ:
int? age;
Dart biết rằng age có thể là null.
Nếu một đoạn code khác yêu cầu một giá trị int chắc chắn không null, Dart sẽ không cho phép sử dụng trực tiếp age.
Lập trình viên phải xác định rằng giá trị đó thực sự tồn tại trước khi sử dụng.
Một cách phổ biến là kiểm tra:
if (age != null) {
// xử lý age
}
Sau khi kiểm tra, Dart có thể hiểu rằng trong phạm vi phù hợp, giá trị đó không còn là null.
Việc kiểm tra null rất quan trọng khi làm việc với dữ liệu đến từ bên ngoài ứng dụng, chẳng hạn như API, database, form hoặc dữ liệu người dùng.
- Toán tử ?
Dấu ? có vai trò quan trọng trong Null Safety.
Khi đặt ? sau kiểu dữ liệu, nó biến kiểu dữ liệu đó thành nullable.
Ví dụ:
String?
int?
double?
User?
Điều này có nghĩa là các kiểu trên có thể chứa null.
Dấu ? không có nghĩa là biến chắc chắn null, mà chỉ cho biết rằng biến được phép có giá trị null.
Ví dụ một biến String? có thể có giá trị “Thương” ở thời điểm này nhưng vẫn có khả năng trở thành null ở thời điểm khác.
- Toán tử !
! được gọi là null assertion operator.
Toán tử này được sử dụng khi lập trình viên tin chắc rằng một giá trị nullable tại thời điểm sử dụng không phải null.
Ví dụ:
String? name;
String value = name!;
Ở đây, ! nói với Dart rằng: Giá trị này chắc chắn không null.
Tuy nhiên, đây là một cơ chế cần sử dụng cẩn thận.
Nếu lập trình viên sử dụng ! nhưng giá trị thực tế lại là null, chương trình có thể xảy ra lỗi trong runtime.
Vì vậy, ! không phải là cách để “né” Null Safety. Nó chỉ nên được sử dụng khi lập trình viên thực sự có cơ sở chắc chắn rằng giá trị không thể null tại thời điểm đó.
Trong dự án thực tế, việc lạm dụng ! thường khiến code khó an toàn hơn.
- Toán tử ?.
?. được sử dụng để truy cập một thuộc tính hoặc gọi một phương thức khi đối tượng có khả năng null.
Thay vì truy cập trực tiếp và có nguy cơ xảy ra lỗi, Dart sẽ kiểm tra đối tượng trước.
Ví dụ:
user?.name
Nếu user tồn tại, Dart lấy name.
Nếu user là null, biểu thức sẽ trả về null thay vì cố gắng truy cập vào một đối tượng không tồn tại.
Toán tử này đặc biệt hữu ích khi làm việc với các đối tượng lấy từ API hoặc database, vì những dữ liệu này có thể không tồn tại.
- Toán tử ??
?? được gọi là null-aware operator dùng để cung cấp một giá trị thay thế khi giá trị ban đầu là null.
Ví dụ:
String displayName = name ?? "Chưa có tên";
Nếu name có giá trị thì sử dụng name.
Nếu name là null thì sử dụng “Chưa có tên”.
Toán tử ?? thường được sử dụng khi giao diện cần hiển thị một giá trị mặc định trong trường hợp dữ liệu không tồn tại.
Trong Flutter, đây là toán tử xuất hiện khá thường xuyên khi xử lý dữ liệu từ API hoặc Model.
- Toán tử ??=
??= có chức năng gần giống ??, nhưng được sử dụng để gán giá trị.
Nó chỉ thực hiện việc gán khi biến hiện tại đang là null.
Ví dụ:
name ??= "Unknown";
Nếu name đã có giá trị thì giá trị đó được giữ nguyên.
Nếu name đang là null thì “Unknown” sẽ được gán cho biến.
Toán tử này thường được sử dụng khi muốn đảm bảo một biến có giá trị mặc định mà không muốn ghi đè giá trị đã tồn tại.
- Required trong Null Safety
Null Safety không chỉ áp dụng cho biến mà còn liên quan đến constructor và tham số của hàm.
Trong Dart, từ khóa required được sử dụng để yêu cầu một tham số phải được truyền vào khi gọi hàm hoặc tạo đối tượng.
Ví dụ:
class User {
final String name;
User({required this.name});
}
Ở đây, name là non-nullable nhưng không được khởi tạo ngay tại khai báo. Vì vậy constructor yêu cầu người sử dụng phải cung cấp name.
required giúp đảm bảo rằng những dữ liệu bắt buộc sẽ được cung cấp ngay từ đầu.
Điều này rất thường gặp trong Flutter khi tạo Widget.
Ví dụ một Widget có thể yêu cầu title, child, onPressed hoặc một số dữ liệu khác phải được truyền vào.
- Default Value
Bên cạnh required, Dart cũng hỗ trợ giá trị mặc định cho những tham số không bắt buộc.
Giá trị mặc định có ý nghĩa là nếu người dùng không cung cấp dữ liệu, chương trình sẽ sử dụng một giá trị được xác định trước.
Ví dụ:
class User {
final String name;
final String role;
User({
required this.name,
this.role = "User",
});
}
Trong trường hợp này:
namelà bắt buộc.rolekhông bắt buộc.- Nếu không truyền
role, giá trị “User” sẽ được sử dụng.
Việc kết hợp required và default value giúp thiết kế class, function và Widget rõ ràng hơn.
- Từ khóa late
late được sử dụng để khai báo một biến non-nullable nhưng cho phép trì hoãn việc khởi tạo.
Thông thường, một biến non-nullable phải được khởi tạo trước khi sử dụng. Tuy nhiên, có những trường hợp lập trình viên chưa thể khởi tạo biến ngay tại thời điểm khai báo.
Khi đó có thể sử dụng late.
Ví dụ:
late String author;
Biến author chưa có giá trị tại thời điểm khai báo nhưng Dart cho phép điều này vì lập trình viên cam kết rằng nó sẽ được khởi tạo trước khi sử dụng.
Điểm quan trọng cần nhớ là:
late không có nghĩa là biến có thể null.
late chỉ có nghĩa là việc khởi tạo được trì hoãn.
Nếu biến late được sử dụng trước khi được gán giá trị, chương trình sẽ xảy ra lỗi runtime.
Vì vậy, khi sử dụng late, lập trình viên phải chịu trách nhiệm đảm bảo biến được khởi tạo đúng thời điểm.
- late final
late final là sự kết hợp giữa hai cơ chế late và final.
late cho phép trì hoãn việc khởi tạo.
final đảm bảo rằng sau khi được khởi tạo, biến không thể được gán lại.
Vì vậy:
late final
→ chưa cần khởi tạo ngay
→ được khởi tạo sau
→ chỉ được gán một lần
late final thường phù hợp với những đối tượng cần được khởi tạo sau khi class hoặc Widget đã được tạo nhưng không cần thay đổi trong suốt vòng đời của đối tượng.
Trong Flutter, late final có thể xuất hiện khi khởi tạo controller, service hoặc một số đối tượng phụ thuộc vào lifecycle của Widget.
- Lazy Initialization
late cũng có thể được sử dụng để thực hiện lazy initialization.
Lazy initialization nghĩa là một đối tượng chỉ được khởi tạo khi nó thực sự được sử dụng lần đầu tiên.
Điều này có thể giúp tránh việc khởi tạo những đối tượng tốn tài nguyên nhưng chưa cần dùng ngay.
Ví dụ:
late final service = SomeService();
Thay vì tạo SomeService ngay khi object được tạo, việc khởi tạo có thể được trì hoãn đến lúc service thực sự được truy cập.
Cơ chế này hữu ích trong những trường hợp đối tượng có quá trình khởi tạo tốn tài nguyên hoặc phụ thuộc vào một điều kiện nào đó.
- Null Safety và var
var là từ khóa cho phép Dart tự suy luận kiểu dữ liệu dựa trên giá trị ban đầu.
Ví dụ:
var name = "Thương";
Dart có thể suy luận rằng name là String.
Điều quan trọng là var không đồng nghĩa với dynamic.
Nếu Dart đã suy luận được kiểu cụ thể, biến vẫn chịu sự kiểm tra kiểu và Null Safety bình thường.
Do đó, var vẫn có thể được sử dụng an toàn trong Dart khi kiểu dữ liệu có thể được suy luận rõ ràng.
- Null Safety và dynamic
dynamic khác với var.
Khi sử dụng dynamic, Dart cho phép một biến chứa nhiều kiểu dữ liệu khác nhau và giảm bớt việc kiểm tra kiểu tại thời điểm biên dịch.
Điều này làm cho dynamic kém an toàn hơn so với việc sử dụng kiểu dữ liệu cụ thể.
Ví dụ:
dynamic value;
Biến value có thể chứa nhiều kiểu dữ liệu khác nhau và cũng có thể chứa null.
Trong các dự án Flutter, không nên lạm dụng dynamic. Khi có thể xác định kiểu dữ liệu, nên khai báo kiểu cụ thể để tận dụng khả năng kiểm tra của Dart.
dynamic thường xuất hiện trong một số tình huống đặc biệt, chẳng hạn khi xử lý dữ liệu chưa xác định rõ kiểu hoặc một số dữ liệu JSON.
- Null Safety khi làm việc với API
Đây là một trong những nơi Null Safety có vai trò rất lớn trong dự án Flutter.
Khi ứng dụng gọi API, dữ liệu nhận về thường không thể được đảm bảo tuyệt đối rằng tất cả trường đều tồn tại.
Ví dụ một sản phẩm có thể có:
- id
- name
- price
- description
- image
Trong đó id, name và price có thể là dữ liệu bắt buộc, trong khi description hoặc image có thể không được server trả về.
Khi xây dựng Model, lập trình viên cần xác định rõ những trường nào nullable.
Ví dụ:
class Product {
final int id;
final String name;
final String? description;
}
Ở đây description được khai báo nullable vì sản phẩm có thể không có mô tả.
Việc xác định nullable ngay từ Model giúp các tầng phía sau của ứng dụng biết rõ dữ liệu nào có thể thiếu và phải xử lý như thế nào.
- Null Safety trong Flutter
Trong Flutter, Null Safety xuất hiện ở rất nhiều vị trí.
Một Widget có thể nhận những tham số bắt buộc và những tham số tùy chọn.
Một Model có thể chứa những trường bắt buộc và những trường không bắt buộc.
Một API có thể trả về dữ liệu hoặc không trả về dữ liệu.
Một database có thể chứa các cột cho phép NULL.
Một State có thể chưa có dữ liệu ở thời điểm đầu tiên và chỉ nhận dữ liệu sau khi quá trình xử lý bất đồng bộ hoàn thành.
Do đó, khi làm Flutter, lập trình viên sẽ thường xuyên gặp các kiểu như:
String?
User?
Product?
List<Product>?
Việc đọc hiểu các kiểu này là điều rất quan trọng.
- Null Safety giúp ích gì trong dự án thực tế?
Null Safety mang lại nhiều lợi ích cho quá trình phát triển phần mềm:
Giảm lỗi runtime: Một số lỗi liên quan đến null có thể được phát hiện ngay trong quá trình viết code.
Code rõ ràng hơn: Khi nhìn vào String?, lập trình viên biết ngay rằng giá trị này có khả năng null. Ngược lại, String thể hiện rằng giá trị này phải tồn tại.
Dễ bảo trì: Khi dự án lớn và có nhiều thành viên, việc xác định rõ nullable và non-nullable giúp các thành viên hiểu cách sử dụng dữ liệu mà không cần đoán.
Hỗ trợ IDE: Các IDE như VS Code có thể dựa vào hệ thống kiểu của Dart để cảnh báo những trường hợp sử dụng dữ liệu không an toàn.
Hạn chế crash: Việc kiểm soát null ngay từ đầu giúp giảm những lỗi xảy ra trong quá trình ứng dụng chạy thực tế.
- Những lỗi thường gặp khi sử dụng Null Safety
Người mới học Dart thường gặp một số lỗi sau:
Lạm dụng: Khi gặp lỗi nullable, nhiều người có xu hướng thêm ! để loại bỏ cảnh báo. Tuy nhiên, nếu dữ liệu thực tế là null, ứng dụng vẫn có thể crash. Vì vậy, cần hiểu nguyên nhân của null thay vì chỉ sử dụng ! để ép kiểu.
Khai báo nullable không cần thiết: Không phải biến nào cũng cần thêm ?. Nếu một dữ liệu bắt buộc phải tồn tại thì nên sử dụng kiểu non-nullable. Việc khai báo mọi thứ thành nullable sẽ làm code khó kiểm soát hơn.
Sử dụng late tùy tiện:late giúp trì hoãn việc khởi tạo nhưng cũng tạo ra trách nhiệm cho lập trình viên. Nếu quên khởi tạo biến trước khi sử dụng, lỗi sẽ xảy ra khi chương trình chạy.
Sử dụng dynamic quá nhiều:dynamic làm giảm khả năng kiểm tra kiểu của Dart và khiến một số lỗi chỉ xuất hiện khi runtime.
- Cách tư duy khi gặp một biến nullable
Khi gặp một biến có dấu ?, không nên chỉ hiểu rằng: “Biến này có thể null.”
Mà nên đặt thêm câu hỏi: Tại sao nó có thể null?
- Dữ liệu chưa được tải?
- API không trả về?
- Người dùng chưa nhập?
- Database không có dữ liệu?
- Đối tượng chưa được khởi tạo?
- Đây là một tham số tùy chọn?
Sau đó mới lựa chọn cách xử lý phù hợp.
Có thể kiểm tra bằng if, sử dụng ?., cung cấp giá trị mặc định bằng ??, hoặc trong trường hợp thực sự chắc chắn thì sử dụng !.
Đây mới là cách sử dụng Null Safety đúng trong dự án thực tế.
- Các thành phần cần nhớ
Khi học Null Safety, không cần học thuộc quá nhiều đoạn code. Quan trọng nhất là hiểu chức năng của những thành phần sau:
? → khai báo kiểu có thể null
! → khẳng định giá trị không null
?. → truy cập an toàn khi có thể null
?? → sử dụng giá trị thay thế khi null
??= → gán giá trị nếu biến đang null
required → tham số bắt buộc
late → trì hoãn khởi tạo
late final → trì hoãn khởi tạo và chỉ cho phép gán một lần
- Null Safety và mối liên hệ với các kiến thức Dart khác
Null Safety không phải là một kiến thức độc lập hoàn toàn mà liên quan đến nhiều phần khác của Dart.
Khi học Dart OOP, Null Safety xuất hiện trong class, thuộc tính, constructor và object.
Khi học Future và Async/Await, Null Safety xuất hiện khi dữ liệu bất đồng bộ chưa có hoặc có thể không trả về.
Khi học REST API và JSON, Null Safety rất quan trọng vì dữ liệu từ server có thể thiếu hoặc chứa null.
Khi học Flutter Widget, Null Safety xuất hiện trong constructor, parameter, State và dữ liệu truyền giữa các Widget.
Khi học State Management, Null Safety được sử dụng để quản lý trạng thái chưa có dữ liệu, đang tải dữ liệu hoặc dữ liệu không tồn tại.
Vì vậy, đây là một kiến thức nền tảng và sẽ được sử dụng xuyên suốt quá trình làm Flutter.
- Kết luận
Null Safety là cơ chế của Dart giúp kiểm soát giá trị null thông qua hệ thống kiểu dữ liệu. Dart mặc định sử dụng nguyên tắc Non-nullable by default, nghĩa là các kiểu dữ liệu thông thường không được phép nhận null. Nếu một biến có khả năng không có giá trị, lập trình viên phải khai báo nó là nullable bằng cách sử dụng dấu ?.
Null Safety giúp chương trình xác định rõ dữ liệu nào bắt buộc phải tồn tại và dữ liệu nào có thể không tồn tại. Nhờ đó, nhiều lỗi liên quan đến null có thể được phát hiện sớm trong quá trình phát triển thay vì chỉ xuất hiện khi ứng dụng đang chạy.
Trong quá trình sử dụng Null Safety, các thành phần quan trọng cần nắm gồm ?, !, ?., ??, ??=, required, late và late final. Tuy nhiên, điều quan trọng nhất không phải là học thuộc các ký hiệu này mà là hiểu khi nào dữ liệu có thể null, nguyên nhân tại sao nó null và cách xử lý phù hợp.
Đối với Flutter, Null Safety đặc biệt quan trọng vì ứng dụng thường xuyên làm việc với API, JSON, database, form, state và các đối tượng có vòng đời khác nhau. Hiểu tốt Null Safety sẽ giúp code Flutter an toàn hơn, dễ đọc hơn và giảm đáng kể các lỗi xảy ra trong quá trình chạy ứng dụng.





Khoá học lập trình game con rắn cho trẻ em




