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.
void newPrint(){
print("Function Called");
}
// Driver Code
main() {
newPrint();
}
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:
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.
num sum(num x, num y){
return x+y;
}
// Driver Code
main() {
print(sum(1,2));
}
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:
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é:
num sum(num x, num y) => x+y;
// Driver Code
main() {
print(sum(1,2));
}
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.
// 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);
}
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.
num square(num x) {
return x * x;
}
main() {
// Driver Code
var result = square(5);
print(result);
}
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.
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
.
// 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);
}
Output:
29
3. Optional Parameters
Một hàm có thể có hai kiểu tham số: required parameter và optional 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 {}
.
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.
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.
printer(num n,{String s1, String s2}) {
print(n);
print(s1);
print(s2);
}
main() {
printer(75, s1: 'hello');
}
Output:
75
hello null
Bây giờ hãy gọi hàm printer
mà không xác định optional parameters nào.
printer(num n,{String s1, String s2}) {
print(n);
print(s1);
print(s2);
}
main() {
printer(75);
}
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.
printer(num n,{String s1, String s2}) {
print(n);
print(s1);
print(s2);
}
main() {
printer(75, s1: 'hello', s2: 'there');
}
Output:
75
hello
there
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.
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.
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);
}
Output:
Billy
said
howdy
Bây giờ, hãy gọi mysteryMessage
trong khi chỉ định cả hai optional parameters.
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);
}
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.
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:
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);
}
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ó.
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ả.
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.
Vì 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.
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;
}
Vì 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.
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);
}
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.
main() {
var tester = [1,2,3];
tester.forEach(print);
}
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:
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:
Ở đâ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ụ:
main() {
var list = [1,2,3];
list.forEach((item) {
print(item*item*item);
});
}
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.
main() {
var list = [1,2,3];
list.forEach(
(item) => print(item*item*item));
}
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.
void outerFunction(){
print("Outer Function");
void nestedFunction(){
print("Nested Function");
}
nestedFunction();
}
main() {
outerFunction();
}
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:
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");
}
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
Tự học Dart: Các Dart Operators (toán tử) bạn cần biết
Sep 02, 2023 • 18 min read
Flutter cơ bản: Điều cần biết khi lập trình ứng dụng đầu tiên
Aug 23, 2023 • 13 min read
Dart là gì? Giới thiệu cơ bản về ngôn ngữ lập trình Dart
Aug 21, 2023 • 11 min read
Flutter là gì? Vì sao nên học công cụ lập trình Flutter?
Aug 19, 2023 • 11 min read
Flutter cơ bản: Widget Tree, Element Tree & Render Tree
Aug 19, 2023 • 10 min read
So sánh StatelessWidget và StatefulWidget
Jul 17, 2022 • 12 min read