Tự học Flutter 2023

New Dart

Null safety trong Dart

Null safety là chức năng của ngôn ngữ nhằm mục đích ngăn chặn lỗi xảy ra do truy cập không chủ ý các biến bị/được đặt giá trị null.
  • Null là gì
  • Nullable và Non nullable
  • Non nullable
  • Nullable
  • Null safety
  • Lợi ích của Null safety
  • Code an toàn hơn
  • Code nhanh hơn
  • Null-aware operators
  • Default Operator (??)
  • Null-aware assignment operator (??=)
  • Null-aware access operator (?.)
  • Null assertion operator (!)
  • Null-aware cascade operator (?..)
  • Null-aware index operator (?[])
  • Null-aware spread operator (…?)
  • Null là gì?

    Bạn đang có một biến để lưu thông tin về màu yêu thích nhất của người dùng, và nếu người dùng không có màu yêu thích bạn sẽ lưu giá trị null.

    
    favouriteColor = null;
                            

    Null nghĩa là không có, không tồn tại.

    Nullable và Non nullable

    Quay ngược về quá khứ, trước Dart 2.12, khi bạn khai báo một biến thì biến đó có thể chứa 2 loại giá trị: thứ nhất là giá trị thuộc về kiểu dữ liệu của nó (ví dụ biến int thì chứa số, biến String chứa chuỗi...) và giá trị null. Nói cách khác, null là kiểu con của mọi loại kiểu dữ liệu.

    
    Color favouriteColor; 
    // favouriteColor có thể là red, yellow... cũng có thể là null;
                            

    Vậy, vấn đề ở đây là gì? Việc một biến có thể chứa cả 2 loại dữ liệu là null và giá trị như vậy có thể gây các tai nạn không mong muốn. Mình tin chắc rằng không ít lần bạn đã gặp lỗi kiểu như the method '.someMethod()' was called on null.

    Lấy một ví dụ cụ thể, bạn và đồng nghiệp cùng code trên một chương trình. Trong chương trình đó có lớp Công Dân, lớp này có một thuộc tính là canCuocCongDan - thuộc tính này là bắt buộc, nghĩa là giá trị trường canCuocCongDan không được phép null.

    Tuy nhiên vì lý do gì đó (có thể vì quên), một trong số đồng nghiệp của bạn đã gán canCuocCongDan = null. Điểm đáng nói là lúc người đồng nghiệp này thực hiện việc gán null ở trên, trình biên dịch không hề thông báo gì - bởi vì việc gán giá trị null cho một biến là hoàn toàn hợp lệ.

    Bây giờ, nếu ở đâu đó trong code bạn thao tác gì đó với trường canCuocCongDan ở trên bạn sẽ dính lỗi null pointer exception và ứng dụng sẽ crash.

    
    congDan.canCuocCongDan.length... // canCuocCongDan is null
                            

    Đây rõ ràng chỉ là một tai nạn, nhưng có cách nào để chúng ta tránh được những tai nạn như thế này không? Dart đã đưa ra giải pháp đó là Null safety. Và để mở đường cho Null safety, việc đầu tiên Dart đã làm là tách biệt dữ liệu ra hai kiểu: một loại cho phép giá trị null và một loại ngược lại. Đây là lúc chúng ta nói về Nullable và Non nullable.

    Nếu như trước đó, Null là kiểu con của mọi loại dữ liệu - mọi kiểu dữ liệu đều có thể chứa Null thì nay đã không còn. Null bây giờ đứng riêng, và mọi kiểu dữ liệu trong Dart bây giờ mặc định là non nullable.

    hierarchy

    Non nullable

    Kiểu non nullable là kiểu chỉ chứa giá trị của nó ngoài ra không còn gì hơn. Một biến kiểu non-nullable int sẽ chỉ chứa giá trị kiểu int, một biến non-nullable kiểu String sẽ chỉ chứa giá trị String... Gán giá trị null cho biến kiểu non nullable sẽ gây lỗi biên dịch.

    Để khai báo một biến là non-nullable, xem ví dụ sau:

    
    String name = "hla"; // name là non nullable, cố gắng để name = null sẽ gây lỗi biên dịch
    int age = 18; // tương tự, age cũng là non nullable, bạn không thể age = null
                            

    Vậy nếu muốn khai báo một biến cho phép chứa giá trị null thì sao? Như ví dụ trước, người dùng không có màu yêu thích là chuyện bình thường và chúng ta cần null để thể hiện điều đó. Chúng ta sẽ đến với kiểu nullable ngay bây giờ.

    Nullable

    Kiểu nullable là kiểu mà ngoài chứa giá trị của nó ra, nó còn chứa giá trị null. Để khai báo kiểu nullable chúng ta thêm dấu ? vào sau kiểu dữ liệu khi khai báo. Như bạn có thể thấy rằng, tất cả các kiểu dữ liệu mặc định sẽ là kiểu non-nullable, và tất cả các kiểu non-nullable đều có một kiểu đối lập nullable, chỉ cần thêm dấu hỏi vào sau là được.

    
    String? name = null;
    int? age = null;
                            

    Null safety

    Null safety là chức năng của ngôn ngữ nhằm mục đích ngăn chặn lỗi xảy ra do truy cập không chủ ý các biến bị/được đặt giá trị null. Và để null safety trở thành hiện thực, sau đây là một số thay đổi (hoặc cũng có thể nói là những đặc điểm):

  • Kiểu mặc định sẽ là non nullable, muốn có kiểu nullable thì sử dụng dấu ?
  • Các tham số tùy chọn hoặc là nullable, hoặc phải có giá trị mặc định. Bạn có thể sử dụng từ khóa required để biến các tham số được đặt tên thành bắt buộc. Các biến non-nullable top level và các biến tĩnh phải có bộ khởi tạo. Các trường non-nullable của đối tượng (class fields) phải được khởi tạo trước khi phần thân của hàm dựng bắt đầu
  • Hàm không phải void phải trả về dữ liệu & trả đúng kiểu (trước đây có thể bỏ qua return)
  • Hệ thống phân tích luồng mới cho phép bạn biến các tham số và biến cục bộ nullable thành các biến và tham số non-nullable có thể sử dụng một cách an toàn. Hệ thống phân tích luồng mới cũng có các quy tắc thông minh hơn cho những việc như: chuyển kiểu dữ liệu, phát hiện thiếu return, phát hiện unreachable code, kiểm tra khởi tạo biến..
  • Từ khóa late giúp bạn sử dụng biến non-nullable và final trong trường hợp bạn không thể khởi tạo giá trị ngay khi khai báo. Ngoài ra nó còn giúp khởi tạo trễ.
  • Method chains after null-aware operators short circuit if the receiver is null. There are new null-aware cascade (?..) and index (?[]) operators. The postfix null assertion “bang” operator (!) casts its nullable operand to the underlying non-nullable type.
  • Null error sẽ được kiểm tra trong quá trình code (edit-time)
  • The List class is changed to prevent uninitialized elements.
  • Trên đây chỉ là bản tóm tắt, nếu bạn muốn tìm hiểu chi tiết hơn, có thể tìm đọc bài viết này: Understanding null safety.

    Lợi ích của Null safety

    Code an toàn hơn

    Null safety giúp ngăn các lỗi do truy cập không chủ ý vào các biến null, vì vậy nó giúp code của chúng ta an toàn hơn. Trình biên dịch có thể báo cho chúng ta biết ngay khi chúng ta làm sai điều gì đó.

    Quay trở lại với ví dụ về canCuocCongDan. Bây giờ với Null safety, bạn hoàn toàn có thể đảm bảo rằng trường canCuocCongDan = null sẽ không bao giờ null. (bằng cách khai báo nó là kiểu non-nullable)

    Khi đồng nghiệp của bạn vô tình gán null cho trường canCuocCongDan, ngay lập tức họ sẽ thấy lỗi biên dịch thông báo rằng họ không thể gán giá trị null cho một biến kiểu non-nullable. Vậy là lỗi đã không xảy ra.

    Code nhanh hơn

    Chương trình nhỏ hơn, nhanh hơn

    Trình biên dịch Dart có thể tối ưu mã giúp chương trình nhỏ hơn và nhanh hơn.

    Null-aware operators

    Cái tên nói lên tất cả, null-aware tức là nhận biết null, operators là các toán tử. Tóm váy lại là phần này giới thiệu các toán tử nhận biết khả năng null của đối tượng để thao tác mượt mà hơn.

    Default Operator (??)

  • Trả về biểu thức bên trái của ?? nếu biểu thức đó không null, và ngược lại thì trả về biểu thức bên phải.
  • Nó cũng còn được gọi là toán tử if-null, và là một toán tử kết hợp
  • 
    String? color = "Red";
    String favouriteColor = color ?? "Yellow";
    print("Favourite color is $favouriteColor");
    // Favourite color is Red
    
    --------------------------------------
    
    String? color = null;
    String favouriteColor = color ?? "Yellow";
    print("Favourite color is $favouriteColor");
    // Favourite color is Yellow
    
                          

    Trong ví dụ đầu tiên, color không null nên favouriteColor sẽ lấy giá trị của color (bên trái). Ở ví dụ phía dưới, color bây giờ null nên favouriteColor sẽ lấy giá trị bên phải của ??, tức là "Yellow".

    Null-aware assignment operator (??=)

    Sử dụng ??= khi ta muốn gán giá trị cho đối tượng phía bên trái nhưng chỉ khi đối tượng đó null.

    
    int? age;
    print(age); // in ra: null
    age ??= 18; // age đang null nên gán lại age thành 18
    print(age); // in ra: 18
    age ??= 28; // age không còn null, không thực hiện gán
    print(age); // in ra: 18
                            

    Null-aware access operator (?.)

    Toán tử này ngăn ứng dụng của bạn khỏi crash khi truy cập một thuộc tính hoặc một phương thức của một đối tượng null. Nói cách khác, nếu bạn muốn truy cập thuộc tính, phương thức của một đối tượng có khả năng sẽ null, sử dụng toán tử này. Nếu đối tượng đó null, toán tử này trả về null thay vì làm crash ứng dụng.

    
    dynamic name;
    //...
    print(name.length); // => crash vì name null
    print(name?.lenght); // => không crash, in ra: null
                            

    Null assertion operator (!)

    Toán tử này dùng để nói với trình biên dịch rằng bạn chắc chắc biến này không null, Dart sẽ bỏ qua việc kiểm tra, cảnh báo. Nhưng nếu trong quá trình runtime, biến đó null thì chương trình sẽ crash. Và tất nhiên đó là lựa chọn của bạn, nên là có chơi có chịu.

    
    String? name;
    
    void main() {
      print(name!.length); // => crash vì name null
    }
                            

    Null-aware cascade operator (?..)

    Toán tử cascade (.. trong Dart cho phép bạn thực hiện một chuỗi các thao tác trên cùng một đối tượng (bao gồm các lệnh gọi hàm và truy cập trường). Cùng với sự xuất hiện của null safety, nay nó cũng sẽ có một phiên bản mới. ?..

    Toán tử Null-aware cascade vẫn sẽ có chức năng như toán tử cascade nhưng thêm một điều kiện đó là khi và chỉ khi đối tượng thao tác không null. (tức là bổ sung thêm phần null-aware)

    
    class User {
      String name;
      int id;
      User(this.name, this.id);
      
      void sayHello() {}
    }
    
    void main() {
      User? user;
      
      // không chắc là user có null hay không, sử dụng Null-aware cascade operator
      user
        ?..id = 1
        ..name = 'hla'
        ..sayHello();
    }
                          

    Null-aware index operator (?[])

    Để thao tác với các phần tử của một danh sách nullable (nullable list) một cách dễ dàng hơn, Dart cung cấp toán tử ?[].

    
    int? getFirstItem(List? items) {
      return items?[0]; 
      // danh sách items có thể null, việc truy cập phần tử thứ nhất items[0] sẽ không an toàn. Để an toàn chúng ta phải kiểm
      // tra items có null hay không, thay vì dùng if else để kiểm tra, ta dùng toán tử ?[]
    }
                          

    Lưu ý rằng toán tử này chỉ giải quyết vấn đề null không giúp bạn giải quyết vấn đề về Index out of range đâu nhé. Như ví dụ trên nếu items rỗng, truy cập item[0] sẽ gặp lỗi Index out of range.

    Null-aware spread operator (…?)

    Toán tử spread (...) dùng để thêm các phần tử của một danh sách đang có vào một danh sách mới. Và cũng tương tự, với Null safety chúng ta có phiên bản null aware spread operator (...?)

    
    var listA = null;
    var listB = [0, ...?listA]; 
    // add tất cả phần tử của listA vào listB nếu listA không null