Facebook Pixel

Tự học ngôn ngữ Dart: Functions

07 Jul, 2021

Nguyên

Author

Tự học ngôn ngữ Dart. Trong bài viết này, chúng ta sẽ đề cập đến các hàm do người dùng tự định nghĩa và tìm hiểu cách viết hàm của riêng mình.

Tự học ngôn ngữ Dart: Functions

Mục Lục

Trong bài viết "Collection của ngôn ngữ Dart", bạn đã được giới thiệu về các hàm tích hợp sẵn hay còn được gọi là các phương thức. Trong bài viết này, chúng ta sẽ đề cập đến các hàm do người dùng tự định nghĩa và tìm hiểu cách viết hàm của riêng mình.

1. Định nghĩa hàm

Hãy đi thẳng vào vấn đề và xem xét một ví dụ về một hàm rất cơ bản do người dùng tự định nghĩa.

Dart
void newPrint(){
  print("Function Called");
}

// Driver Code
main() {
  newPrint();
}
Bash
Output:
Function Called

Hàm trên có vẻ không có ý nghĩa gì lắm vì nó chỉ đơn giản là in ra câu lệnh Function Called. Tuy nhiên cái quan trọng ở đây là syntax chung của hàm đó.

Syntax:

void cho chúng ta biết là hàm sẽ không trả về kết quả gì.

newPrint là tên mà bạn có thể tự chọn cho hàm. Đảm bảo rằng tên phải có ý nghĩa đối với chức năng của hàm.

Sau newPrint là dấu ()

Nội dung của hàm sẽ được thể hiện trong dâu ngoặc nhọn {}.

print("Function Called"); chính là nội dung của hàm.

Syntax chung như sau:

syntax hàm người dùng tự định nghĩa

1.1 Những hàm được tham số hóa :

Chúng ta có thể tạo ra các hàm nhận giá trị giống như là phương thức.

Hãy tạo ra một hàm nhận hai tham số và trả về tổng của chúng.

Dart
num sum(num x, num y){
  return x+y;
}

// Driver Code
main() {
  print(sum(1,2));
}  
Bash
Output:
3

Trong ví dụ đầu tiên, tên hàm của chúng ta được theo sau bởi dấu ngoặc đơn (()) trống. Lần này, dấu ngoặc chứa các tham số sẽ được chuyển cho hàm.

Ở dòng num sum(num x, num y){, (num x, num y) cho chúng ta biết rằng hàm của chúng ta nhận hai tham số. Cái đầu tiên có tên là x và phải có kiểu num. Cái thứ hai có tên là y và cũng phải thuộc kiểu num.

Ở dòng return x+y;, chúng ta đang trả về x+y bằng cách sử dụng từ khóa return.

Các tham số được phân tách bằng dấu phẩy ,.

Syntax đặc biệt

Như chúng ta đã thảo luận, nội dung của một hàm nằm trong dấu ngoặc nhọn ({}). Tuy nhiên, bạn có thể chọn không sử dụng dấu ngoặc nhọn nếu nội dung của hàm chỉ bao gồm một biểu thức duy nhất và viết mọi thứ trong một dòng duy nhất.

Syntax sẽ hơi khác một chút:

Những hàm được tham số hóa

Khi sử dụng syntax này, bạn không cần phải sử dụng từ khóa return để trả về giá trị. Mũi tên sẽ phân tách tham s, kiểu của hàm với nội dung của hàm.

Hãy viết hàm sum bằng syntax ngắn hơn nhé:

Dart
num sum(num x, num y) => x+y;

// Driver Code
main() {
    print(sum(1,2));
}
Bash
Output:
3

Cho đến bây giờ, chúng ta đã viết tất cả mã trong hàm main(). Các hàm do người dùng tự định nghĩa thì  không cần phải được xác định trong hàm main, nhưng nó vẫn có thể làm được điều đó.

Hãy cùng khám phá điều này trong phần tiếp theo.

2. Gọi hàm

Khi bạn muốn sử dụng một hàm, nó cần phải được gọi vào. Cách bạn gọi một hàm do người dùng tự định nghĩa cũng giống như cách bạn gọi một hàm tích hợp sẵn. Chúng ta sẽ gọi tên của nó, theo sau là dấu (). Hãy gọi hàm newPrint và hàm sum mà chúng ta đã xác định trong phần trước.

Dart
// Print the statement "Function Called"
void newPrint(){
  print("Function Called");
}

// Return the sum of two numbers
num sum(num x, num y){
  return x+y;
}

main() {
  // Calling newPrint
  newPrint();

  //Calling sum
  var result = sum(5,3);
  print(result);
}
Bash
Output:
Function 
Called 8

Ở dòng newPrint(); của đoạn code trên, chúng ta chỉ đơn giản gọi hàm newPrint, hàm sẽ in ra output là Function Called.

Ở dòng thứ var result = sum(5,3);, chúng ta chuyển 5 và 3 vào hàm sum. Kết quả của hàm sum này sẽ được chứa trong biến result . Dòng print(result); sẽ in ra giá trị của biến đó.

2.1 Gọi hàm trong hàm

Đôi khi chúng ta cần chức năng của một hàm đã tồn tại xuất hiện trong một hàm mới. Thay vì viết lại đoạn code mới, chúng ta chỉ cần gọi hàm cũ trong phần nội dung của hàm mới mà chúng ta đang viết. Điều này sẽ được làm rõ trong ví dụ dưới đây.

Hãy viết một hàm cho chúng ta bình phương của một số nhất định.

Dart
num square(num x) {
  return x * x; 
}

main() {
  // Driver Code
  var result = square(5);
  print(result);
}
Bash
Output:
25

Bây giờ, chúng ta muốn viết một hàm lấy tổng bình phương của hai số. Hãy thử làm điều này bằng cách sử dụng hàm square mà chúng ta vừa định nghĩa ở trên.

Dart
num squareSum(num x, num y){
  return square(x) + square(y);
}

Trong đoạn code bên trên, chúng ta gọi hàm square bên trong hàm SquareSum .

Dart
// Function to find the square of a number
num square(num x) {
  return x * x; 
}
  
// Function to find the sum of the squares of two numbers
num squareSum(num x, num y){
  return square(x) + square(y);
}

main() {
  var result = squareSum(2,5);
  print(result);
}
Bash
Output:
29

3. Optional Parameters

Một hàm có thể có hai kiểu tham số: required parameteroptional parameter . Required parameter được liệt kê trước theo sau là những optional parameter. Optional parameter có thể là named parameters hoặc positional parameters.

Named parameters

Trong một hàm, những named parameters được khai báo bằng dấu ngoặc nhọn {}.

Named parameters

Các named parameters có tên gọi như vậy vì chúng cần được đặt tên trong khi gọi hàm. Dưới đây là syntax để chỉ định các named parameters trong một cuộc gọi hàm.

Trong ví dụ dưới đây, chúng ta đang in một số và hai chuỗi. Số là một required parameter trong khi các chuỗi là các optional named parameters.

Dart
printer(num n,{String s1, String s2}) { 
  print(n); 
  print(s1); 
  print(s2);
}

Hãy gọi hàm printer trong khi chỉ xác định một trong những optional parameters.. Đảm bảo rằng tên tham số bạn đang chỉ định giống với tên mà bạn đã chỉ định làm tham số khi khai báo hàm.

Dart
printer(num n,{String s1, String s2}) { 
  print(n); 
  print(s1); 
  print(s2);
}

main() {
  printer(75, s1: 'hello');
}
Bash
Output:
75 
hello null

Bây giờ hãy gọi hàm printer mà không xác định optional parameters nào.

Dart
printer(num n,{String s1, String s2}) { 
  print(n); 
  print(s1); 
  print(s2);
}

main() {
  printer(75);
}
Bash
Output:
75 
null 
null

Khi chúng ta không chỉ định một tham số tùy chọn nào thì Dart cho ra output là null

Cuối cùng, hãy gọi hàm printer trong khi xác định tất cả các optional parameters.

Dart
printer(num n,{String s1, String s2}) { 
  print(n); 
  print(s1); 
  print(s2);
}

main() {
  printer(75, s1: 'hello', s2: 'there');
}
Bash
Output:
75 
hello 
there

Positional Parameters

Positional Parameters

Khi gọi một hàm, chúng ta truyền một optional positional parameter  giống như chúng ta thực hiện một required parameter. Trong ví dụ dưới đây, chúng ta có một required parameter và optional parameter.

Dart
String mysteryMessage(String who, [String what, String where]){
  var message = '$who';
  if(what != null && where == null){
    message = '$message said $what';
  } else if (where != null){
    message = '$message said $what at $where';
  }
  return message;
}

Trong đoạn mã ở trên, giá trị trả về của chúng ta sẽ khác nếu optional parameter được chuyển trong khi gọi hàm. Mục đích của dòng if(what != null && where == null){ là để kiểm tra xem optional parameter đã được chỉ định hay chưa.

Hãy gọi mysteryMessage và chỉ xác định optional parameter đầu tiên.

Dart
String mysteryMessage(String who, [String what, String where]){
  var message = '$who';
  if(what != null && where == null){
    message = '$message said $what';
  } else if (where != null){
    message = '$message said $what at $where';
  }
  return message;
}

main() {
  var result = mysteryMessage('Billy', 'howdy');
  print(result);
}
Bash
Output:
Billy 
said 
howdy

Bây giờ, hãy gọi mysteryMessage trong khi chỉ định cả hai optional parameters.

Dart
String mysteryMessage(String who, [String what, String where]){
  var message = '$who';
  if(what != null && where == null){
    message = '$message said $what';
  } else if (where != null){
    message = '$message said $what at $where';
  }
  return message;
}

main() {
  var result = mysteryMessage('Billy', 'howdy', 'the ranch');
  print(result);
}
Bash
Output:
Billy said howdy at the ranch

Một trong những điểm khác biệt chính giữa các positional parameters và named parameters nằm ở chỗ bạn phải sử dụng các positional parameters theo thứ tự chúng được khai báo.

Trong ví dụ trên, chúng ta không thể sử dụng where mà không sử dụng what. Tuy nhiên, đây không phải là điều cần quan tâm đối với các named parameters.

4. Hàm đệ quy

Hàm đệ quy là các hàm tự gọi lại nội dung hàm của chính nó. Điều này có vẻ nghe hơi lạ lúc này, nhưng hãy xem cách nó hoạt động là bạn sẽ hiểu ngay thôi.

Đệ quy là quá trình chia nhỏ một biểu thức thành các biểu thức nhỏ hơn và nhỏ hơn nữa cho đến khi bạn có thể sử dụng cùng một thuật toán để giải từng biểu thức. Các biểu thức nhỏ hơn là các phiên bản tương tự của biểu thức ban đầu.

Một cách để triển khai các hàm đệ quy là sử dụng câu lệnh if-else. if đại diện cho trường hợp cơ sở (là biểu thức nhỏ nhất có thể có mà một thuật toán sẽ chạy) và else đại diện cho cuộc gọi đệ quy; khi một hàm gọi chính nó, nó được gọi là một cuộc gọi đệ quy.

Hàm đệ quy sẽ tiếp tục gọi chính nó theo cách lồng nhau mà không kết thúc cho đến khi nó tương đương với trường hợp cơ sở, lúc đó thuật toán sẽ được áp dụng. Các lệnh gọi hàm sẽ tiếp tục thực hiện lệnh đi ngược ra lại, chấm dứt từng cuộc gọi hàm bằng cách sử dụng kết quả bên trong mỗi cuộc gọi hàm đó. Cứ vậy cho đến khi chúng đạt đến lệnh gọi hàm ban đầu.

Hãy hình dung cuộc gọi đệ quy như là ví dụ dưới đây.

Cuộc gọi đệ quy

Việc thực hiện các thừa số là một bài toán đệ quy rất nổi tiếng. Giai thừa của một số đạt được bằng cách nhân tất cả các số liên tiếp bắt đầu với 1 cho đến khi bạn đạt được số được đề cập. Vì vậy, giai thừa của 4 (được biểu diễn là 4!) Tương đương với 1 x 2 x 3 x 4 = 24. Theo đệ quy, nó sẽ giống như 4! = 4 x (3!).

Trước khi bắt đầu viết mã, chúng ta hãy nhìn nhận vấn đề từ một góc độ khác.

Một cách đơn giản để hiểu về đệ quy là bạn hãy hình dung cách mình trả lời một câu hỏi gì đó mà giống như không trả lời vậy. Là như thế này, ai đó hỏi bạn giai thừa của 5 là bao nhiêu? Bạn trả lời nó gấp 5 lần giai thừa của 4. Trả lời cũng như không đúng không? rồi người đó tiếp tục hỏi bạn giai thừa của 4 là bao nhiêu. Câu trả lời là gấp 4 lần giai thừa của 3. Cứ tiếp tục hỏi và trả lời như vậy cho đến khi tới giai thừa của 1. Tới đây thì quá dễ dàng rồi đúng không 1! = 1 (đây là trường hợp cơ sở trong đệ quy). Lúc này bạn sẽ lấy kết quả và lần lượt ghép vào các câu trả lời trước. Cuối cùng bạn sẽ nhận được kết quả của câu hỏi đầu tiên (giai thừa của 5 là bao nhiêu?)

Xem cách thực hiện bài toán giai thừa trong ngôn ngữ Dart:

Dart
int factorial(int x) {
  if (x == 1) { // Base Case
    return 1;
  } else {
    return x*factorial(x-1); // Recursive Call
  }
}

main() {
  // Driver Code
  var result = factorial(5);
  print(result);
}
Bash
Output:
120

factorial là một hàm đệ quy nhận một tham số (số có giai thừa được tính). Trường hợp cơ sở là số đó bằng 1. Trong trường hợp đó, tất cả những gì chúng ta phải làm là trả về 1 vì đó là giai thừa của chính nó (dòng if (x == 1) { // Base Case,  return 1;). Chúng ta không thể chia nhỏ biểu thức này hơn nữa.

Nếu số không bằng 1, biểu thức else sẽ được thực thi trong đó giá trị trả về là số được nhân với giai thừa của số trước nó (dòng } else {,  return x*factorial(x-1); // Recursive Call ). Điều này sẽ tiếp tục cho đến khi chúng ta chuyển tới 1 trong cuộc gọi đệ quy của mình. Hãy xem cách triển khai về cách hoạt động của nó.

giai thừa - đệ quy

Bạn có thấy kết quả của một lệnh gọi hàm bên trong được sử dụng như thế nào để kết thúc lệnh gọi hàm bên ngoài của nó không?

Đệ quy là một khái niệm khó hiểu, nhưng một khi bạn đã làm được, nó là một công cụ cực kỳ mạnh mẽ mà bạn có thể sử dụng trong một ngôn ngữ lập trình hàm.

5. Higher-Order Functions

Dart là một ngôn ngữ hướng đối tượng, do đó các hàm cũng là đối tượng và có kiểu Function. Các hàm được coi là những giá trị first-class. Điều này có nghĩa là giống như bất kỳ giá trị nào khác, một hàm có thể được gán cho các biến, được truyền như tham số đến một hàm khác và cũng có thể được trả về dưới dạng kết quả.

Higher-Order Functions

Các hàm có thể nhận các hàm khác làm tham số hoặc có thể trả về kết quả là các hàm thì được gọi là Higher-Order Functions.

Function là một kiểu giống như num hoặc List, chúng ta có thể tạo các hàm có các tham số kiểu Function. Điều này có nghĩa là khi gọi hàm đã tạo đó, bạn phải chuyển cho nó một hàm khác làm tham số.

Hãy tạo một hàm forAll . Nó lấy một danh sách và một hàm khác làm tham số f. forAll thực hiện chức năng của f trên mọi phần tử trong danh sách được cung cấp. forAll trả về một danh sách mới với các phần tử đã sửa đổi.

Dart
List<int> forAll(Function f, List<int> intList){
  var newList = List<int>();
  for(var i = 0; i < intList.length; i ++){
    newList.add(f(intList[i]));
  }
  return newList;
}

forAll trả về một danh sách, chúng ta đã chỉ định kiểu trả về của nó là List<int>. newList là danh sách được trả về ở cuối hàm. Giá trị của mọi phần tử trong intList được sửa đổi theo chức năng của f và giá trị đã sửa đổi được lưu trữ trong newList (dòng newList.add(f(intList[i]));).

Chúng ta sẽ gọi cho forAll trong đoạn mã bên dưới. Hàm chúng ta sẽ truyền dưới dạng tham số sẽ là hàm factorial đã tạo trong bài học trước.

Dart
List<int> forAll(Function f, List<int> intList){
  var newList = List<int>();
  for(var i = 0; i < intList.length; i ++){
    newList.add(f(intList[i]));
  }
  return newList;
}

// Recursive factorial function
int factorial(int x) {
  if (x == 1) {
    return 1;
  } else {
    return x*factorial(x-1);
  }
}

main() {
  var tester = [1,2,3];
  var result = forAll(factorial, tester);
  print(tester);
  print(result);
}
Bash
Output:
[1, 2, 3]
[1, 2, 6]

Danh sách số nguyên đầu tiên được hiển thị dưới dạng đầu ra là danh sách ban đầu được cung cấp cho forAll (dòng var tester = [1,2,3];). Danh sách số nguyên thứ hai là danh sách các phần tử đã sửa đổi được trả về bởi forAll (dòng var result = forAll(factorial, tester);).

Phương thức forEach

Dart có một phương thức tích hợp sẵn đó là forEach, có chức năng tương tự như hàm forAll của chúng ta. forEach có một tham số duy nhất của kiểu Function và được gọi trên một kiểu collection. Chức năng của hàm được truyền cho forEach được áp dụng cho mọi phần tử mà forEach được gọi. Tuy nhiên, hàm được truyền qua không được có giá trị trả về, tức là nó phải là void.

Trong đoạn code bên dưới, chúng ta đang chuyển phương thức print cho forEach và gọi nó trên danh sách các số nguyên.

Dart
main() {
  var tester = [1,2,3];
  tester.forEach(print);
}
Bash
Output:
1 2 3

6. Anonymous Functions (Hàm ẩn)

Thỉnh thoảng chúng ta chỉ cần sử dụng các hàm một lần, hoặc tạm thời; chỉ cần chức năng của các hàm đó cho một trường hợp duy nhất thôi. Cho nên việc đặt tên cho chúng là một việc bổ sung không cần thiết.

Dart cung cấp một giải pháp được gọi là hàm ẩn.

Syntax như sau:

Anonymous Functions

Vì vậy, nếu chúng ta muốn viết một hàm ẩn trả về lập phương của một số, nó sẽ giống như sau:

Anonymous Functions

Ở đây, x là tham số chưa nhập của hàm ẩn danh và x * x * x là nội dung của hàm.

Ví dụ:

Dart
main() {
  var list = [1,2,3];
  list.forEach((item) {
   print(item*item*item);
  });
}
Bash
Output:
1 
8 
27

Hãy chuyển hàm lập phương ẩn của chúng ta sang phương thức tích hợp sẵn forEach. Mục tiêu của chúng ta là lấy lập phương của mọi phần tử trong list.

Hàm ẩn được gọi cho từng phần tử trong danh sách, list. Nếu hàm chỉ chứa một câu lệnh trong nội dung của hàm, bạn có thể rút ngắn nó bằng cách sử dụng ký hiệu mũi tên như đã thảo luận trong bài học trước.

Dart
main() {
  var list = [1,2,3];
  list.forEach(
    (item) => print(item*item*item));
}
Bash
Output:
1 
8 
27

7. Nested Functions

Nói một cách đơn giản, nested functions là các hàm được định nghĩa bên trong một hàm khác. Khi chúng ta tạo các hàm trong hàm main(), chúng ta đang tận dụng các hàm lồng nhau vì main() là một hàm riêng của nó. Hãy xem một ví dụ đơn giản dưới đây.

Dart
void outerFunction(){
    print("Outer Function");
    void nestedFunction(){
      print("Nested Function");
    }
    nestedFunction();
}

main() {
  outerFunction();
}
Bash
Output:
Outer Function 
Nested Function

Chúng ta đang khai báo một hàm outerFunction đang in câu lệnh Outer Function. Nội dung của outerFunction cũng chứa một khai báo hàm cho nestedFunction. nestedFunction in câu lệnh Nested Function.

Phần nội dung hàm của outerFunction kết thúc bằng một lệnh gọi hàm đến nestedFunction.

Trong hàm main(), chúng ta đang gọi hàm outerFunction, hàm này sẽ in ra cả hai câu lệnh.

Nếu chúng ta gọi nestedFunction  trong main() chứ không phải là outerFunction, chúng ta sẽ gặp lỗi vì nestedFunction nằm ngoài phạm vi của main().

8. Scope

Lexical scope là phạm vi chức năng của một biến để nó chỉ có thể được gọi từ trong block code mà nó được định nghĩa.

Dart là một ngôn ngữ có Lexical scope, có nghĩa là phạm vi của các biến được xác định một cách static

Mỗi tập hợp các dấu ngoặc nhọn ({}) có được phạm vi mới của riêng nó trong khi kế thừa từ phạm vi mà nó đã được khai báo. Với lexical scope, phạm vi con sẽ truy cập vào biến cùng tên được khai báo gần đây nhất.

Nếu chúng ta định nghĩa một biến bên ngoài một hàm, chúng ta cũng có thể định nghĩa một biến có cùng tên bên trong hàm mà không ảnh hưởng đến biến bên ngoài. Hãy xem một ví dụ dưới đây:

Dart
int square(int x){
  return x * x;
}

main() {
  var amIVisible = 0;

  void result() {
    var amIVisible = square(3);
    print("Variable Inside Block: $amIVisible");
  }

  result();
  print("Variable Outside Block: $amIVisible");
} 
Bash
Output:
Variable Inside Block: 9 
Variable Outside Block: 0

Dù ta in 2 biến cùng tên amIVisible , nhưng biến thực sự khác nhau nên ta nhận được hai kết quả khác nhau.

Trong bài viết này, chúng ta đã đi qua các kiến thức về ham. Hãy sang bài viết tiếp theo để tìm hiểu về classes nhé!

Bài viết liên quan

Lập trình backend expressjs

xây dựng hệ thống microservices
  • Kiến trúc Hexagonal và ứng dụngal font-
  • TypeScript: OOP và nguyên lý SOLIDal font-
  • Event-Driven Architecture, Queue & PubSubal font-
  • Basic scalable System Designal font-

Đăng ký nhận thông báo

Đừng bỏ lỡ những bài viết thú vị từ 200Lab