+3

Tham chiếu function trong Kotlin: sử dụng function như lambda ở mọi nơi

Tham chiếu Function là một trong những cải tiến tuyệt vời mà chúng ta có được với Kotlin, bắt nguồn từ Java. Bạn đã biết rằng Kotlin hỗ trợ function như một type, có nghĩa là bạn có thể lưu một function trong một biến variable, sử dụng nó như một đối số của function khác, hoặc thậm chí làm cho một function trả về một function khác. Đây là tính năng chính của một ngôn ngữ hỗ trợ phong cách lập trình chức năng, và Kotlin có hỗ trợ điều này. Bạn có thể khai báo một function trong một biến như sau:

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

Đây là một function nhận đầu vào là 2 số nguyên integer và trả về 1 số nguyên integer. Trong thực hiện cụ thể này, lambda áp dụng toán tử +. Bạn có thể có một function chấp nhận lambda như là đối số đầu vào:

fun applyOp(x: Int, y: Int, op: (Int, Int) -> Int): Int = op(x, y)

Function này nhận đầu vào là 2 số nguyên integer và 1 function khác, vì vậy, chúng ta có thể sử dụng như sau:

applyOp(2, 3, sum)

Thật đơn giản, đúng không? Có thể bạn đã biết về điều này, nhưng bây giờ, chúng ta sẽ đi đến những điều thú vị hơn:

Tham chiếu function: Bất kỳ function nào cũng có thể là một lambda

Tương tự theo cách này, lambda có thể được truyền vào như một đối số, hoặc được lưu vào một biến, chúng ta có thể làm tương tự với các function thông thường. Nhờ có tham chiếu function, code của chúng ta sẽ trở nên rõ ràng hơn, và chúng ta có thể áp dụng phong cách lập trình chức năng cho các thư viện hay framework mà chỉ sử dụng các function đơn giản. Trước tiên, tôi sẽ giải thích nó với ví dụ trên. Hãy tưởng tượng thay vì một lambda, bạn có một function đơn giản:

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

Điều này đang là như nhau, nhưng thay vì một biến giữ function, chúng ta chỉ có một function. Bây giờ, nếu thực hiện lời gọi sau sẽ bị fail:

applyOp(2, 3, sum)

sum không phải là một lambda, nó chỉ được gọi như một function thông thường. Nhưng nếu bạn để ý kỹ, nó có cấu trúc giống nhau: nó nhận 2 số nguyên integer và trả về một số nguyên integer. Vậy chúng ta còn thiếu điều gì? Chúng ta cần sử dụng tham chiếu function. Để làm điều đó, bạn chỉ cần thêm hai dấu hai chấm :: vào tên của function:

applyOp(2, 3, ::sum)

Và như vậy là xong. Để bạn hiểu được ý tưởng hoàn chỉnh về điều này, tham chiếu function sẽ hoạt động như một lambda, và như vậy bạn có thể gán tham chiếu này cho một biến có cùng cấu trúc:

val sumLambda: (Int, Int) -> Int = ::sum

Chúng ta hãy xem xét tiếp trong một số trường hợp thực tế hơn.

Đưa các tham chiếu function vào sử dụng thực tế

Hãy tưởng tượng một ví dụ điển hình, bạn nhận được dữ liệu để cập nhật UI, nhưng bạn chỉ cần làm điều đó trong trường hợp dữ liệu không phải là null. Cách tiếp cận trực tiếp nhất sẽ là:

private fun updateUI(data: Data?) {
    if(data != null){
        applyUiChanges(data)
    }
}
 
private fun applyUiChanges(data: Data) {
    // Do cool stuff in UI
}

Nếu bạn đã biết về function let, thì cách sau là hợp lý hơn một chút:

data?.let { applyUiChanges(data) }

Nhưng chúng ta hãy suy nghĩ thêm về điều này, bạn hãy kiểm tra cấu trúc của function let:

public inline fun <T, R> T.let(block: (T) -> R): R = block(this)

Đây là một function nhận vào một lambda với 2 đối số là TR. Trong ví dụ, TData, RUnit, bởi vì chúng ta không return lại giá trị nào của let. Vì vậy, nó sẽ giống như sau:

public inline fun let(block: (Data) -> Unit) = block(this)

Để ý function applyUiChanges, nó nhận đầu vào là Data và return Unit, là phù hợp với cấu trúc của function let. Vậy tại sao chúng ta không gọi function như sau:

data?.let(this::applyUiChanges)

Do function là thành viên (member) của class, đó là lý do tại sao bạn phải thêm this vào lời gọi. Với Kotlin 1.2, bạn có thể loại bỏ this:

data?.let(::applyUiChanges)

Nguyên tắc chung: Khi trong một lambda, chúng ta sử dụng một function nhận tất cả các giá trị đầu vào của lambda làm đối số, thì bạn chỉ có thể sử dụng tham chiếu function.

Lambda với nhiều giá trị đầu vào

Tất nhiên, điều này làm việc với bất kỳ giá trị nào. Hãy tưởng tượng bạn có một observable delegate như sau:

var items: List<MediaItem> by Delegates.observable(emptyList()) { property, oldValue, newValue ->
    notifyChanges(oldValue, newValue)
}

Như bạn thấy, chúng ta không thể chỉ sử dụng tham chiếu function vì notifyChanges không cùng cấu trúc. Nó chỉ nhận 2 giá trị thay vì 3. Nhưng chỉ bằng cách thay đổi cấu trúc của function một chút:

fun notifyChanges(property: KProperty<*>, oldValue: List<MediaItem>,
                          newValue: List<MediaItem>) {
    ...
}

Bây giờ chúng ta có thể sử dụng tham chiếu thay thế:

var items: List<MediaItem> by Delegates.observable(emptyList(), ::notifyChanges)

Tham chiếu Property (thuộc tính)

Các tham chiếu không chỉ giới hạn cho function. Hãy quan sát data class MediaItem sau:

data class MediaItem(val title: String, val url: String)

Chúng ta muốn in một danh sách các url được sắp xếp theo title của item.

items
    .sortedBy(MediaItem::title)
    .map(MediaItem::url)
    .forEach(::println)

Theo tôi, cách làm này là dễ đọc hơn so với việc viết ra các lambda rõ ràng như sau:

items
    .sortedBy { it.title }
    .map { it.url }
    .forEach { print(it) }

Nhìn vào bytecode

Bạn sẽ tự hỏi, có sự khác biệt nào ở phía bytecode không, và một trong những lựa chọn là hiệu quả hơn lựa chọn khác. Vì vậy, hãy sử dụng công cụ Bytecode Kotlin cho ví dụ trên để kiểm tra nó. Đây là bytecode cho phần lambda rõ ràng:

Iterable $receiver$iv = (Iterable)items;
$receiver$iv = (Iterable)CollectionsKt.sortedWith($receiver$iv, (Comparator)(new HomeContentFragmentKt$getSortedUrls$$inlined$sortedBy$1()));
Collection destination$iv$iv = (Collection)(new ArrayList(CollectionsKt.collectionSizeOrDefault($receiver$iv, 10)));
Iterator var4 = $receiver$iv.iterator();
 
while(var4.hasNext()) {
   Object item$iv$iv = var4.next();
   MediaItem it = (MediaItem)item$iv$iv;
   String var11 = it.getUrl();
   destination$iv$iv.add(var11);
}
 
$receiver$iv = (Iterable)((List)destination$iv$iv);
Iterator var2 = $receiver$iv.iterator();
 
while(var2.hasNext()) {
   Object element$iv = var2.next();
   String it = (String)element$iv;
   System.out.print(it);
}

Còn đây là bytecode cho phần tham chiếu function:

Iterable $receiver$iv = (Iterable)items;
$receiver$iv = (Iterable)CollectionsKt.sortedWith($receiver$iv, (Comparator)(new HomeContentFragmentKt$getSortedUrls$$inlined$sortedBy$1()));
Collection destination$iv$iv = (Collection)(new ArrayList(CollectionsKt.collectionSizeOrDefault($receiver$iv, 10)));
Iterator var4 = $receiver$iv.iterator();
 
while(var4.hasNext()) {
   Object item$iv$iv = var4.next();
   String var10 = ((MediaItem)item$iv$iv).getUrl();
   destination$iv$iv.add(var10);
}
 
$receiver$iv = (Iterable)((List)destination$iv$iv);
Iterator var2 = $receiver$iv.iterator();
 
while(var2.hasNext()) {
   Object element$iv = var2.next();
   System.out.println(element$iv);
}

Không có gì ngạc nhiên, phần code này là khá giống nhau. Với tham chiếu function, chúng ta tiết kiệm được những khai báo biến, tương ứng với các biến it trong lambda. Vì vậy, chúng ta có thể nói nó là một chút hiệu quả hơn, nhưng là không đáng kể.

Kết luận

Có nên sử dụng tham chiếu function? Tham chiếu function là một tính năng mới có thể làm rõ ràng code và làm nó nhiều ngữ nghĩa. Nhưng điều này đi kèm với việc bạn phải bỏ thêm thời gian ra để tự học hỏi. Đó là điều mà các Developer Java không quen, và những người mới áp dụng Kotlin có thể thấy chúng mơ hồ vào lúc đầu.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí