【Kotlin学习日记】Day12:扩展机制

大家好,我是William李梓峰,欢迎加入我的Kotlin学习之旅。
今天是我学习 Kotlin 的第十二天,内容是 Extensions - 扩展机制。

下文的 extension 包含多个意思,有的是指扩展机制,有的是指扩展函数和扩展属性,如果遇到难以理解的中文描述,请先看英文原文,欢迎大家勘误校正,一起进步。

官方文档:

Extensions - 扩展机制

Kotlin, similar to C# and Gosu, provides the ability to extend a class with new functionality without having to inherit from the class or use any type of design pattern such as Decorator.
Kotlin,跟 C# 语言 以及 Gosu 语言 很像。Kotlin 提供了一种新的扩展机制,不用继承或设计模式(例如装饰器模式)。

This is done via special declarations called extensions. Kotlin supports extension functions and extension properties.
这种机制就叫 extensions (这名字一点都不酷炫)。Kotlin 支持函数扩展和属性扩展。(跟 javascript prototype 继承很像的)

Extension Functions - 函数的扩展机制

To declare an extension function, we need to prefix its name with a receiver type, i.e. the type being extended.
为了声明一个函数的扩展,我们需要在其函数名前面写一个类型的前缀,这个类型就是需要被扩展的类的类型。

The following adds a swap function to MutableList<Int>:
例如,给 MutableList<Int> 类扩展一个 swap() 函数:

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]   // this 与 list 相对应
    this[index2] = tmp
}

The this{: .keyword } keyword inside an extension function corresponds to the receiver object (the one that is passed before the dot).
那个 this 关键字,就是代表 MutableList 对象。因为 MutableList 是一个类,我们通过 MutableList 的对象去调用 swap(),所以 swap() 内部的 this 就是代表 MutableList 的对象。(这里直译会很难懂)

Now, we can call such a function on any MutableList<Int>:
现在呢,我们可以这样子在任何一个 MutableList 对象里面,调用那个 swap() :

val l = mutableListOf(1, 2, 3)    // new 一个 MutableList 对象
l.swap(0, 2) // 'this' inside 'swap()' will hold the value of 'l' 

Of course, this function makes sense for any MutableList<T>, and we can make it generic:
当然,这个函数并非支持泛型(上面的例子是写死了<Int>),我们可以这样子改造一下:

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this' corresponds to the list
    this[index1] = this[index2]   // this 代表对象本身
    this[index2] = tmp
}

We declare the generic type parameter before the function name for it to be available in the receiver type expression. See Generic functions.
我们在函数名的前面声明了一个泛型参数。详情可看 泛型函数

Extensions are resolved statically - 静态处理扩展机制

Extensions do not actually modify classes they extend. By defining an extension, you do not insert new members into a class, but merely make new functions callable with the dot-notation on variables of this type.
扩展机制并非真的改变了被扩展的类。在定义一个扩展函数的时候,你不可以添加新的成员到类里面去,只能够在类的变量上绑定一个新的函数。(类似 Java 静态方法,但这个是可以动态地添加静态方法,静态方法由类去调用)

We would like to emphasize that extension functions are dispatched statically, i.e. they are not virtual by receiver type.
我们强调的是,扩展函数是静态调用的,即,它们并不能根据类型虚拟化。(这里有点绕口,就是没有面向对象的多态性,不支持多态调用)

This means that the extension function being called is determined by the type of the expression on which the function is invoked, not by the type of the result of evaluating that expression at runtime. For example:
意思就是说,扩展函数的调用是由当时编写的类型所有决定的,而不是由当时运行的类型决定的(就是不能够多态调用,不能用父类的引用调用子类的方法),例如:

open class C       // open 让 C 类可以被继承

class D: C()        // D 类继承了 C 类

fun C.foo() = "c"      // 声明 C 类的扩展函数 foo()

fun D.foo() = "d"     // 声明 D 类的扩展函数 foo()

fun printFoo(c: C) {     // 传入 C 类对象
    println(c.foo())        // 打印 C 类的 foo() 返回的字符串
}

printFoo(D())   // 传入 D 类对象,打印结果是 "c",而不是 “d”

This example will print "c", because the extension function being called depends only on the declared type of the parameter c, which is the C class.
这个例子执行后会打印 “c”,因为扩展函数的调用依赖于声明的类型,即 C 类。

If a class has a member function, and an extension function is defined which has the same receiver type, the same name and is applicable to given arguments, the member always wins.
如果一个类有个成员函数(方法),而且恰巧有个扩展函数跟其同名,而且它们俩的参数还是一样的,那么这时候,成员函数(方法)总会赢。(成员函数蛮拼的,爱拼才会赢嘛)

For example:

class C {
    fun foo() { println("member") }
}

fun C.foo() { println("extension") }

If we call c.foo() of any c of type C, it will print "member", not "extension".
如果我们调用 c.foo() ,就会打印 “member”,而不是 “extension”。

However, it's perfectly OK for extension functions to overload member functions which have the same name but a different signature:
虽然如此,只要扩展函数有不同的形参,即便同名,也能百分百地调用到哦:

class C {
    fun foo() { println("member") }
}

fun C.foo(i: Int) { println("extension") }  // 多了个 int i 参数

The call to C().foo(1) will print "extension".
这样子就可以调取扩展函数了,打印出 “extension”。

Nullable Receiver - 可为空的接收器

Note that extensions can be defined with a nullable receiver type. Such extensions can be called on an object variable even if its value is null, and can check for this == null inside the body.
扩展机制可以被一个可为空的接收器类型所定义。这些扩展机制可以在对象变量上所调用,即便该对象是空的,而且,还可以在扩展函数内部用 this == null 来判断该对象是否就是空的。(太屌了,又是一个杀手级特性,Kotlin 的扩展机制已经超越 jQuery 的 extend() 函数 )

This is what allows you to call toString() in Kotlin without checking for null: the check happens inside the extension function.
在这个例子中,你可以直接调用扩展函数 toString() 。因为扩展函数允许你的对象即便为空也可以直接调取,这样子就不容易报空指针异常啦。用扩展函数来保护成员函数,这招厉害了我的哥。

fun Any?.toString(): String {
    if (this == null) return "null"
    // after the null check, 
    // 'this' is autocast to a non-null type,
    // so the toString() below
    // resolves to the member function of the Any class
    return toString()   // 这里调用真正的成员函数
}

Extension Properties - 属性的扩展机制

Similarly to functions, Kotlin supports extension properties:
跟函数类似,Kotlin 支持扩展属性:

val <T> List<T>.lastIndex: Int
    get() = size - 1         // 属性有访问器 setter getter

Note that, since extensions do not actually insert members into classes, there's no efficient way for an extension property to have a backing field. This is why initializers are not allowed for extension properties. Their behavior can only be defined by explicitly providing getters/setters.
要知道,扩展机制并非是给类添加成员的,所以扩展属性没有备用属性。这也是为什么 初始化代码中不允许有扩展属性,因为初始化代码中,默认的 setter 要给备用属性赋值。因此,扩展属性只能显式地声明 setter 或 getter,并且在访问器中不可以访问备用属性(备用字段)。

Example:

val Foo.bar = 1 // error: initializers are not allowed for extension properties

Companion Object Extensions - 伴随对象的扩展机制

这里突然出现了伴随对象,之前也出现过,但是由于对象表达式在后面才会正式介绍,这里可以先留个心眼,大概看懂就行了。伴随对象就是在类的声明体内部写一个匿名或非匿名的静态内部类,伴随对象的方法可以被外部类用静态的方式来调用。

If a class has a companion object defined, you can also define extension functions and properties for the companion object:
如果一个类有伴随对象,你就可以给它定义扩展函数和扩展属性:

class MyClass {
    companion object { }  // will be called "Companion"
}

fun MyClass.Companion.foo() {   // Companion 是当伴随对象的默认名字
    // ...
}

Just like regular members of the companion object, they can be called using only the class name as the qualifier:
就像普通的伴随对象成员一样,它们也可以用外部类来直接访问:

MyClass.foo()

Scope of Extensions - 扩展机制的域

Most of the time we define extensions on the top level, i.e. directly under packages:
大多数时候,我们都是在顶层定义扩展机制(顶层即该文件的全局层),即直接在包下定义:

package foo.bar
 
fun Baz.goo() { ... }    // Barz 类的扩展函数 goo()

To use such an extension outside its declaring package, we need to import it at the call site:
为了在包的外部调用这些扩展机制,我们需要 import 一下:

package com.example.usage

import foo.bar.goo // importing all extensions by name "goo"
                   // or
import foo.bar.*   // importing everything from "foo.bar"

fun usage(baz: Baz) {
    baz.goo()
}

See Imports for more information.
详细请看 引入

Declaring Extensions as Members - 声明扩展为成员

Inside a class, you can declare extensions for another class. Inside such an extension, there are multiple implicit receivers - objects members of which can be accessed without a qualifier.
在一个类里面,你可以为其他类声明扩展函数和扩展属性。在这里头,会存在多个隐式接收器 - 对象的成员可以直接被访问。(这里翻译的不好,反正遇到这些别扭的原文描述,我都会选择看代码吧)

The instance of the class in which the extension is declared is called
dispatch receiver, and the instance of the receiver type of the extension method is called extension receiver.
一个类的实例的扩展函数或扩展属性被称为dispatch receiver,并且一个扩展函数的接收器类型的实例被称为extension receiver。(不知道在讲什么,直接看代码。)

class D {                    // 类 D
    fun bar() { ... }        // 类 D 方法 bar()
}

class C {                     // 类 C
    fun baz() { ... }        // 类 C 的方法 baz()         

    fun D.foo() {           // 类 C 为 类 D 声明扩展函数 foo()
        bar()   // calls D.bar     调取类 D 方法
        baz()   // calls C.baz     调取类 C 方法
    }

    fun caller(d: D) {      // 类 C 的方法 caller()
        d.foo()   // call the extension function
    }
}

总的来说,一个类可以为其他类声明扩展方法。这种机制会催生出很多新的设计模式,但同时也增加了代码的复杂性,如果用不好的话,会造成你想象不到的 debug 灾难。(在不同的类里头跳来跳去)

上面例子里面, fun caller(d: D) 中形参 d 就是 extension receiver。如果有 C() 这个 类 C 的对象存在,则该对象为 dispatch receiver。

In case of a name conflict between the members of the dispatch receiver and the extension receiver, the extension receiver takes precedence. To refer to the member of the dispatch receiver you can use the qualified this syntax.
针对 dispatch receiver 和 extension receiver 之间的名字冲突,extension receiver 通常会获得更高的优先级。为了能够调用到 dispatch receiver 的成员,你可以直接用 语法糖: ‘this’ 的指定
(看完这个链接的内容你就能理解这里到底讲什么了)

class C {                  // dispatch receiver
    fun D.foo() {         // extension receiver
        toString()         // calls D.toString()
        this@C.toString()  // calls C.toString()
    }

Extensions declared as members can be declared as open and overridden in subclasses. This means that the dispatch of such
functions is virtual with regard to the dispatch receiver type, but static with regard to the extension receiver type.
扩展成员可以标记 open 并可在子类中复写。意思是说,这些函数的 dispatch 对于 dispatch receiver 类型来说是虚拟的,而对于 extension receiver 类型来说是静态的。(看不懂就直接看代码吧,我也是这么干的)

open class D {   // 父类 D
}

class D1 : D() {    // 子类 D1 继承 父类 D
}

open class C {                  // 父类 C
    open fun D.foo() {         // 可被复写的 类D 扩展方法
        println("D.foo in C")
    }

    open fun D1.foo() {         // 可被复写的 类 D1 扩展方法
        println("D1.foo in C")
    }

    fun caller(d: D) {              // 调用 类 D 扩展方法
        d.foo()   // call the extension function
    }
}

class C1 : C() {                       // 子类 C1 继承 父类 C
    override fun D.foo() {          // 复写父类 C 的为父类 D 声明的扩展函数
        println("D.foo in C1")
    }

    override fun D1.foo() {   // 复写父类 C 的为子类 D1 声明的扩展函数
        println("D1.foo in C1")
    }
}

// 例子很复杂,慢慢来

C().caller(D())   // prints "D.foo in C"
C1().caller(D())  // prints "D.foo in C1" - dispatch receiver is resolved virtually
C().caller(D1())  // prints "D.foo in C" - extension receiver is resolved statically

反正我就看懂了,D1.foo() 从来都没有被人调用过,因为内部传参就是类 D,扩展函数都是静态调用的,而C1 之所以能够调取自己复写的扩展函数,是因为C1 复写了啊。如果没有复写,照样会调取类 C 的 foo() 。

虽然稍微复杂点,多看几遍,多用几遍就能理解了,一般来说,实际开发中不会整得这么复杂,就算是 Java 里头,也会尽量避免使用继承机制,而是用组合机制。代码好理解,可读性高,才能提高项目的可维护性。

Motivation - 使用扩展机制的动机

In Java, we are used to classes named "*Utils": FileUtils, StringUtils and so on. The famous java.util.Collections belongs to the same breed.
And the unpleasant part about these Utils-classes is that the code that uses them looks like this:
在 Java 里面,我们使用 Utils 为后缀的命名方式来声明工具类:FileUtils、StringUtils 等等。最著名的类 ‘java.util.Collections’ 属于这类了。但是对于工具类静态方法调用来说,大概会是这么一个画风:

// Java
Collections.swap(
    list,
    Collections.binarySearch(
        list, 
        Collections.max(otherList)
    ), 
    Collections.max(list)
)

重复写很多 Collections,冗长的代码啊。

Those class names are always getting in the way. We can use static imports and get this:
这些类名可以用静态引入来优化:

// Java
swap(
    list,
    binarySearch(
        list,
        max(otherList)
    ),
    max(list)
)

但还是要写很多个 list 对象啊。

This is a little better, but we have no or little help from the powerful code completion of the IDE. It would be so much better if we could say
这种虽然好像好一点点,但我们对于IDE的代码补全来说一点作用都没有。(如果你用 VIM 作为IDE,大部分代码补全的插件都不支持 Java7 的静态引入,这时候会让你很抓狂)

要是这样子就很好了:

// Java
list.swap(               // 假设Collections的方法作为 Collection 的扩展函数
    list.binarySearch(
        otherList.max()
    ), 
    list.max()
)

But we don't want to implement all the possible methods inside the class List, right? This is where extensions help us.
但是,我们不想实现所有Collections的方法到 List 类里面,对吧。所以扩展机制就能发挥所长了。

嗯,我对扩展机制保留意见,还需要看看 Kotlin 官方例子是怎么使用这个特性的,感觉乱用滥用都会出很大问题。这篇 《Day12 扩展机制》翻译难度真大,写得好吐血,不过还是坚持写下来了,谢谢大家花那么长时间看到这里。

完。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容