[Dart] Sound Null Safety
1. Sound Null Safety trong Dart là gì?
Sound null safety là một tính năng của Dart giúp ngăn chặn lỗi null reference bằng cách yêu cầu bạn xác định rõ ràng liệu một biến có thể chứa giá trị null hay không.
- Sound: Hệ thống kiểm tra null là “sound” (chắc chắn) nghĩa là nếu mã của bạn đã biên dịch mà không có lỗi null safety, bạn chắc chắn không gặp lỗi null runtime.
- Null safety: Dart phân biệt hai loại biến:
- Nullable type (có thể null): Được khai báo với dấu
?
. Ví dụ:String?
. - Non-nullable type (không thể null): Không cần dấu
?
. Ví dụ:String
.
- Nullable type (có thể null): Được khai báo với dấu
Tính năng này có mặt từ Dart 2.12 và được tích hợp trong Flutter để giúp mã an toàn hơn và giảm lỗi runtime.
2. Lợi ích của Sound Null Safety
- Ngăn chặn lỗi runtime: Giảm nguy cơ gặp lỗi null tại runtime.
- Biên dịch hiệu quả hơn: Dart có thể tối ưu mã khi biết chắc chắn biến không bao giờ null.
- Cải thiện độ tin cậy và dễ đọc: Bạn buộc phải giải quyết các trường hợp có thể null trong lúc code.
3. Ví dụ cơ bản
// Non-nullable type (không thể null)
String name = "John";
// name = null; // Lỗi biên dịch
// Nullable type (có thể null)
String? nullableName;
nullableName = null; // Hợp lệ
// Giải quyết null bằng null check
if (nullableName != null) {
print(nullableName.length);
}
// Sử dụng toán tử null-aware (?.)
int? length = nullableName?.length;
// Toán tử bang (!) để ép kiểu non-null
print(nullableName!.length); // Lỗi runtime nếu nullableName là null
DartCase Study
- Xây dựng ứng dụng quản lý người dùng
- Mô tả: Ứng dụng quản lý người dùng cho phép tạo tài khoản với email và mật khẩu. Một số thông tin (như số điện thoại) có thể không bắt buộc.
class User {
final String email; // Không thể null
final String password; // Không thể null
String? phoneNumber; // Có thể null
User(this.email, this.password, {this.phoneNumber});
}
void main() {
// Tạo user bắt buộc có email và password
User user1 = User("user@example.com", "password123");
// Thêm số điện thoại sau
user1.phoneNumber = "123456789";
// Kiểm tra trước khi sử dụng
if (user1.phoneNumber != null) {
print("Số điện thoại: ${user1.phoneNumber}");
} else {
print("Người dùng không có số điện thoại");
}
}
Dart2. API trả về dữ liệu có thể null
Mô tả: Bạn xây dựng ứng dụng gọi API, nhưng kết quả trả về đôi khi thiếu thông tin.
Future<String?> fetchUsernameFromApi() async {
// Giả sử API trả về null khi không tìm thấy user
return Future.delayed(Duration(seconds: 1), () => null);
}
void main() async {
String? username = await fetchUsernameFromApi();
// Kiểm tra null trước khi sử dụng
if (username != null) {
print("Username là $username");
} else {
print("Không tìm thấy username");
}
// Hoặc cung cấp giá trị mặc định
print("Username: ${username ?? 'Guest'}");
}
Dart3. Flutter Form với Null Safety
Mô tả: Xây dựng form đăng nhập trong Flutter với các giá trị nullable.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: LoginForm(),
);
}
}
class LoginForm extends StatefulWidget {
@override
_LoginFormState createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
String? email;
String? password;
void _submit() {
if (email != null && password != null) {
print("Email: $email");
print("Password: $password");
} else {
print("Hãy điền đủ thông tin!");
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Login Form")),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
decoration: InputDecoration(labelText: "Email"),
onChanged: (value) => email = value,
),
TextField(
decoration: InputDecoration(labelText: "Password"),
obscureText: true,
onChanged: (value) => password = value,
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _submit,
child: Text("Login"),
),
],
),
),
);
}
}
Dart4. Kết luận
Sound null safety giúp bạn:
- Xác định rõ ràng biến nào có thể null.
- Tránh lỗi runtime liên quan đến null.
- Viết code an toàn, rõ ràng và dễ bảo trì.
5. Bổ sung giải thích
Khai báo final String
trong class User
có ý nghĩa rất quan trọng để đảm bảo tính bất biến (immutability) của dữ liệu. Dưới đây là các lý do cụ thể:
1.Bảo vệ giá trị không bị thay đổ
Khi bạn khai báo một thuộc tính là final
, nó chỉ có thể được gán giá trị một lần duy nhất, thường là trong constructor. Sau khi được gán giá trị, biến đó không thể thay đổi.
class User {
final String email;
final String password;
User(this.email, this.password);
}
void main() {
var user = User("user@example.com", "password123");
// Không thể thay đổi giá trị email hoặc password sau khi khởi tạo
// user.email = "new_email@example.com"; // Lỗi biên dịch
}
DartĐiều này rất hữu ích để đảm bảo rằng các giá trị quan trọng như email, password không bị sửa đổi sau khi đối tượng được tạo.
2.Giảm lỗi logic trong ứng dụng
Bằng cách làm cho các thuộc tính không thể thay đổi, bạn tránh được các lỗi phát sinh từ việc vô tình thay đổi giá trị trong lúc ứng dụng chạy. Điều này giúp mã của bạn an toàn hơn.
Ví dụ:
- Một email không hợp lệ có thể gây ra lỗi khi xác thực.
- Một password thay đổi có thể dẫn đến lỗi không mong muốn.
3.Tăng tính bất biến (Immutability)
Các đối tượng bất biến dễ kiểm soát hơn, đặc biệt khi làm việc với các hệ thống lớn hoặc đa luồng. Khi một đối tượng không thể thay đổi, bạn có thể yên tâm rằng trạng thái của nó sẽ luôn nhất quán.
4.Dễ dàng sử dụng trong cấu trúc dữ liệu không thay đổi (Immutable Data Structure)
Trong nhiều ứng dụng, dữ liệu được chia sẻ giữa nhiều phần khác nhau của hệ thống. Nếu dữ liệu này bất biến, bạn không cần lo lắng về việc một phần của hệ thống thay đổi nó một cách không mong muốn.
Ví dụ, trong Flutter, việc sử dụng các đối tượng bất biến rất quan trọng để tối ưu hóa hiệu suất và tránh lỗi.
5.Định nghĩa rõ ràng vai trò của dữ liệu
Khai báo final
làm cho ý định của bạn rõ ràng:
- Những thuộc tính như
email
vàpassword
là các giá trị cố định, không cần thay đổi. - Những thuộc tính như
phoneNumber
(có thể thay đổi sau) thì không nên khai báo làfinal
.
Ví dụ:
class User {
final String email;
final String password;
String? phoneNumber; // Có thể thay đổi sau khi đối tượng được tạo
User(this.email, this.password);
}
void main() {
var user = User("user@example.com", "password123");
user.phoneNumber = "123456789"; // Hợp lệ
// user.email = "new_email@example.com"; // Lỗi biên dịch
}
Dart6.Hỗ trợ dễ dàng trong các thư viện như equatable
hoặc json_serializable
Khi sử dụng thư viện như equatable
(so sánh giá trị) hoặc json_serializable
(chuyển đổi JSON), các thuộc tính bất biến như final
giúp bạn dễ dàng quản lý trạng thái hơn.
7.Khi nào không cần final
?
- Khi bạn biết chắc rằng giá trị sẽ cần thay đổi sau khi đối tượng được tạo.
- Khi thuộc tính cần quản lý trạng thái trong suốt vòng đời đối tượng.
Ví dụ:
class Counter {
int value = 0; // Không dùng final vì giá trị sẽ thay đổi
void increment() {
value++;
}
}
Dart8.Tóm lại
Bạn nên khai báo final
với các thuộc tính:
- Chỉ cần khởi tạo một lần (email, password, ID…).
- Không thay đổi trong suốt vòng đời của đối tượng.
Điều này làm cho mã an toàn hơn, rõ ràng hơn và ít lỗi hơn.