Tự học Flutter 2023

New Dart

Biến (variable)

Variable in Dart
Bài viết này sẽ hướng dẫn bạn làm quen với biến (variable) trong Dart. Cách khai báo khởi tạo biến, giá trị mặc định của chúng, các kiểu dữ liệu; tìm hiểu sơ qua về null safety, type safety; phân biệt được final và const, var và dynamic.

  • Sơ qua về Null safety
  • Sơ qua về Type safety
  • Các loại biến
  • Khai báo & khởi tạo
  • Từ khóa late
  • Giá trị mặc định
  • Kiểu dữ liệu
  • Var vs dynamic
  • Const vs final
  • Biến đơn giản là một cái tên chúng ta đặt cho một vùng bộ nhớ nơi mà ta lưu trữ dữ liệu. Mọi thứ bạn có thể gán cho một biến đều là một đối tượng (object). Ngay cả số, hàm và null cũng đều là là các object. Tất cả các chúng đều kế thừa từ lớp Object ngoại trừ null (nếu bạn bật Sound null safety).

    Nói sơ qua về Null safety

    Null safety nói nôm na là: biến không thể gán giá trị null trừ khi chúng ta chỉ định rõ rằng biến đó được phép null. Trong Dart để chỉ định rằng biến được phép null (nullable variable) chúng ta thêm dấu ? vào ngay sau kiểu dữ liệu khi khai báo biến. Ví dụ: int? age. Tại thời điểm này bạn chỉ cần lưu ý một điều là biến sẽ có 2 loại: nullable (cho phép null) và non-nullable (không cho phép null). Cụ thể cách khai báo chúng ta sẽ tìm hiểu ở phần dưới.

    Mục đích của Null safety là nhằm ngăn ngừa các lỗi do truy cập không chủ ý vào các biến được đặt thành null. Ví dụ, bạn code chung với đồng nghiệp, mọi người thỏa thuận với nhau rằng đối tượng công dân bắt buộc phải có chứng minh nhân dân (cmnd), nên là trường cmnd sẽ không được có giá trị null. Tuy nhiên một ai đó trong lúc nhầm lẫn đã gán giá trị null cho trường cmnd (vì không có null safety nên mọi biến đều có thể chứa giá trị null, lúc biên dịch sẽ không thấy báo lỗi gì, hoàn toàn hợp lệ). Bây giờ nếu bạn thao tác gì đó với trường cmnd này (vd: you.cmnd.length), bạn sẽ dính lỗi Null Pointer. Với Null safety, trường cmnd ngay từ đầu sẽ được khai báo là một biến non-nullable (không chứa giá trị null). Ngay khi đồng nghiệp của bạn vô tình gán cmnd = null thì người đó sẽ nhận được thông báo lỗi ngay lập tức khi biên dịch rằng trường này không cho phép null. Vậy là lỗi sẽ không xảy ra.

    Ban đầu Dart không có Null safety, cho đến tháng 3 năm 2021, với Dart 2.12 thì Null safety mới được hỗ trợ. Và cũng bắt đầu từ đây, cụm từ "nếu bạn bật Null safety" được đưa vào rất nhiều trong các tài liệu hướng dẫn mà bạn thấy trên mạng. Lý do đơn giản là vì nếu bạn bật Null safety thì cách khai báo biến, cách sử dụng sẽ khác với việc không bật Null safety. Và nếu bạn chưa biết trước điều này, mình cam đoan là nhiều bạn sẽ bị "loạn não", sao chỗ này bày thế này, chỗ kia bày thế kia, ai đúng? ai sai?

    Bạn nên bật Null safety (thực ra là từ Dart 2.12 Null safety đã mặc định được bật, nên là bạn không nên tắt Null safety). Cho dù bạn có tắt đi nữa thì tương lai bạn cũng phải bật, vì đã có thông báo là từ Dart 3 (2023), Null safety là bắt buộc. Và cũng vì vậy, trong tất cả bài viết của mình, mặc định là luôn có Null safety (khi hướng dẫn cách khai báo biến chẳng hạn), mình không quan tâm đến những thứ không có null safety, cái gì nên loại bỏ cứ loại bỏ, nhét nhiều vô đầu đau não.

    Mình có một bài viết khác về Null safety cụ thể hơn ở đây: Null safety.

    Nói sơ qua về Type safety

    Lại thêm một cái nói qua, nhưng khá quan trọng và cũng có liên quan ở phần tiếp theo, nên là bạn chịu khó đọc.

    Dart là ngôn ngữ type safe / strong type. Có nghĩa là nó sẽ luôn kiểm tra để đảm bảo rằng giá trị bạn gán cho biến đúng với kiểu dữ liệu của biến đó. Ví dụ: bạn không thể gán một chuỗi cho một biến kiểu int. Cũng vì vậy, về lý thuyết khi khai báo một biến bạn phải luôn khai báo kiểu dữ liệu của biến đó.

    Tuy nhiên, trên thực tế bạn không khai báo kiểu cũng được. Lý do là vì Dart có thể suy luận ra kiểu dữ liệu (Type inference). Trong phần tiếp theo, khi bạn khai báo biến với từ khóa var, đó là ví dụ cho việc Dart sẽ tự suy luận ra kiểu dữ liệu.

    Ngoài ra, Dart còn có một loại kiểu dữ liệu là dynamic, dynamic là kiểu dữ liệu có thể chấp nhận mọi loại dữ liệu. Bạn có thể sử dụng một biến kiểu dynamic để chứa chuỗi, số, null...

    Các loại biến trong Dart

    Trong Dart có 4 loại biến sau:

  • Biến toàn cục (Top level)
  • Biến tĩnh (Static)
  • Biến thành viên (Instance)
  • Biến cục bộ (Local)
  • Lý do mình nhắc đến các loại biến trước khi tìm hiểu cách khai báo biến là vì có một số lưu ý liên quan đến loại biến khi khai báo biến.

    Khai báo & khởi tạo

    Trong Dart, để khai báo một biến ta có 2 cách như bên dưới.

    
    1. var tênBiến = giáTrịKhởiTạo;
    2. kiểuDữLiệu tênBiến = giáTrịKhởiTạo;
                            

    Lưu ý quan trọng là: bạn phải khởi tạo giá trị cho tất cả các biến non-nullable trước khi sử dụng. Nói cách khác là ngay khi bạn khai báo một biến non-nullable, bạn phải gán giá trị khởi tạo cho nó ngay. Rule này áp dụng cho tất cả các loại biến, ngoại trừ biến cục bộ.Cũng có những trường hợp mà bạn không muốn gán giá trị ngay khi khai báo, lúc đó chúng ta cần tới từ khóa late, sẽ tìm hiểu ở những đoạn tiếp theo.

     

    1. var tênBiến = giáTrịKhởiTạo;

    Với cách khai báo này, bạn không cần phải chỉ định kiểu dữ liệu của biến. Dart sẽ tự suy luận ra kiểu dữ liệu dựa vào giá trị khởi tạo. (à, từ bây giờ bạn có thể bắt đầu vọc Dartpad được rồi đấy) Ví dụ:

                            
    var name = "hla";
    var age = 12;
    var score = 9.6;
    
    void main() {
    print("name is => ${name.runtimeType}"); // String
    print("age is => ${age.runtimeType}"); // int
    print("score is => ${score.runtimeType}"); // double
    }
    
    // Bấm run để xem kết quả nhé
                            
                            
  • Bạn có thể khai báo nhiều biến cùng một lúc như sau. Các biến có thể khác kiểu dữ liệu.
  • var a = 1, b = "two", c = [];
  • Nếu bạn gán lại giá trị cho các biến với kiểu dữ liệu khác bạn sẽ gặp lỗi biên dịch.
  • Nếu bạn không gán giá trị khởi tạo hoặc gán giá trị khởi tạo là null, biến của bạn sẽ là kiểu dynamic. Biến kiểu dynamic có thể chứa tất cả các loại dữ liệu bao gồm cả null. Kiểu dữ liệu chính xác của nó được xác định tại thời điểm runtime.
  •  

    2. kiểuDữLiệu tênBiến = giáTrịKhởiTạo;

    Với cách này bạn sẽ chỉ định rõ kiểu dữ liệu của biến (int, String, num...).

                          
    String name = "hla";
    int age = 12;
    double score = 9.6;
    
    void main() {
    print("name is => $name");
    print("age is => $age");
    print("score is => $score");
    }
    
    // Bấm run để xem kết quả nhé
                          
                          

  • Bạn có thể khai báo nhiều biến cùng một lúc như sau. Lưu ý là tất cả các biến cùng kiểu dữ liệu.
  • int a = 1, b = 2, c = 3; String a = "a", b = "b", c = "c";
  • Nếu bạn gán lại giá trị cho các biến với kiểu dữ liệu khác bạn sẽ gặp lỗi biên dịch thông báo rằng bạn không thể gán dữ liệu khác kiểu dữ liệu đã khai báo.
  • Nếu bạn không gán giá trị khởi tạo, bạn sẽ gặp lỗi biên dịch thông báo rằng bạn phải khởi tạo giá trị cho biến non-nullable. (Khi khai báo như thế này với tất cả kiểu dữ liệu ngoại trừ dynamic thì biến của bạn là biến non-nullable)
  • Nếu bạn khai báo biến của bạn kiểu dynamic, bạn sẽ không cần khởi tạo giá trị vì mặc định nó sẽ là null.
  •  

    Khai báo biến nullable

    Như đã nhắc qua ở những đoạn trước, để khai báo một biến có thể chứa giá trị null (nullable), bạn chỉ cần thêm dấu ? vào sau kiểu dữ liệu khi khai báo. Đối với biến nullable, sẽ có một số toán tử đặc biệt khi thao tác với chúng. Bạn sẽ tìm hiểu sau khi chúng ta nói kỹ hơn về Null safety.

    
    String? name;
    int? age;
    double? score = 9.6;
    
    void main() {
      print("name is => $name");
      print("age is => $age");
      print("score is => $score");
      score = null; // biến score là nullable nên ta có thể gán null
      print("score is => $score");
    }
    // Bấm run để xem kết quả nhé
    
    
    Với biến nullable, bạn có thể bỏ qua không cần phải gán giá trị khởi tạo cho nó, vì mặc định nó sẽ là null.

    Lưu ý quan trọng

  • Bạn không cần phải gán giá trị khởi tạo đối với biến cục bộ.
  • Nếu bạn không khởi tạo giá trị ngay lúc khai báo thì bạn cũng phải gán giá trị cho nó trước khi sử dụng.
  • Trong trường hợp bạn khai báo biến bằng từ khóa var và không gán giá trị khởi tạo thì biến của bạn sẽ là biến kiểu dynamic.
  • Ý kiến cá nhân

    Cá nhân mình thì mình không khuyến khích bạn khai báo biến bằng từ khóa var vì: thứ nhất nó không tường minh. Thứ 2, khi xài var kết hợp với việc không gán giá trị khởi tạo biến của bạn sẽ là biến kiểu dynamic. Hãy hạn chế dùng dynamic ở mức tối đa.

    Khi một biến là dynamic, static type-checking sẽ bị disable, dẫn đến nhiều lỗi tiềm tàng. Ví dụ như trường hợp này, trình biên dịch không hề cảnh báo bạn điều gì cả, nhưng khi runtime bạn sẽ gặp lỗi vì int không có làm length.

    
    var anything;
    
    void main() {
      anything = 18;
      print("Length of anything => ${anything.length}");
    }
    // Bấm run để xem kết quả nhé
    
    

    Từ khóa late

    Đặt từ khóa late trước kiểu dữ liệu khi khai báo biến như thế này: late String name;

    Từ khóa late được dùng trong 2 trường hợp:

  • Khai báo một biến non-nullable mà không muốn khởi tạo giá trị ngay lúc khai báo.
  • Khởi tạo giá trị một biến vào lúc lần đầu sử dụng (chứ không phải lúc khai báo - lazy).
  • Với trường hợp thứ 2, ví dụ bạn viết thế này late int totalAmount = calcTotalAmount();, thì hàm calcTotalAmount sẽ chỉ được gọi khi lần đầu tiên bạn sử dụng biến totalAmount chứ không phải ngay lúc bạn khai báo. Điều này có thể giúp tối ưu chương trình nếu biến totalAmount có thể sẽ không dùng tới, hoặc hàm calcTotalAmount sử dụng nhiều tài nguyên.

    Giá trị mặc định

    Các biến nullable thì giá trị mặc định ban đầu là null. Còn các biến non-nullable thì giá trị ban đầu là do bạn đặt. Nhắc lại quy tắc là bạn phải khởi tạo giá trị cho tất cả các biến non-nullable trước khi sử dụng.

    Biến toàn cục (top level) và biến thành viên (instance) được khởi tạo trễ (lazy); việc khởi tạo chỉ được thực hiện khi lần đầu tiên bạn sử dụng biến.

    Kiểu dữ liệu

    Bên dưới là một số kiểu dữ liệu cơ bản mà Dart hỗ trợ, mình sẽ có một bài viết khác đi vào chi tiết từng kiểu dữ liệu sau.

  • Numbers (int, double)
  • Strings (String)
  • Booleans (bool)
  • List
  • Set
  • Map
  • Runes
  • Symbol
  • Null
  • Var và dynamic

    Trên Stackoverflow, một câu hỏi vẫn hay được đặt lại từ 10 năm trước đến tận bây giờ là: vardynamic khác gì nhau? Cá nhân mình nghĩ rằng việc vẫn lăn tăn giữa var và dynamic là vì không hiểu bản chất.

    Đầu tiên, dynamic là một kiểu dữ liệu. Nó cũng như int, string, bool,... là một kiểu dữ liệu. Có điều nó được xem là một kiểu đặc biệt, bởi vì nó có thể chứa bất kỳ giá trị kiểu nào. Bạn có thể gán một số, một chuỗi, một danh sách... cho một biến dynamic mà không gặp lỗi gì.

    var đơn giản chỉ là một từ khóa (keyword) dùng để khai báo biến. Và khi dùng var thì chúng ta không chỉ rõ kiểu dữ liệu, trình biên dịch sẽ tự xác định kiểu của biến dựa vào giá trị khởi tạo. Và nếu chúng ta không gán giá trị khởi tạo (ví dụ: var aVariable;) thì lúc này trình biên dịch không suy ra được kiểu dữ liệu của biến và nó gán biến đó là kiểu dynamic. Có thể đây là điểm mà nhiều bạn hay lăn tăn, bởi vì lúc này var aVariable; sẽ tương đương với dynamic aVariable;. Hãy thoát khỏi lối suy nghĩ này, không có chuyện var aVariable; = dynamic aVariable;, chẳng qua là complier không đủ thông tin để xác định kiểu của biến nên aVariable sẽ được gắn là kiểu dynamic mà thôi.

    Hãy nhớ, var chỉ là từ khóa để khai báo biến, dynamic là một kiểu dữ liệu, hai đứa chúng nó chả liên quan gì nhau.

    Const và final

    Cả final và const đều được dùng để khai báo các hằng số (các biến có giá trị cố định không thay đổi trong suốt quá trình sử dụng). Tuy vậy, đó cũng là điểm chung duy nhất giữa chúng, và ngay cả trong điểm chung này cũng có chút khác biệt.

    Final

    Final có nghĩa là chỉ định một lần: biến hoặc trường final sau khi được gán một giá trị, giá trị đó sẽ không thể thay đổi. Cố gắng gán lại giá trị mới cho biến/trường đó sẽ gây lỗi.

    Biến final, cũng như biến bình thường, bạn phải gán giá trị ngay khi khai báo (ngoại trừ biến cục bộ), nếu muốn gán sau thì bạn có thể dùng từ khóa late.

    Lưu ý rằng mặc dù một object final (Lists, Maps, Classs) là không thể thay đổi nhưng các trường của nó vẫn có thể thay đổi giá trị được.

                          
    final colors = ["red", "blue", "yellow"];
    
    void main() {
      // colors = ["green", "white", "brown"]; // gán lại lỗi
      colors[1] = "green"; // thay đổi field được
      print("$colors");
    }
    
    // Bỏ comment dòng 4 và bấm run để xem kết quả nhé
                          
                          

    Const

    Const là hằng số lúc compile, tại thời điểm biên dịch giá trị phải được xác định. Vì vậy, bạn bắt buộc phải gán giá trị cho biến const ngay khi khai báo (không dùng late được), và tất nhiên giá trị đó sẽ không thể thay đổi. Cố gắng gán lại giá trị mới cho biến/trường đó sẽ gây lỗi.

    Các biến const là "canonicalized". Với bất kỳ một "giá trị" const nào, chỉ duy nhất một đối tượng được tạo ra và tái sử dụng. Điều này giúp cải thiện hiệu năng.

                          
    class Cake {
      final String type;
      final double price;
      const Cake({required this.type, required this.price});
    }
    
    void main() {
      var banhCuaTui = const Cake(type: "Birthday", price: 100);
      var banhCuaBan = const Cake(type: "Birthday", price: 100);
      var banhCuaCoAy = const Cake(type: "Birthday", price: 100);
    
      print("banhCuaTui => ${identityHashCode(banhCuaTui)}");
      print("banhCuaBan => ${identityHashCode(banhCuaBan)}");
      print("banhCuaCoAy => ${identityHashCode(banhCuaCoAy)}");
    }
    
    // Cả 3 cái bánh đều là 1 object, bấm Run để xem 
                          
                          

    Không giống như một object final, một object là const thì mọi thứ trong nó đều là const, bạn không thể cập nhật giá trị.

                          
    const colors = ["red", "blue", "yellow"];
    
    void main() {
      colors = ["green", "white", "brown"]; // gán lại sẽ lỗi
      colors[1] = "green"; // thay đổi field cũng không được
      print("$colors");
    }
    
    // Bấm run để xem kết quả nhé