Các hàm Kotlin là hàm hạng nhất , có nghĩa là chúng có thể được lưu trữ trong các biến và cấu trúc dữ liệu, được truyền dưới dạng đối số đến và trả về từ các hàm bậc cao hơn . Bạn có thể thao tác với các hàm theo bất kỳ cách nào có thể đối với các giá trị phi hàm khác.

Để tạo điều kiện thuận lợi cho việc này, Kotlin, với tư cách là một ngôn ngữ lập trình được định kiểu tĩnh, sử dụng một họ các loại hàm để biểu diễn các hàm và cung cấp một tập hợp các cấu trúc ngôn ngữ chuyên biệt, chẳng hạn như các biểu thức lambda .

1. Hàm thứ bậc cao hơn

Một hàm bậc cao hơn là một hàm nhận các hàm khác làm tham số hoặc trả về một hàm.

Một ví dụ điển hình là thành ngữ lập trình hàm fold cho collection, lấy một giá trị tích lũy ban đầu và một hàm kết hợp và xây dựng giá trị trả về của nó bằng cách kết hợp liên tục giá trị bộ tích lũy hiện tại với mỗi phần tử bộ sưu tập, thay thế cho bộ tích lũy:

fun <T, R> Collection<T>.fold(
    initial: R, 
    combine: (acc: R, nextElement: T) -> R
): R {
    var accumulator: R = initial
    for (element: T in this) {
        accumulator = combine(accumulator, element)
    }
    return accumulator
}

Trong đoạn mã trên, tham số combine có loại hàm (R, T) -> R , vì vậy nó chấp nhận một hàm nhận hai đối số kiểu R và T, và trả về giá trị kiểu R. Nó được gọi bên trong vòng lặp for và giá trị trả về sau đó được gán cho accumulator.

Để gọi fold, chúng ta cần chuyển nó một instance của loại hàm làm đối số và biểu thức lambda (được mô tả chi tiết hơn bên dưới) được sử dụng rộng rãi cho mục đích này tại các trang web gọi hàm bậc cao hơn:

val items = listOf(1, 2, 3, 4, 5)

// Lambdas are code blocks enclosed in curly braces.
items.fold(0, { 
    // When a lambda has parameters, they go first, followed by '->'
    acc: Int, i: Int -> 
    print("acc = $acc, i = $i, ") 
    val result = acc + i
    println("result = $result")
    // The last expression in a lambda is considered the return value:
    result
})

// Parameter types in a lambda are optional if they can be inferred:
val joinedToString = items.fold("Elements:", { acc, i -> acc + " " + i })

// Function references can also be used for higher-order function calls:
val product = items.fold(1, Int::times)

Các phần sau sẽ giải thích chi tiết hơn các khái niệm được đề cập.

2. Loại hàm

Kotlin sử dụng một họ của các loại hàm như (Int) -> String cho việc khai báo có liên quan tới hàm: (Đoạn code)

Các kiểu này có một ký hiệu đặc biệt tương ứng với đặc trưng của các hàm, tức là các tham số và giá trị trả về của chúng:

  • Tất cả các loại hàm có một danh sách các loại tham số trong ngoặc đơn và một kiểu trả về: (A, B) -> C biểu thị một kiểu đại diện cho chức năng lấy hai đối số của hai kiểu Avà Bvà trả về một giá trị kiểu C. Danh sách các loại tham số có thể trống, như trong () -> A. Các kiểu trả về đơn vị không thể bỏ qua.
  • Các loại hàm có thể tùy chọn có thêm kiểu của receiver, được chỉ định trước dấu chấm trong ký hiệu: kiểu A.(B) -> C biểu thị các hàm có thể được gọi trên đối tượng người nhận  A với tham số là B và trả về giá trị C. Các hàm literals với receiver thường được sử dụng cùng với các loại này.
  • Các hàm tạm ngừng thuộc về các loại hàm thuộc loại đặc biệt, có bổ ngữ suspend trong ký hiệu, chẳng hạn như suspend () -> Unit hoặc suspend A.(B) -> C.

Các loại hàm ký hiệu có thể bao gồm tên cho các thông số của hàm: (x: Int, y: Int) -> Point. Những tên này có thể được sử dụng để ghi lại ý nghĩa của các tham số.

Để xác định rằng một loại hàm là nullable , sử dụng dấu ngoặc đơn: ((Int, Int) -> Int)?.

Các loại hàm có thể được kết hợp bằng cách sử dụng dấu ngoặc đơn: (Int) -> ((Int) -> Unit)

Ký hiệu mũi tên là liên kết phải, (Int) -> (Int) -> Unit tương đương với ví dụ trước, nhưng không tương đương với ((Int) -> (Int)) -> Unit.

Bạn cũng có thể đặt tên thay thế cho loại hàm bằng cách sử dụng bí danh cho loại :

typealias ClickHandler = (Button, ClickEvent) -> Unit

2.1. Khởi tạo một loại hàm

Có một số cách để lấy một instance của một loại hàm:

  • Sử dụng một khối code trong một literal hàm, ở một trong các dạng sau:
  • Literals của hàm với receiver có thể được sử dụng làm giá trị của các loại hàm với receiver.
  • Sử dụng tham chiếu có thể gọi đến khai báo hiện có:
    • một hàm top-level, hàm địa phương, hàm thành viên, hoặc hàm gia hạn : (Đoạn code)
    • một thuộc tính top-level, thuộc tính thành viên, hoặc thuộc tính gia hạn : (Đoạn code)
    • một hàm tạo :::Regex
  • Chúng bao gồm các tham chiếu callable ràng buộc, các tham chiếu này chỉ đến một thành viên của một instance đặc biệt: foo::toString.
  • Sử dụng các instance của một class tùy chỉnh triển khai một loại hàm làm interface:
class IntTransformer: (Int) -> Int {
    override operator fun invoke(x: Int): Int = TODO()
}

val intFunction: (Int) -> Int = IntTransformer()

Trình biên dịch có thể suy ra các loại hàm cho các biến nếu có đủ thông tin:

val a = { i: Int -> i + 1 } // The inferred type is (Int) -> Int

Các giá trị không phải literals của các loại hàm có và không có receiver có thể hoán đổi cho nhau, để receiver có thể thay thế cho tham số đầu tiên và ngược lại. Ví dụ: một giá trị của kiểu (A, B) -> C có thể được truyền hoặc gán khi một giá trị kiểu A.(B) -> C ngược lại:

val repeatFun: String.(Int) -> String = { times -> this.repeat(times) }
val twoParameters: (String, Int) -> String = repeatFun // OK

fun runTransformation(f: (String, Int) -> String): String {
    return f("hello", 3)
}
val result = runTransformation(repeatFun) // OK

Lưu ý rằng loại hàm không có receiver được suy ra theo mặc định, ngay cả khi một biến được khởi tạo với tham chiếu đến hàm mở rộng. Để thay đổi điều đó, hãy chỉ định kiểu của biến một cách rõ ràng.

2.2. Gọi một instance của loại hàm

Giá trị của một loại hàm có thể được gọi bằng cách sử dụng toán tử invoke(…) của nó : f.invoke(x) hoặc f(x).

Nếu giá trị có kiểu của receiver, thì đối tượng receiver phải được truyền làm đối số đầu tiên. Một cách khác để gọi một giá trị của một loại hàm với receiver là thêm vào trước nó với các đối tượng receiver, như thể giá trị là một hàm mở rộng : 1.foo(2),

Thí dụ:

val stringPlus: (String, String) -> String = String::plus
val intPlus: Int.(Int) -> Int = Int::plus

println(stringPlus.invoke("<-", "->"))
println(stringPlus("Hello, ", "world!")) 

println(intPlus.invoke(1, 1))
println(intPlus(1, 2))
println(2.intPlus(3)) // extension-like call

2.3. Các hàm nội tuyến

Đôi khi sẽ có lợi hơn khi chúng ta sử dụng các hàm nội tuyến, các hàm này cung cấp luồng điều khiển linh hoạt cho các hàm bậc cao.

3. Biểu thức Lambda và các hàm ẩn danh

Các biểu thức Lambda và các hàm ẩn danh là các ‘literals hàm’, tức là các hàm không được khai báo, nhưng được truyền ngay lập tức dưới dạng một biểu thức. Hãy xem xét ví dụ sau:

max(strings, { a, b -> a.length < b.length })

Hàm max là một hàm bậc cao hơn, nó nhận một giá trị của hàm khác làm đối số thứ hai. Đối số thứ hai này là một biểu thức mà bản thân nó là một hàm, tức là một literals hàm, tương đương với hàm được đặt tên sau:

fun compare(a: String, b: String): Boolean = a.length < b.length

3.1. Cú pháp của biểu thức Lambda

Dạng cú pháp đầy đủ của biểu thức lambda như sau:

val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }

Một biểu thức lambda luôn được bao quanh bởi dấu ngoặc nhọn, khai báo tham số ở dạng cú pháp đầy đủ nằm ở bên trong dấu ngoặc nhọn và có chú thích kiểu tùy chọn, phần thân đi sau dấu ->. Nếu kiểu trả về được suy ra của lambda không phải là Unit, thì biểu thức cuối cùng (hoặc có thể là biểu thức đơn) bên trong thân lambda được coi là giá trị trả về.

Nếu chúng ta bỏ qua tất cả các chú thích tùy chọn, những gì còn lại sẽ trông giống như sau:

val sum = { x: Int, y: Int -> x + y }

3.2. Truyền trailing lambdas 

Trong Kotlin, có một quy ước như sau: nếu tham số cuối cùng của một hàm là một hàm, thì biểu thức lambda được truyền làm đối số tương ứng có thể được đặt bên ngoài dấu ngoặc đơn:

val product = items.fold(1) { acc, e -> acc * e }

Cú pháp như vậy còn được gọi là trailing lambdas.

Nếu lambda là đối số duy nhất cho lệnh gọi đó, thì hoàn toàn có thể bỏ qua dấu ngoặc đơn:

run { println("...") }

3.3. it: tên ngầm định của một tham số

Ta rất hay gặp trường hợp một biểu thức lambda chỉ có một tham số.

Nếu trình biên dịch có thể tự tìm ra dấu hiện, nó được phép không khai báo tham số duy nhất và bỏ qua dấu  ->. Tham số sẽ được khai báo ngầm dưới tên it:

ints.filter { it > 0 } // this literal is of type '(it: Int) -> Boolean'

3.4. Trả về giá trị từ biểu thức lambda

Chúng ta có thể trả về một giá trị rõ ràng từ lambda bằng cách sử dụng cú pháp trả về đạt điều kiện. Nếu không, giá trị của biểu thức cuối cùng được trả về một cách ngầm định.

Do đó, hai đoạn code sau tương đương với nhau:

ints.filter {
    val shouldFilter = it > 0 
    shouldFilter
}

ints.filter {
    val shouldFilter = it > 0 
    return@filter shouldFilter
}

Quy ước này, cùng với việc chuyển một biểu thức lambda ra bên ngoài dấu ngoặc đơn, cho phép code kiểu LINQ :

strings.filter { it.length == 5 }.sortedBy { it }.map { it.toUpperCase() }

3.5. Dấu gạch dưới cho các biến không sử dụng (kể từ 1.1)

Nếu tham số lambda không được sử dụng, bạn có thể đặt dấu gạch dưới thay cho tên của nó:

map.forEach { _, value -> println("$value!") }

3.6. Hủy cấu trúc trong lambdas (kể từ 1.1)

Việc hủy cấu trúc trong lambdas được mô tả như là một phần của các khai báo hủy cấu trúc .

3.7. Các hàm ẩn danh

Một điều còn thiếu trong cú pháp biểu thức lambda được trình bày ở trên là khả năng chỉ định kiểu trả về của hàm. Trong hầu hết các trường hợp, điều này là không cần thiết vì kiểu trả về có thể được suy ra tự động. Tuy nhiên, nếu bạn cần chỉ định nó một cách rõ ràng, bạn có thể sử dụng một cú pháp thay thế: một hàm ẩn danh .

fun(x: Int, y: Int): Int = x + y

Một hàm ẩn danh trông rất giống một khai báo hàm thông thường, ngoại trừ việc tên của nó bị bỏ qua. Phần thân của nó có thể là một biểu thức (như hình trên) hoặc một khối code:

fun(x: Int, y: Int): Int {
    return x + y
}

Các tham số và kiểu trả về được chỉ định giống như đối với các hàm thông thường, ngoại trừ việc các kiểu tham số có thể bị bỏ qua nếu chúng có thể được suy ra từ ngữ cảnh:

ints.filter(fun(item) = item > 0)

Việc suy luận kiểu trả về cho các hàm ẩn danh hoạt động giống như đối với các hàm thông thường: kiểu trả về được suy ra tự động cho các hàm ẩn danh có phần thân chứa biểu thức và phải được chỉ định rõ ràng (hoặc được giả định là Unit) cho các hàm ẩn danh có phần thân là khối code.

Lưu ý rằng các tham số của hàm ẩn danh luôn được truyền vào bên trong dấu ngoặc đơn. Cú pháp viết tắt cho phép để hàm bên ngoài dấu ngoặc đơn chỉ hoạt động đối với các biểu thức lambda.

Một khác biệt khác giữa biểu thức lambda và các hàm ẩn danh là hành vi của các giá trị trả về phi cục bộ . Một câu lệnh trả về không có nhãn luôn trở về từ một hàm được khai bảo với từ khóa fun. Điều này có nghĩa là sự trả về bên trong một biểu thức lambda sẽ trả về từ hàm bao quanh, trong khi sự trả về bên trong một hàm ẩn danh sẽ trả về từ chính hàm ẩn danh.

3.8. Closures 

Một biểu thức lambda hoặc hàm ẩn danh (cũng như một hàm cục bộ và một biểu thức đối tượng) có thể truy cập vào phần đóng của nó, tức là các biến được khai báo trong phạm vi bên ngoài. Các biến trong phần đóng có thể được sửa đổi trong lambda:

var sum = 0
ints.filter { it > 0 }.forEach {
    sum += it
}
print(sum)

3.9. Literal hàm với receiver

Các loại hàm với receiver, chẳng hạn như A.(B) -> C, có thể được khởi tạo bằng một dạng đặc biệt của literal hàm- đó chính là các literal hàm với receiver.

Như đã nói ở trên, Kotlin cung cấp khả năng gọi một instance của một loại hàm với receiver cung cấp đối tượng receiver.

Bên trong phần thân của hàm, đối tượng receiver được chuyển đến một cuộc gọi sẽ trở thành một this ngầm hiểu, để bạn có thể truy cập vào các thành viên của đối tượng receiver đó mà không cần bất kỳ định tính bổ sung nào hoặc truy cập đối tượng receiver bằng cách sử dụng một biểu thức.this

Hành vi này tương tự như các hàm mở rộng, cũng cho phép bạn truy cập vào các thành viên của đối tượng receiver bên trong phần thân của hàm.

Đây là một ví dụ về một literal hàm với receiver cùng với kiểu của nó, nơi plus được gọi trên đối tượng receiver:

val sum: Int.(Int) -> Int = { other -> plus(other) }

Cú pháp hàm ẩn danh cho phép bạn chỉ định loại receiver của một literal hàm một cách trực tiếp. Điều này có thể hữu ích nếu bạn cần khai báo một biến loại hàm với receiver và sử dụng nó sau này.

val sum = fun Int.(other: Int): Int = this + other

Các biểu thức Lambda có thể được sử dụng như các literal hàm với receiver khi kiểu của receiver có thể được suy ra từ ngữ cảnh. Một trong những ví dụ quan trọng nhất về việc sử dụng chúng là các trình tạo kiểu an toàn :

class HTML {
    fun body() { ... }
}

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()  // create the receiver object
    html.init()        // pass the receiver object to the lambda
    return html
}

html {       // lambda with receiver begins here
    body()   // calling a method on the receiver object
}

Tài liệu từ cafedev:

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của cafedev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!