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:
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:
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ụ:
Lưu ý:
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]
:
Lưu ý:
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:
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}
Lưu ý:
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ụ:
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}
Lưu ý:
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ụ:
Tổng kết ngắn gọn
1. Required positional Parameters
kiểuDữLiệu tênThamSố
. Ví dụ: int age, String name
int age = 18
❌, int age
✅
functionName(param1Value, param2Value);
functionName(param2Value, param1Value);
❌
2. Optional positional Parameters
[]
, ví dụ:
[String? name, int age = 18]
functionName(param1Value, param2Value);
functionName(param2Value, param1Value);
❌
3. Required named Parameters
{}
và thêm chú thích
required
vào trước kiểu dữ liệu. Ví dụ:
{required String name, required int age}
{required int age = 18}
❌,
{required int age}
✅
functionName(name: "Linh", age: 18);
functionName(name: "Linh", age: 18);
✅functionName(age: 18, name: "Linh");
✅
4. Optional named Parameters
{}
, ví dụ:
{String name = "", int age = 18, int? height}
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à:
// 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.
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:
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
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
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ả
}