抛砖引玉
做C/C++开发的同学肯定熟悉域作用符(::),这个符号在kotlin中可以获取函数的引用。
函数引用是kotlin引入的一个功能,对于java开发者来说,刚开始可能有点不适应。
对于程序员来说,最合适的抛砖引玉还是上代码:
val sum: (Int, Int) -> Int = { x, y -> x + y }
上面的sum是一个函数类型的变量,lambda表达式执行了相加的操作。
fun applyOp(x: Int, y: Int, op: (Int, Int) -> Int): Int = op(x, y)
applyOp是一个接收三个参数的函数,第三个参数是lambda类型。可以使用下面的方式调用这个函数:
applyOp(2, 3, sum)
上面的三行代码是很基本的很普通的kotlin语法,但是也体现了相对于java的优势即高阶函数:函数可以作为一个类型赋值给变量,也可以作为另一个函数的参数。
函数引用:任何函数都可以是一个lambda
前面例子中的sum的函数体是一个lambda表达式,其实可以用纯函数的形式:
fun sum(x: Int, y: Int) = x + y
这样写实现的效果是一样的,区别在于这样不需要使用变量来保持对函数的引用,但是这样的话调用applOp函数要做一些更改,原来的
applyOp(2, 3, sum)
写法会报类型不匹配的语法错误。我们需要做的更改是使用函数引用,代码如下:
applyOp(2, 3, ::sum)
通过上面两个例子,你应该大致了解了函数引用的概念。其实函数引用和lambda的行为是一样的,而且你也可以把函数引用赋值给变量:
val sumLambda: (Int, Int) -> Int = ::sum
applyOp(2, 3, sumLambda)
惊不惊喜?意不意外?所以,尽量使用函数引用。
函数引用实战
下面的例子是函数引用实战。想象下面的情景:要接收一些data数据来更新UI,但是只在data非空的时候来执行更新UI的操作。代码如下:
private fun updateUI(data: Data?) {
if(data != null){
applyUiChanges(data)
}
}
private fun applyUiChanges(data: Data) {
// Do cool stuff in UI
}
如果你熟悉let操作符,可以使用let操作符避免烦人的if-else的非空检查。
data?.let { applyUiChanges(data) }
看下let操作符的源码:
public inline fun <T, R> T.let(block: (T) -> R): R = block(this)
let接收一个lambda参数,有两个范型:T和R,在这里T是Data,R是Unit
结合这里的具体情形,去掉范型:
public inline fun Data.let(block: (Data) -> Unit) = block(this)
你可以看到,里面的lambda和函数applyUiChanges的结构(这里的结构是指参数和返回值)一模一样,重点来了,这个lambda参数可以用函数引用代替:
data?.let(::applyUiChanges)
很酷吧?
请记住The rule of thumb(经验法则):如果在一个lambda中使用函数,而且这个这个函数的参数全是lambda的输入,那么就完全可以使用函数引用。
那么,让我们走的更远一点。
多个参数的lambda
如果lambda有多个参数,那么这个经验法则同样适用。假如你有下面的观察代理:
var items: List<MediaItem> by Delegates.observable(emptyList()) { property, oldValue, newValue ->
notifyChanges(oldValue, newValue)
}
这里的notifyChanges不能使用函数引用,原因你可能已经猜到了,notifyChanges函数和lambda的结构不一样,lambda接收三个参数,而notifyChanges函数只接收两个参数,为了能够使用函数引用,需要对notifyChanges函数做一些小的改动:
fun notifyChanges(property: KProperty<*>, oldValue: List<MediaItem>,
newValue: List<MediaItem>) {
...
}
经过这样的改动,我们就可以使用函数引用了:
fun notifyChanges(property: KProperty<*>, oldValue: List<MediaItem>,
newValue: List<MediaItem>) {
...
}
由于我的疏忽,上段代码和上上段代码重复了,十分感谢@JarryLeo的评论,错误放在这里,然后下面给出正确的代码:
var items: List<MediaItem> by Delegates.observable(emptyList(), ::notifyChanges)
属性引用
引用不仅仅局限于函数方面,还可以对属性进行引用,返回一个值,类似java中的getters方法。
我们来看下MediaItem的结构:
data class MediaItem(val title: String, val url: String)
如果有一个MediaItem的列表,我们要根据每个MediaItem的title排序,排完序后打印出每个MediaItem的url,一般的代码会这样写:
items
.sortedBy { it.title }
.map { it.url }
.forEach { print(it) }
然而,下面的引用风格对我来说更加简洁易懂:
items
.sortedBy(MediaItem::title)
.map(MediaItem::url)
.forEach(::println)
那么上面的两种写法的字节码有什么不同呢?对比来了:
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);
}
引用风格的字节码:
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);
}
可以看到,两者的字节码基本相同。但是引用风格的字节码节省了几个变量,因此是稍微高效一丢丢,当然你也可以不必在乎这些。
下面再展示一些cool的代码,体会下“引用”这个新的feature:
//1,获取对应类型的,比如下面代码获取到 KProperty<Int>
var x = 1
fun main(args: Array<String>) {
println(::x.get()) // prints "1"
::x.set(2)//KMutableProperty<Int>
println(x) // prints "2"
}
//2,获取 KClass
val c = MyClass::class
项目中要使用函数引用吗?
当然要,赶快在项目中使用吧!程序员需要用代码装逼,用代码说话,当然学习成本还是要的。我认为这是kotlin的一个很好的特性。