Tự học Flutter 2023

New Dart

Hàm (Function) trong Dart

Nội dung bài viết này sẽ hướng dẫn bạn tìm hiểu về hàm (function) trong Dart: cách khai báo hàm, cách gọi, các loại tham số, hàm ẩn danh, phạm vi biến, Calable class... Chỉ mục bài viết cho các bạn dễ tham khảo:
  • Định nghĩa hàm
  • Gọi hàm
  • Arrow shorthand syntax
  • Tham số
  • 1. Required positional Parameters
  • 2. Optional positional Parameters
  • 3. Required named Parameters
  • 4. Optional named Parameters
  • Trộn lẫn tham số
  • Tổng kết về tham số
  • Nên sử dụng loại nào?
  • Truyền hàm như tham số
  • Hàm ẩn danh (Anonymous function)
  • Lexical scope
  • Lexical closure
  • Callable class
  • Định nghĩa hàm

    Để định nghĩa một hàm, chúng ta theo cú pháp sau:

    kiểuTrảVề tênHàm(thamSốNếuCó) {
      // nội dung hàm
    }

    Lấy ngay một ví dụ cho dễ hiểu:

    Định nghĩa hàm trong Dart

  • (1) String: là kiểu trả về của hàm
  • (2) getFullName: là tên hàm
  • (3) firstName, lastName: là 2 tham số đầu vào
  • (4) là nội dung hàm
  • Gọi hàm

    Để gọi hàm chúng ta thực hiện cú pháp như sau

    tênHàm(thamSốNếuCó);

    Nối tiếp ví dụ ở trên, bây giờ để gọi hàm getFullName, chúng ta làm như sau:

                            
    String getFullName(String firstName, String lastName) {
      return "$lastName $firstName";
    }
    
    void main() {
      String fullName = getFullName("Linh", "Nguyen Van");
      print(fullName);
    }
    
    // bấm run để xem kết quả
                            
                            

    Hàm getFullName nhận vào hai tham số (firstName, lastName) rồi trả về fullname. Nên ở ví dụ trên mình tạo ra một biến tên là fullName để chứa giá trị trả về, sau đó in ra để xem.

    Arrow shorthand syntax

    Arrow shorthand syntax (cú pháp ngắn gọn =>), như tên gọi, nó là một cú pháp ngắn gọn dành cho các hàm chỉ có một biểu thức duy nhất. Lúc này, thay vì đặt nội dung hàm trong cặp {}, cú pháp mới sẽ là:

    kiểuTrảVề tênHàm(thamSốNếuCó) => nộiDungHàm;

    Như hàm getFullName ở trên, nội dung hàm chỉ có một biểu thức duy nhất viết trên một dòng, ta có thể viết lại như sau:

    
    // viết lại với Arraw shorthand syntax
    String getFullName(String firstName, String lastName) => "$lastName $firstName";
    
    void main() {
    String fullName = getFullName("Linh", "Nguyen Van");
    print(fullName);
    }
    
    // bấm run để xem kết quả
    
    

    Tham số (parameter)

    Trong Dart, tham số hàm có thể chia làm 2 loại: tham số theo tên (named parameter) và tham số vị trí (positional parameter). Với mỗi loại lại chia thành 2 loại con là bắt buộc (Required) và tùy chọn (Optional). Vậy, chúng ta sẽ có 4 loại tham số như sau:

  • 1. Required positional Parameters (vị trí - bắt buộc)
  • 2. Optional positional Parameters (vị trí - tùy chọn)
  • 3. Required named Parameters (tên - bắt buộc)
  • 4. Optional named Parameters (tên - tùy chọn)
  • Bây giờ chúng ta sẽ đi lần lượt qua từng loại tham số.

    1. Required positional Parameters

    Loại đầu tiên, tham số theo vị trí + bắt buộc. Vì sao gọi là tham số theo vị trí? bởi vì với kiểu tham số này, tham số được tham chiếu dựa theo vị trí thứ tự (đầu tiên, thứ hai, thứ 3...). Chúng ta phải tự biết vị trí của tham số để truyền vào cho đúng khi gọi hàm.

    Như ở ví dụ trước String getFullName(String firstName, String lastName). firstName, lastName là 2 tham số vị trí. Khi chúng ta gọi hàm, chúng ta phải nhớ/biết rằng tham số đầu tiên là firstName, tham số thứ 2 là lastName.

    Gọi là là tham số bắt buộc là bởi vì... nó bắt buộc. Có nghĩa là khi gọi hàm bạn bắt buộc phải truyền giá trị cho tham số đó. Tiếp tục với String getFullName(String firstName, String lastName), firstName và lastName là 2 tham số bắt buộc. Khi gọi hàm bạn phải truyền cả 2 tham số, bạn không thể chỉ truyền 1 trong 2, hoặc là không truyền tham số nào.

    Khai báo & và truyền giá trị

    Để khai báo một tham số vị trí + bắt buộc cũng như cách truyền? Hãy xem một vài ví dụ:

    Required positional Parameters

    Lưu ý:

  • Không đặt giá trị mặc định cho tham số vị trí bắt buộc. Ví dụ: int name = "Linh" sẽ gặp lỗi biên dịch.
  • Ngay tại thời điểm này có lẽ bạn cũng nhận ra một vấn đề rồi nhỉ? Khi gọi hàm có các tham số vị trí, bạn phải biết giá trị bạn cần truyền nằm ở vị trí nào, nếu bạn truyền nhầm, may mắn thì sẽ có lỗi lúc biên dịch, nếu xui thì xảy ra lỗi logic. Ví dụ như hàm getFullName bạn có thể sẽ truyền nhầm giữa firstName và lastName, trình biên dịch sẽ không báo lỗi vì cả 2 tham số đều nhận vào kiểu String, tuy nhiên rõ ràng bây giờ fullname trả về sẽ bị ngược.

     

    2. Optional positional Parameters

    Tiếp theo, tham số theo vị trí + tùy chọn. Bây giờ nó vẫn là tham số theo vị trí, tuy nhiên tham số này là tùy chọn (optional), có nghĩa là khi gọi hàm bạn có thể bỏ qua không truyền giá trị cho tham số đó.

    Khai báo

    Để khai báo một tham số vị trí là tùy chọn, bạn đặt tham số đó trong cặp [] như thế này [int height = 0, int? weight]:

    Optional positional Parameters

    Lưu ý:

  • Nếu tham số của bạn là non-nullable, bạn cần phải đặt giá trị mặc định.
  • Nếu tham số của bạn là nullable, giá trị mặc định đã là null, bạn không cần đặt giá trị mặc định nữa.
  • Bạn có thể sử dụng song song tham số vị trí bắt buộc và tùy chọn, nhưng tham số tùy chọn phải đặt sau cùng.
  • Truyền giá trị

    Để truyền giá trị cho tham số khi sử dụng tham số vị trí tùy chọn chúng ta làm như sau:

    Optional positional Parameters

     

    3. Required named Parameters

    Khai báo

    Chúng ta khai báo một tham số theo tên bắt buộc bằng cách đặt nó vào trong cặp {} và thêm chú thích required vào trước kiểu dữ liệu như thế này {required String name, required int age}

    Required named Parameters

    Lưu ý:

  • Tham số bắt buộc không được đặt giá trị mặc định. Ví dụ: required String name = "", như này sẽ gặp lỗi biên dịch.
  • Truyền giá trị

    Chúng ta truyền theo cú pháp như cũ tênBiến: giáTrị. Ví dụ:

    Required named Parameters

     

    4. Optional named Parameters

    Khai báo

    Chúng ta khai báo một tham số theo tên tùy chọn bằng cách đặt nó vào trong cặp {} như thế này {String name, int age}

    Optional named Parameters

    Lưu ý:

  • Nếu tham số của bạn là non-nullable, bạn cần phải đặt giá trị mặc định.
  • Nếu tham số của bạn là nullable, giá trị mặc định đã là null, bạn không cần đặt giá trị mặc định nữa.
  • Truyền giá trị

    Để truyền giá trị cho tham số khi sử dụng tham số theo tên chúng ta truyền theo cú pháp tênBiến: giáTrị. Ví dụ:

    Optional named Parameters

     

    Tổng kết ngắn gọn

     

    1. Required positional Parameters

  • Cú pháp khai báo kiểuDữLiệu tênThamSố. Ví dụ: int age, String name
  • Không chấp nhận giá trị mặc định. Ví dụ: int age = 18❌, int age
  • Buộc phải có khi gọi hàm
  • Khi gọi hàm, truyền tham số theo thứ tự. Ví dụ: functionName(param1Value, param2Value);
  • Thứ tự tham số có ý nghĩa quan trọng. functionName(param2Value, param1Value);
  • 2. Optional positional Parameters

  • Để khai báo, đặt các tham số vào bên trong cặp [], ví dụ: [String? name, int age = 18]
  • Bắt buộc phải gán giá trị mặc định (nếu tham số nullable thì giá trị mặc định đã là null)
  • Bạn có thể bỏ qua tham số tùy chọn khi gọi hàm
  • Khi gọi hàm, truyền tham số theo thứ tự. Ví dụ: functionName(param1Value, param2Value);
  • Thứ tự tham số có ý nghĩa quan trọng. functionName(param2Value, param1Value);
  • 3. Required named Parameters

  • Để khai báo, đặt các tham số vào bên trong cặp {} và thêm chú thích required vào trước kiểu dữ liệu. Ví dụ: {required String name, required int age}
  • Không chấp nhận giá trị mặc định {required int age = 18}❌, {required int age}
  • Buộc phải có khi gọi hàm
  • Khi gọi hàm, truyền tham số theo tên. Ví dụ: functionName(name: "Linh", age: 18);
  • Thứ tự tham số không quan trọng. functionName(name: "Linh", age: 18);functionName(age: 18, name: "Linh");
  • 4. Optional named Parameters

  • Để khai báo, đặt các tham số vào bên trong cặp {}, ví dụ: {String name = "", int age = 18, int? height}
  • Bắt buộc phải gán giá trị mặc định (nếu tham số nullable thì giá trị mặc định đã là null)
  • Bạn có thể bỏ qua tham số tùy chọn khi gọi hàm
  • Khi gọi hàm, truyền tham số theo tên
  • Thứ tự tham số không quan trọng
  •  

    Trộn lẫn các loại tham số

    Bạn có thể trộn lẫn các kiểu tham số với nhau. Như các ví dụ trên, chúng ta đã trộn lẫn 2 loại tham số vị trí (required và optional) với nhau. Tương tự chúng ta cũng có thể trộn lẫn 2 loại tham số theo tên (required và optional) với nhau.

    Ngoài ra chúng ta còn có thể trộn lẫn tham số vị trí và tham số tên với nhau. Nhưng lưu ý là:

  • Không thể trộn lẫn tham số tên với tham số vị trí tùy chọn
  • Tham số vị trí bắt buộc luôn phải đứng trước các tham số loại khác
  • 
    // OK
    void requiredPositionalAndOptionalPositional(String name, [int? age]) {}
    void requiredNamedAndOptionalNamed({required String name, int? age}) {}
    void requiredPositionalAndRequiredNamed(int? age, {required String name}) {}
    void requiredPositionalAndOptionalNamed(int? age, {String? name}) {}
    
    // COMPILE ERROR
    void optionalPositionalThenRequiredPositional([int? age], String name) {
      // required positional phải được đặt ở vị trí nhóm đầu tiên
    }
    void requiredNamedAndOptionalPositional({required String name}, [int? age]) {
      // named không kết hợp được với optional positional 
    }
    void requiredNamedAndOptionalPositional2([int? age], {required String name}) {
      // named không kết hợp được với optional positional bất kể vị trí tứ tự
    }
    void requiredNamedAndRequiredPositional({required String name}, int? age) {
      // required positional phải được đặt ở vị trí nhóm đầu tiên
    }
                          

     

    Nên sử dụng loại nào?

    Từ những phân tích và ví dụ ở trên, bạn có thể tự mình đưa ra quyết định cho bản thân. Miễn sao nó phù hợp với bạn, phù hợp với từng tình huống cụ thể.

    Lựa chọn của cá nhân mình là "Named parameter" cho đa số trường hợp. Mặc dù code có dài hơn chút xíu nhưng xứng đáng.

     

    Truyền hàm như một tham số

    Dart là một ngôn ngữ lập trình hướng đối tượng đúng nghĩa, Hàm cũng là object, vì vậy có thể truyền hàm như tham số, cũng có thể return hàm từ một hàm khác.

    Anonymous function

    Anonymous function (Hàm ẩn danh), như tên gọi là hàm không có tên. Còn được biết với các tên gọi khác như: nameless function, lambda, closure, function literal. Nó cũng như hàm bình thường chúng ta đã tìm hiểu, chỉ khác mỗi là không có tên :D. Cú pháp định nghĩa như sau:

    (thamSố) {
       nội dung hàm
    }

    Hàm ẩn danh thường được dùng như là một tham số để truyền vào một hàm khác. Lấy ví dụ, List có phương thức forEach nhận vào là một hàm void. void forEach(void Function(E element) action). Bạn có thể viết một hàm bình thường sau đó truyền vào cho hàm forEach, cũng có thể sử dụng hàm ẩn danh cho nhanh gọn.

    Anonymous function

    Lexical scope / Phạm vi biến trong các hàm lồng nhau

    Lexical Scope chỉ việc giải quyết biến trong các hàm lồng nhau: các hàm bên trong có quyền truy cập vào các biến và tài nguyên khác trong phạm vi của cha chúng.

    Dart là một ngôn ngữ "Lexical scope", có nghĩa là phạm vi của các biến được xác định tĩnh, đơn giản bằng cách bố trí mã. Mà chính xác hơn là dựa vào cặp {}. Tất cả các biến, hàm... được khai báo bên trong một cặp {} thì thuộc về scope đó. Scope con (lồng) bên trong có thể truy cập biến và tài nguyên của scope bên ngoài. Nhưng ngược lại thì không được.

    Nói thì trừu tượng, ví dụ thì đơn giản:

    Optional named Parameters

  • nestedFunction có thể truy cập tất cả các biến thuộc phạm vi bên ngoài nó: topLevel, insideMain, insideFunction
  • Tương tự, myFunction có thể truy cập tất cả các biến thuộc phạm vi bên ngoài nó: topLevel, insideMain
  • Nhưng, myFunction không thể truy cập vào biến của nestedFunction: insideNestedFunction. Tương tự, main cũng không thể truy cập vào biến của myFunction.
  • Phần này mình thấy mình mô tả cũng không được tốt lắm. Để hiểu sâu hơn, bạn có thể Google để tìm hiểu với 2 từ khóa: Lexical scope (static scope) và dynamic scope.

    Closure

    Những hàm bên trong (inner functions) mà nắm giữ các biến thuộc về phạm vi của hàm bên ngoài ngay cả khi hàm bên ngoài đã được return được gọi là closure.

    Callable class

    Callable class nghĩa là thể hiện của một lớp có thể được gọi như một hàm. Để tạo một Callable class, bạn phải định nghĩa phương thức call bên trong lớp đó. Phương thức call có thể nhận bất kỳ số lượng đối số nào và trả về bất kỳ loại giá trị nào.

    
    class Add {
      num call(num a, num b) => a + b;
    }
    
    void main() {
      var add = Add();
      print(add(2, 3));
      print(Add()(20, 30));
      // bấm run để xem kết quả
    }