1. Giới thiệu
Trong lớp học lập trình Sử dụng các kiểu hàm và biểu thức lambda trong Kotlin, bạn đã tìm hiểu các hàm bậc cao hơn. Đây là các hàm nhận các hàm khác làm tham số và/hoặc trả về một hàm, chẳng hạn như repeat()
. Các hàm bậc cao hơn chuyên dùng cho bộ sưu tập vì chúng giúp bạn thực hiện các tác vụ phổ biến (chẳng hạn như sắp xếp hoặc lọc) và cần lập trình ít hơn. Giờ thì khi bạn đã có một nền tảng vững chắc để làm việc với các bộ sưu tập, đã đến lúc xem lại các hàm bậc cao hơn.
Ở lớp học lập trình này, bạn sẽ tìm hiểu về các hàm được dùng trên các kiểu bộ sưu tập, bao gồm forEach()
, map()
, filter()
, groupBy()
, fold()
và sortedBy()
. Trong quá trình này, bạn sẽ được thực hành thêm khi sử dụng biểu thức lambda.
Điều kiện tiên quyết
- Làm quen với các loại hàm và biểu thức lambda.
- Làm quen với cú pháp trailing lambda, chẳng hạn như với hàm
repeat()
. - Kiến thức về nhiều loại bộ sưu tập trong Kotlin, chẳng hạn như
List
.
Kiến thức bạn sẽ học được
- Cách nhúng biểu thức lambda vào các chuỗi.
- Cách sử dụng các hàm bậc cao hơn với bộ sưu tập
List
, bao gồmforEach()
,map()
,filter()
,groupBy()
,fold()
vàsortedBy()
.
Bạn cần có
- Một trình duyệt web có quyền truy cập vào Kotlin Playground.
2. forEach() và các mẫu chuỗi với các hàm lambda
Mã bắt đầu
Trong các ví dụ sau, bạn sẽ lấy List
đại diện cho trình đơn bánh quy của một tiệm bánh (ngon tuyệt vời!) và sử dụng các hàm bậc cao hơn để định dạng trình đơn theo nhiều cách khác nhau.
Hãy bắt đầu bằng cách thiết lập mã ban đầu.
- Chuyển đến phần Kotlin Playground.
- Ở phía trên hàm
main()
, hãy thêm lớpCookie
. Mỗi bản sao củaCookie
đại diện cho một mục trên trình đơn, vớiname
,price
và các thông tin khác về bánh quy.
class Cookie(
val name: String,
val softBaked: Boolean,
val hasFilling: Boolean,
val price: Double
)
fun main() {
}
- Ở dưới lớp
Cookie
, bên ngoàimain()
, hãy tạo một danh sách các loại bánh quy như minh hoạ. Loại này được dự đoán làList<Cookie>
.
class Cookie(
val name: String,
val softBaked: Boolean,
val hasFilling: Boolean,
val price: Double
)
val cookies = listOf(
Cookie(
name = "Chocolate Chip",
softBaked = false,
hasFilling = false,
price = 1.69
),
Cookie(
name = "Banana Walnut",
softBaked = true,
hasFilling = false,
price = 1.49
),
Cookie(
name = "Vanilla Creme",
softBaked = false,
hasFilling = true,
price = 1.59
),
Cookie(
name = "Chocolate Peanut Butter",
softBaked = false,
hasFilling = true,
price = 1.49
),
Cookie(
name = "Snickerdoodle",
softBaked = true,
hasFilling = false,
price = 1.39
),
Cookie(
name = "Blueberry Tart",
softBaked = true,
hasFilling = true,
price = 1.79
),
Cookie(
name = "Sugar and Sprinkles",
softBaked = false,
hasFilling = false,
price = 1.39
)
)
fun main() {
}
Lặp lại danh sách với forEach()
Hàm bậc cao nhất đầu tiên mà bạn tìm hiểu là hàm forEach()
. Hàm forEach()
thực thi hàm được truyền ở dạng tham số một lần cho mỗi mục trong bộ sưu tập. Hàm này hoạt động tương tự như hàm repeat()
hoặc vòng lặp for
. Hàm lambda được thực thi cho thành phần đầu tiên, sau đó đến thành phần thứ hai và cứ thế tiếp tục cho đến khi hàm được thực thi cho từng thành phần trong tập hợp. Chữ ký phương thức sẽ có dạng như sau:
forEach(action: (T) -> Unit)
forEach()
nhận một tham số hành động duy nhất – hàm thuộc loại (T) -> Unit
.
T
tương ứng với bất kỳ loại dữ liệu nào có trong bộ sưu tập. Vì hàm lambda nhận được một tham số duy nhất nên bạn có thể bỏ qua tên gọi và tham chiếu đến tham số đó bằng it
.
Dùng hàm forEach()
để in các mục trong danh sách cookies
.
- Trong
main()
, hãy gọiforEach()
trên danh sáchcookies
, bằng cách sử dụng cú pháp trailing lambda. Vì hàm trailing lambda là đối số duy nhất nên bạn có thể loại bỏ dấu ngoặc đơn khi gọi hàm.
fun main() {
cookies.forEach {
}
}
- Trong phần nội dung hàm lambda, hãy thêm một câu lệnh
println()
để init
.
fun main() {
cookies.forEach {
println("Menu item: $it")
}
}
- Chạy đoạn mã rồi quan sát kết quả. Tất cả dòng in ra đều là tên của kiểu (
Cookie
) và là giá trị nhận dạng duy nhất của đối tượng chứ không phải nội dung của lớp.
Menu item: Cookie@5a10411 Menu item: Cookie@68de145 Menu item: Cookie@27fa135a Menu item: Cookie@46f7f36a Menu item: Cookie@421faab1 Menu item: Cookie@2b71fc7e Menu item: Cookie@5ce65a89
Nhúng biểu thức trong chuỗi
Khi lần đầu làm quen với các mẫu chuỗi, bạn đã thấy cách ký hiệu đô la ($
) được sử dụng với tên biến để chèn vào một chuỗi. Tuy nhiên, chức năng này không hoạt động như mong đợi khi kết hợp với toán tử dấu chấm (.
) để truy cập vào các thuộc tính.
- Trong lệnh gọi đến
forEach()
, hãy sửa đổi nội dung hàm lambda để chèn$it.name
vào chuỗi.
cookies.forEach {
println("Menu item: $it.name")
}
- Chạy đoạn mã của bạn. Xin lưu ý rằng thao tác này sẽ chèn tên của lớp (
Cookie
) và giá trị nhận dạng duy nhất của đối tượng theo sau là.name
. Không truy cập được vào giá trị của thuộc tínhname
.
Menu item: Cookie@5a10411.name Menu item: Cookie@68de145.name Menu item: Cookie@27fa135a.name Menu item: Cookie@46f7f36a.name Menu item: Cookie@421faab1.name Menu item: Cookie@2b71fc7e.name Menu item: Cookie@5ce65a89.name
Để truy cập vào các thuộc tính và nhúng chúng vào một chuỗi, bạn cần có một biểu thức. Bạn có thể tạo một phần biểu thức của chuỗi mẫu bằng cách đặt đối tượng đó trong dấu ngoặc nhọn.
Biểu thức lambda được đặt giữa dấu ngoặc nhọn mở và đóng. Bạn có thể truy cập vào thuộc tính, thực hiện các phép toán tử, gọi hàm, v.v. và trả về giá trị của hàm lambda được chèn vào chuỗi.
Vui lòng sửa đổi mã để tên được chèn vào chuỗi.
- Đặt
it.name
trong dấu ngoặc nhọn để tạo biểu thức lambda.
cookies.forEach {
println("Menu item: ${it.name}")
}
- Chạy đoạn mã của bạn. Đầu ra chứa
name
của từngCookie
.
Menu item: Chocolate Chip Menu item: Banana Walnut Menu item: Vanilla Creme Menu item: Chocolate Peanut Butter Menu item: Snickerdoodle Menu item: Blueberry Tart Menu item: Sugar and Sprinkles
3. map()
Hàm map()
cho phép bạn chuyển đổi một bộ sưu tập thành bộ sưu tập mới với cùng số lượng phần tử. Ví dụ: map()
có thể chuyển đổi List<Cookie>
thành List<String>
chỉ chứa name
của bánh quy, miễn là bạn cho hàm map()
biết cách tạo String
từ mỗi mục Cookie
.
Giả sử bạn đang viết một ứng dụng cho thấy thực đơn tương tác được của một tiệm bánh. Khi người dùng chuyển đến màn hình hiển thị thực đơn bánh quy, họ có thể muốn xem dữ liệu được trình bày theo logic, chẳng hạn như tên bánh và theo sau là giá. Bạn có thể tạo danh sách các chuỗi, được định dạng bằng dữ liệu liên quan (tên và giá), bằng cách sử dụng hàm map()
.
- Xoá tất cả mã trước đó khỏi
main()
. Tạo một biến mới có tên làfullMenu
và đặt biến đó bằng với kết quả của lệnh gọimap()
trên danh sáchcookies
.
val fullMenu = cookies.map {
}
- Trong phần nội dung của hàm lambda, hãy thêm một chuỗi được định dạng để đưa
name
vàprice
củait
vào.
val fullMenu = cookies.map {
"${it.name} - $${it.price}"
}
- In nội dung của
fullMenu
. Bạn có thể thực hiện việc này bằng cách sử dụngforEach()
. Bộ sưu tậpfullMenu
domap()
trả về có loạiList<String>
thay vìList<Cookie>
. MỗiCookie
trongcookies
tương ứng với mộtString
trongfullMenu
.
println("Full menu:")
fullMenu.forEach {
println(it)
}
- Chạy đoạn mã. Kết quả khớp với nội dung của danh sách
fullMenu
.
Full menu: Chocolate Chip - $1.69 Banana Walnut - $1.49 Vanilla Creme - $1.59 Chocolate Peanut Butter - $1.49 Snickerdoodle - $1.39 Blueberry Tart - $1.79 Sugar and Sprinkles - $1.39
4. filter()
Hàm filter()
cho phép bạn tạo một tập hợp con của bộ sưu tập. Ví dụ: nếu có một danh sách số, bạn có thể dùng filter()
để tạo một danh sách mới chỉ chứa các số chia hết cho 2.
Trong khi kết quả của hàm map()
luôn tạo ra một tập hợp có cùng quy mô, thì filter()
sẽ tạo ra một tập hợp có cùng quy mô hoặc nhỏ hơn tập hợp ban đầu. Không giống như map()
, bộ sưu tập thu được cũng có cùng một loại dữ liệu, vì vậy việc lọc List<Cookie>
sẽ dẫn đến một List<Cookie>
khác.
Giống như map()
và forEach()
, filter()
nhận một biểu thức lambda duy nhất làm tham số. Hàm lambda chứa một tham số duy nhất đại diện cho mỗi mục trong bộ sưu tập và trả về một giá trị Boolean
.
Đối với từng mục trong bộ sưu tập:
- Nếu kết quả của biểu thức lambda là
true
, mục này sẽ được đưa vào tập hợp mới. - Nếu kết quả là
false
, mục này sẽ không xuất hiện trong bộ sưu tập mới.
Điều này rất hữu ích nếu bạn muốn lấy một tập hợp con của dữ liệu trong ứng dụng. Ví dụ: giả sử tiệm bánh muốn làm nổi bật các loại bánh quy nướng mềm của mình trong một mục riêng của thực đơn. Trước tiên, bạn có thể filter()
danh sách cookies
trước khi in các mục.
- Trong
main()
, hãy tạo một biến mới có tên làsoftBakedMenu
và đặt biến này thành kết quả của lệnh gọifilter()
trên danh sáchcookies
.
val softBakedMenu = cookies.filter {
}
- Trong phần nội dung của hàm lambda, hãy thêm một biểu thức boolean để kiểm tra xem liệu thuộc tính
softBaked
của bánh quy có bằngtrue
hay không. Vì bản thânsoftBaked
là mộtBoolean
nên phần nội dung lambda chỉ cần chứait.softBaked
.
val softBakedMenu = cookies.filter {
it.softBaked
}
- In nội dung của
softBakedMenu
bằng cách dùngforEach()
.
println("Soft cookies:")
softBakedMenu.forEach {
println("${it.name} - $${it.price}")
}
- Chạy đoạn mã. Trình đơn được in như trước, nhưng chỉ đưa vào các loại bánh quy nướng mềm.
... Soft cookies: Banana Walnut - $1.49 Snickerdoodle - $1.39 Blueberry Tart - $1.79
5. groupBy()
Hàm groupBy()
có thể dựa vào một hàm để biến danh sách thành bản đồ. Mỗi giá trị trả về duy nhất của hàm sẽ trở thành một khoá trong bản đồ thu được. Giá trị cho mỗi khoá là tất cả các mục trong tập hợp đã tạo ra giá trị trả về duy nhất đó.
Kiểu dữ liệu của các khoá giống với kiểu dữ liệu trả về của hàm được truyền vào groupBy()
. Loại dữ liệu của các giá trị sẽ là danh sách các mục từ danh sách ban đầu.
Thật khó để có thể đưa ra một khái niệm, vì vậy chúng ta sẽ bắt đầu bằng một ví dụ đơn giản. Với cùng một danh sách các số như trước đây, hãy nhóm các số đó thành số lẻ hoặc chẵn.
Bạn có thể phân biệt một số là số lẻ hay số chẵn bằng cách chia số đó cho 2
và kiểm tra xem số dư là 0
hay 1
. Nếu số dư là 0
thì đó là một số chẵn. Còn nếu số dư là 1
thì số này là một số lẻ.
Bạn có thể thực hiện việc này bằng toán tử modulo (%
). Toán tử modulo chia số bị chia ở bên trái của một biểu thức cho số chia ở bên phải.
Thay vì trả về kết quả của phép chia, toán tử modulo sẽ trả về một số dư giống như toán tử chia (/
). Điều này rất hữu ích cho việc kiểm tra xem một số là số chẵn hay số lẻ.
Hàm groupBy()
được gọi bằng biểu thức lambda sau: { it % 2 }
.
Bản đồ thu được có hai khoá: 0
và 1
. Mỗi khoá có giá trị thuộc loại List<Int>
. Danh sách khoá 0
chứa tất cả các số chẵn, còn danh sách khoá 1
chứa tất cả các số lẻ.
Trên thực tế, có thể là ứng dụng hình ảnh nhóm các ảnh theo chủ đề hoặc vị trí chụp ảnh. Đối với trình đơn bánh trong ví dụ của chúng ta, hãy nhóm trình đơn theo phân loại bánh quy có được nướng mềm hay không.
Sử dụng groupBy()
để nhóm trình đơn dựa vào thuộc tính softBaked
.
- Xoá lệnh gọi tới
filter()
khỏi bước trước đó.
Mã cần xoá
val softBakedMenu = cookies.filter {
it.softBaked
}
println("Soft cookies:")
softBakedMenu.forEach {
println("${it.name} - $${it.price}")
}
- Gọi
groupBy()
trên danh sáchcookies
, lưu trữ kết quả trong biến có têngroupedMenu
.
val groupedMenu = cookies.groupBy {}
- Truyền vào biểu thức lambda trả về
it.softBaked
. Loại dữ liệu trả về sẽ làMap<Boolean, List<Cookie>>
.
val groupedMenu = cookies.groupBy { it.softBaked }
- Tạo biến
softBakedMenu
chứa giá trịgroupedMenu[true]
và biếncrunchyMenu
chứa giá trịgroupedMenu[false]
. Vì kết quả của chỉ sốMap
có thể là giá trị null (rỗng) nên bạn có thể dùng toán tử Elvis (?:
) để trả về một danh sách trống.
val softBakedMenu = groupedMenu[true] ?: listOf()
val crunchyMenu = groupedMenu[false] ?: listOf()
- Thêm mã để in trình đơn cho bánh quy mềm, theo sau là trình đơn cho bánh quy giòn.
println("Soft cookies:")
softBakedMenu.forEach {
println("${it.name} - $${it.price}")
}
println("Crunchy cookies:")
crunchyMenu.forEach {
println("${it.name} - $${it.price}")
}
- Chạy đoạn mã. Bạn có thể tách thành 2 danh sách bằng cách sử dụng hàm
groupBy()
, dựa trên giá trị của một trong các thuộc tính.
... Soft cookies: Banana Walnut - $1.49 Snickerdoodle - $1.39 Blueberry Tart - $1.79 Crunchy cookies: Chocolate Chip - $1.69 Vanilla Creme - $1.59 Chocolate Peanut Butter - $1.49 Sugar and Sprinkles - $1.39
6. fold()
Hàm fold()
dùng để tạo một giá trị duy nhất từ bộ sưu tập. Hàm này thường dùng trong những trường hợp cần tính giá tổng hoặc tổng hợp tất cả phần tử trong danh sách để tìm giá trị trung bình.
Hàm fold()
nhận 2 tham số:
- Giá trị ban đầu. Loại dữ liệu được dự đoán khi gọi hàm (nghĩa là giá trị ban đầu của
0
được suy luận thànhInt
). - Một biểu thức lambda trả về giá trị có cùng loại với giá trị ban đầu.
Biểu thức lambda cũng có 2 tham số:
- Tham số đầu tiên là tham số tích lũy. Nó có cùng loại dữ liệu với giá trị ban đầu. Hãy xem đây là tổng các giá trị đang chạy. Mỗi lần biểu thức lambda được gọi, tham số tích luỹ bằng với giá trị trả về từ lần trước khi hàm lambda được gọi.
- Tham số thứ hai cùng loại với từng phần tử trong bộ sưu tập.
Giống như các hàm khác mà bạn đã thấy, biểu thức lambda được gọi cho mỗi phần tử trong một tập hợp, vì vậy, bạn có thể dùng fold()
như một cách nhanh gọn để tổng hợp tất cả các phần tử.
Hãy dùng fold()
để tính giá tổng của tất cả các loại bánh quy.
- Trong
main()
, hãy tạo một biến mới có tên làtotalPrice
và đặt biến đó bằng với kết quả của lệnh gọifold()
trên danh sáchcookies
. Truyền vào0.0
cho giá trị ban đầu. Loại của giá trị được dự đoán làDouble
.
val totalPrice = cookies.fold(0.0) {
}
- Bạn cần chỉ định cả hai tham số cho biểu thức lambda. Dùng
total
cho hàm tích lũy vàcookie
cho phần tử của bộ sưu tập. Sử dụng mũi tên (->
) sau danh sách tham số.
val totalPrice = cookies.fold(0.0) {total, cookie ->
}
- Ở phần nội dung của hàm lambda, hãy tính tổng của
total
vàcookie.price
. Giá trị này được dự đoán là giá trị trả về và được truyền vàototal
trong lần gọi hàm lambda tiếp theo.
val totalPrice = cookies.fold(0.0) {total, cookie ->
total + cookie.price
}
- In giá trị của
totalPrice
được định dạng dưới dạng chuỗi để dễ đọc.
println("Total price: $${totalPrice}")
- Chạy đoạn mã. Kết quả phải bằng tổng giá trong danh sách
cookies
.
... Total price: $10.83
7. sortedBy()
Khi tìm hiểu về bộ sưu tập, bạn hẳn đã biết có thể dùng hàm sort()
để sắp xếp các phần tử. Tuy nhiên, hàm này sẽ không hoạt động trên tập hợp các đối tượng Cookie
. Lớp Cookie
có một vài thuộc tính và Kotlin sẽ không biết bạn muốn phân loại theo thuộc tính nào (name
, price
, v.v.).
Đối với những trường hợp này, bộ sưu tập Kotlin sẽ cung cấp hàm sortedBy()
. sortedBy()
cho phép bạn chỉ định một hàm lambda trả về thuộc tính mà bạn muốn sử dụng để sắp xếp phân loại. Chẳng hạn nếu bạn muốn phân loại theo price
, hàm lambda sẽ trả về it.price
. Miễn là kiểu dữ liệu của giá trị có thứ tự sắp xếp thông thường — các chuỗi được sắp xếp theo thứ tự bảng chữ cái, các giá trị số được sắp xếp theo thứ tự tăng dần — thì chuỗi sẽ được sắp xếp giống như một tập hợp của kiểu đó.
Bạn sẽ sử dụng sortedBy()
để sắp xếp danh sách bánh quy theo thứ tự bảng chữ cái.
- Trong
main()
sau mã hiện có, hãy thêm một biến mới có tênalphabeticalMenu
và đặt biến đó bằng với lệnh gọisortedBy()
trên danh sáchcookies
.
val alphabeticalMenu = cookies.sortedBy {
}
- Trong biểu thức lambda, hãy trả về
it.name
. Danh sách thu được sẽ vẫn thuộc loạiList<Cookie>
nhưng được sắp xếp theoname
.
val alphabeticalMenu = cookies.sortedBy {
it.name
}
- In tên các loại bánh quy trong
alphabeticalMenu
. Bạn có thể dùngforEach()
để in từng tên trên một dòng mới.
println("Alphabetical menu:")
alphabeticalMenu.forEach {
println(it.name)
}
- Chạy đoạn mã của bạn. Tên bánh quy được in theo thứ tự bảng chữ cái.
... Alphabetical menu: Banana Walnut Blueberry Tart Chocolate Chip Chocolate Peanut Butter Snickerdoodle Sugar and Sprinkles Vanilla Creme
8. Kết luận
Xin chúc mừng! Bạn vừa xem một số ví dụ về cách sử dụng các hàm bậc cao hơn với bộ sưu tập. Các thao tác phổ biến, chẳng hạn như sắp xếp và lọc, có thể được thực hiện trong một dòng mã giúp chương trình của bạn gọn gẽ và trực quan hơn.
Tóm tắt
- Bạn có thể lặp lại từng phần tử trong một bộ sưu tập bằng
forEach()
. - Có thể chèn biểu thức vào các chuỗi.
map()
được dùng để định dạng các mục trong một bộ sưu tập, thường là dưới dạng tập hợp của một loại dữ liệu khác.filter()
có thể tạo một tập hợp con của bộ sưu tập.groupBy()
phân tách một tập hợp dựa trên giá trị trả về của một hàm.fold()
chuyển bộ sưu tập thành một giá trị duy nhất.sortedBy()
được dùng để sắp xếp một bộ sưu tập theo thuộc tính cụ thể.