Kotlin 中的扩展函数和扩展属性

Kotlin 扩展函数

在 android 开发中,经常会使用到各种 utils 工具类。比如 ViewUtils, ScreenUtils, FileUtils 等,

这些类往往会提供一些静态方法,让我们进行一些通用的操作。

或者,我们常常会希望给某一个类增加一两个小属性或者方法,在用到这个类的地方都能调用,方便我们的开发。比如,我们希望每个 String 对象都能有一个方法,返回这个 String 对象包含的大写字母个数。

我们可能会在某个 utils 中这样写

public static int getStringCapitalCount(String s) throws Exception {
        if (s == null) {
            throw new Exception("string should not be null!");
        }
        int count = 0;
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c >= 65 && c <= 90) {
                count++;
            }
        }
        return count;
    }

或者, 我们会考虑使用一个子类来继承这个父类,然后给子类添加这个方法,在代码中使用子类。当然,String 是不可继承的。

这样写,并没有什么问题,一贯的 java 写法。

但是在 Kotlin 中,我们有了更优雅的方式。既然我们最开始的目标是希望 String 类型多一个方法,那么我们的代码也应该写得像这样,如下:

// 函数名以类名作为前缀: ClassName.funName
// 该前缀叫做 接收者类型(receiver type)
fun String.capitalCount(): Int{ 
    var count = 0
    this.forEach { // this 指的是类的实例对象, 叫 接收者对象(receiver object)
        if (it.toInt() in 65..90) {
            count ++
        }
    }
    return count
}

然后,我们可以在任何使用到 String 的地方,调用这个方法,就好像 String 类型本来就拥有这个方法一样。

fun main() {
    val s = "AbCdEfg"
    println(s.capitalCount())
}

这就是 Kotlin 中的扩展函数。

一般,将扩展函数定义在一个 Extensions.kt 的文件中,最外层直接写函数就可以,不需要像 java 一样定义一个类。这样定义的函数是顶层函数,类似于 java 中的静态函数,可以在任何地方被调用。

image-20200426114707470.png

类似地, 我们再写一个例子,比如经常用到的将 Date 转为 String 的函数,我们使用扩展函数的方式:

fun Date.dateString(): String{
    val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
    return dateFormat.format(this)
}

// 调用时
fun main() {
    println(Date().dateString())
}

注意

  • 扩展函数虽然看起来像是往类里面添加了成员函数,但是实际上并没有改变类的任何结构。所以,对于类内部 private、protected 以及 internal(模块外访问)修饰的成员变量和函数,是不能够访问到的,编译器会报错。

    Encapsulation of an Extension Function in Kotlin
  • 如果扩展函数和类内部定义的成员函数重名,那么调用时执行的是类的函数。

  • 扩展是静态解析的,调⽤的扩展函数是由函数调⽤所在的表达式的类型来决定的,⽽不是由表达式运⾏时求值结果决定的。例如:

open class Shape
class Rectangle: Shape()
fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"
fun printClassName(s: Shape) {
println(s.getName())
}
printClassName(Rectangle())

这个例子会输出 “Shape” ,因为调用的扩展函数只取决于 s 参数的类型声明,这里它是 Shape 类。

Kotlin 扩展属性

和扩展函数类似,Kotlin 同样支持扩展属性。比如:

// 给 StringBuilder 类添加扩展属性 firstLetter
var StringBuilder.firstLetter: Char
    get() = get(0)
    set(value) = this.setCharAt(0, value)

可以把扩展属性看作是两个扩展函数,一个是 getter, 一个是 setter 。

调用时:

fun main(args: Array<String>) {
    val message = StringBuilder("hello !")
    println("${message.firstLetter} is the first letter of $message")
    message.firstLetter = 'H'
    println("${message.firstLetter} is the first letter of $message")
}

输出结果:

h is the first letter of hello !
H is the first letter of Hello !

注意

扩展属性没有初始化器,即扩展属性不能初始化,只能使用 getter / setter 来操作扩展属性的值。

val StringBuilder.firsetLetter = 'a' // 这是错误的, 不能初始化扩展属性

这是因为扩展属性并没有真正地插入一个属性到类中,所以扩展属性没有幕后字段(backing field),不能进行初始化。同样,也不能在扩展属性的 setter 里使用 field 关键字。

配合泛型使用

可以使用泛型来给多个类添加扩展属性。比如:

// 得到逆序的 string
val <T> T.reverseString: String
    get() {
        return this.toString().reversed() // 实际上 reversed 函数也是 Kotlin String 类型自带的扩展函数
    }

扩展的 Java 代码

Kotlin 究竟是如何实现扩展函数和扩展属性的呢?

还是使用之前 String.capitalCount 的例子,反编译后的 java 代码如下:

// Extension.kt 文件会被编译成 ExtensionKt.class
public final class ExtensionsKt {
   public static final int capitalCount(@NotNull String $this$capitalCount) {
      int count = 0;
      CharSequence $this$forEach$iv = (CharSequence)$this$capitalCount;
      int $i$f$forEach = false;
      CharSequence var4 = $this$forEach$iv;

      for(int var5 = 0; var5 < var4.length(); ++var5) {
         char element$iv = var4.charAt(var5);
         int var8 = false;
         if ('A' <= element$iv) {
            if ('Z' >= element$iv) {
               ++count;
            }
         }
      }
 return count;
}

实际上和在 java util 中实现方法一致,生成了一个以 String 类型为形参的静态方法,而函数形参 this$capitalCount 其实就是在 kotlin 中 this 指代的那个对象。

再看 StringBuilder.firstLetter 属性的反编译 java 代码:

public static final char getFirstLetter(@NotNull StringBuilder $this$firstLetter) {
      return $this$firstLetter.charAt(0);
   }

public static final void setFirstLetter(@NotNull StringBuilder $this$firstLetter, char value) {
      $this$firstLetter.setCharAt(0, value);
   }

其实就是生成了两个静态的方法,一个 setter 和 一个 getter。用两个扩展函数也可以达到同样的效果。

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

推荐阅读更多精彩内容