Các hàm bậc cao hơn với bộ sưu tập

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()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ồm forEach(), map(), filter(), groupBy(), fold()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.

  1. Chuyển đến phần Kotlin Playground.
  2. Ở phía trên hàm main(), hãy thêm lớp Cookie. Mỗi bản sao của Cookie đại diện cho một mục trên trình đơn, với name, 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() {

}
  1. Ở dưới lớp Cookie, bên ngoài main(), 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.

  1. Trong main(), hãy gọi forEach() trên danh sách cookies, 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 {

    }
}
  1. Trong phần nội dung hàm lambda, hãy thêm một câu lệnh println() để in it.
fun main() {
    cookies.forEach {
        println("Menu item: $it")
    }
}
  1. 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.

  1. 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")
}
  1. 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ính name.
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.

2c008744cee548cc.png

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.

  1. Đặt it.name trong dấu ngoặc nhọn để tạo biểu thức lambda.
cookies.forEach {
    println("Menu item: ${it.name}")
}
  1. Chạy đoạn mã của bạn. Đầu ra chứa name của từng Cookie.
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.

e0605b7b09f91717.png

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().

  1. 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ọi map() trên danh sách cookies.
val fullMenu = cookies.map {

}
  1. Trong phần nội dung của hàm lambda, hãy thêm một chuỗi được định dạng để đưa nameprice của it vào.
val fullMenu = cookies.map {
    "${it.name} - $${it.price}"
}
  1. 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ụng forEach(). Bộ sưu tập fullMenu do map() trả về có loại List<String> thay vì List<Cookie>. Mỗi Cookie trong cookies tương ứng với một String trong fullMenu.
println("Full menu:")
fullMenu.forEach {
    println(it)
}
  1. 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.

d4fd6be7bef37ab3.png

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()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.

  1. 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ọi filter() trên danh sách cookies.
val softBakedMenu = cookies.filter {
}
  1. 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ằng true hay không. Vì bản thân softBaked là một Boolean nên phần nội dung lambda chỉ cần chứa it.softBaked.
val softBakedMenu = cookies.filter {
    it.softBaked
}
  1. In nội dung của softBakedMenu bằng cách dùng forEach().
println("Soft cookies:")
softBakedMenu.forEach {
    println("${it.name} - $${it.price}")
}
  1. 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 đó.

54e190b34d9921c0.png

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.

4c3333da9e5ee352.png

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ẻ.

4219eacdaca33f1d.png

Hàm groupBy() được gọi bằng biểu thức lambda sau: { it % 2 }.

Bản đồ thu được có hai khoá: 01. 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.

  1. 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}")
}
  1. Gọi groupBy() trên danh sách cookies, lưu trữ kết quả trong biến có tên groupedMenu.
val groupedMenu = cookies.groupBy {}
  1. 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 }
  1. Tạo biến softBakedMenu chứa giá trị groupedMenu[true] và biến crunchyMenu 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()
  1. 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}")
}
  1. 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.

a9e11a1aad05cb2f.png

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ành Int).
  • 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.

  1. 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ọi fold() trên danh sách cookies. Truyền vào 0.0 cho giá trị ban đầu. Loại của giá trị được dự đoán là Double.
val totalPrice = cookies.fold(0.0) {
}
  1. 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 ->
}
  1. Ở phần nội dung của hàm lambda, hãy tính tổng của totalcookie.price. Giá trị này được dự đoán là giá trị trả về và được truyền vào total trong lần gọi hàm lambda tiếp theo.
val totalPrice = cookies.fold(0.0) {total, cookie ->
    total + cookie.price
}
  1. In giá trị của totalPrice được định dạng dưới dạng chuỗi để dễ đọc.
println("Total price: $${totalPrice}")
  1. 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 đó.

5fce4a067d372880.png

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.

  1. Trong main() sau mã hiện có, hãy thêm một biến mới có tên alphabeticalMenu và đặt biến đó bằng với lệnh gọi sortedBy() trên danh sách cookies.
val alphabeticalMenu = cookies.sortedBy {
}
  1. Trong biểu thức lambda, hãy trả về it.name. Danh sách thu được sẽ vẫn thuộc loại List<Cookie> nhưng được sắp xếp theo name.
val alphabeticalMenu = cookies.sortedBy {
    it.name
}
  1. In tên các loại bánh quy trong alphabeticalMenu. Bạn có thể dùng forEach() để in từng tên trên một dòng mới.
println("Alphabetical menu:")
alphabeticalMenu.forEach {
    println(it.name)
}
  1. 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ể.

9. Tìm hiểu thêm