[Dart] Sound Null Safety

[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:
    1. Nullable type (có thể null): Được khai báo với dấu ?. Ví dụ: String?.
    2. Non-nullable type (không thể null): Không cần dấu ?. Ví dụ: String.

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

  1. Ngăn chặn lỗi runtime: Giảm nguy cơ gặp lỗi null tại runtime.
  2. 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.
  3. 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
Dart

Case Study

  1. 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");
  }
}
Dart

2. 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'}");
}
Dart

3. 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"),
            ),
          ],
        ),
      ),
    );
  }
}
Dart

4. 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ư emailpassword 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
}
Dart

6.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++;
  }
}
Dart

8.Tóm lại

Bạn nên khai báo final với các thuộc tính:

  1. Chỉ cần khởi tạo một lần (email, password, ID…).
  2. 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.

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *