[译]Kotlin珍品 6 -扩展:好的,坏的,丑的

翻译说明: 翻译水平有限,文章内可能会出现不许确甚至错误的理解,请多多包涵!欢迎批评和指正.每篇文章会结合本身的理解和例子,但愿对你们学习Kotlin起到一点帮助.html

原文地址: [Kotlin Pearls 6] Extensions: The Good, The Bad and The Ugly java

原文做者: Uberto Barbiniandroid

扩展仍是不扩展?

前言

扩展方法(还有属性)对于Java开发者来讲算是个新东西,实际上它们已经在C#中出现了很长时间,不过JVM对它们的支持首次出如今Kotlin中.面试

ps:扩展函数不难理解,可是在使用场景和规范上的说明和教程并很少.若是你对kotlin的扩展不太熟悉建议先看一下官方文档对扩展的说明.若是你对扩展有了解,那么这篇文章能帮助你们进一步掌握它.算法

官方文档-扩展编程

通过学习庞大的Kotlin代码库和浏览开源的Kotlin代码后,我注意到在扩展的地方常常会出现一个现象就是,有些能提升代码的可读性可是有些却弄得更难理解.bash

这也是一个在第一次用Kotlin作团队开发是的热门讨论话题,因此我认为经过个人成功和失败经验,在这里基于扩展作个总结仍是有点价值的.app

若是你有不一样的意见或者你有好的例子,请必定联系我.

函数

正文

  • 扩展的简单介绍学习

    在Kotlin中有两种扩展类型:扩展函数和扩展属性.

    首先让咱们了解一下什么是一个扩展函数和如何在Kotlin中声明它.

    fun String.tail() = this.substring(1)
    
    fun String.head() = this.substring(0, 1)
    复制代码

    我刚写了两个函数,一个返回字符串的第一个字符,另外一个返回余下的字符串.

    这里的重点是咱们函数名(好比tail)放在了咱们但愿去调用函数的类型的后面,在这个例子中是String.

    咱们写一个测试方法大概长这样:

    @Test
    fun `head and tail`() {t+
        assertThat("abcde".head()).isEqualTo("a")
        assertThat("abcde".tail()).isEqualTo("bcde")
    }
    复制代码

    很明显,如你所料.咱们只是给String添加了两个方法而已.咱们把代码转成java代码再看看:

    @NotNull
    public static final String head(@NotNull String $this$head) {
       String var10000 = $this$head.substring(0, 1);
       return var10000;
    }
    复制代码

    发现head变成了静态的方法,并有一个String类型的参数.那这就是一个语法糖?对,这就是个语法糖!

    仍旧,扩展能提升可读性也能变得更难理解.

    扩展函数类型看上去像这样:
    String.() -> String

    String.()左边的String咱们称为函数接收者


  • 泛型扩展函数
    看完上面的介绍后,咱们知道扩展函数能够应用到任何一个类.那有没有更多的应用场景呢?有!就是下面要说的泛型扩展函数.

    fun <T> T.foo() : String = "foo $this"
    复制代码

    一旦foo在你的做用域,那么你能够用任何对象去调用这个方法,包括null.

    下面是证明的例子:

    assertThat(123.foo()).isEqualTo("foo 123")
        val frank = User(1, "Frank")
        assertThat(frank.foo()).isEqualTo("foo User(id=1, name=Frank)")
        assertThat(null.foo()).isEqualTo("foo null")
    复制代码

    咱们也能够经过改变泛型参数去作扩展限制.比方说咱们只想让某些类和它的子类调用:

    fun <T: Number> T.doubleIt(): Double = this.toDouble() * 2
    这里有一点请注意,咱们把泛型限定为Number还不是Number?(可空的Number).这样的话null就不是一个被这个扩展函数容许的接收者类型了.

    assertThat(123.doubleIt()).isEqualTo(246.0)
        assertThat(123.4.doubleIt()).isEqualTo(246.8)
        assertThat(123L.doubleIt()).isEqualTo(246.0)
        //assertThat(null.doubleIt()).isEqualTo(null)  编译失败!
    复制代码

    因此,若是咱们想限制上面提到的foo函数只能是非空类型的函数接收者的话,咱们能够像下面这样声明:

    fun <T: Any> T.foo() = "foo $this"

  • 扩展函数在中缀表示法中的应用

    若是对中缀比较陌生能够先看一下官方文档的解释 官方文档-中缀表示法

    举个例子,我不太喜欢Kotlin中可空字符串的拼接.我觉得的结果应该是这样null + null == nullnull + "A" == "A"可是实际在Kotlin中结果是"nullnull""和"nullA".

    因此咱们能够经过中缀函数+扩展函数++ (在反引号中)来实现咱们预期的结果:

    infix fun String?.`++`(s:String?):String? = 
            if (this == null) s else if (s == null) this else this + s
    复制代码

    经过验证,达到了语气效果:

    assertThat(null `++` null).isNull()
        assertThat("A" `++` null).isEqualTo("A")
        assertThat(null `++` "B").isEqualTo("B")
        assertThat("A" `++` "B").isEqualTo("AB")
    复制代码

    中缀表示法对开发内部使用的DSL来讲很是有用.

  • 扩展属性

    如今让咱们看一下扩展属性是作什么的.

    很简单!扩展函数让咱们能够给现有的类添加方法,那么扩展属性就让咱们能够给现有的类添加属性.

    可能大家已经知道,Kotlin编译器会在咱们访问Java Bean对象的时候生成对应属性.

    public class JavaPerson {
            private int age;
            private String name;
        
            public String getName() {
                return name;
            }
        
            public void setName(String name) {
                this.name = name;
            }
        
            public int getAge() {
                return age;
            }
        
            public void setAge(int age) {
                this.age = age;
            }
        }
    复制代码

    当咱们在Kotlin中使用这个bean对象的时候,全部getters和setters都变成了属性:

    val p = JavaPerson()
        p.name = "Fred"
        p.age = 32
        
        assertThat(p.name).isEqualTo("Fred")
        assertThat(p.age).isEqualTo(32)
    复制代码

    经过转码能够看到,这些属性都是直接经过映射到getters和setters获得的:

    L1
            LINENUMBER 15 L1
            ALOAD 1
            LDC "Fred"
            INVOKEVIRTUAL com/ubertob/extensions/JavaPerson.setName (Ljava/lang/String;)V
    复制代码

    如何声明新的属性呢?咱们给Java的Date类添加一个millis属性:

    var Date.millis: Long
            get() = this.getTime()
            set(x) = this.setTime(x)
    复制代码

    测试代码

    val d = Date()
        d.millis = 1001
        
        assertThat(d.millis ).isEqualTo(1001L)
        assertThat(d.millis ).isEqualTo(d.time)
    复制代码
  • 扩展的应用

    如今让我同个一个例子来展现如何扩展优化你的代码.FizzBuzz是一个面试会被常常问道的问题.若是你不太了解什么是FizzBuzz的话能够简单理解成一个简单算法题(写一个程序打印1到100这些数字。可是遇到数字为3的倍数的时候,打印“Fizz”替代数字,5的倍数用“Buzz”代替,既是3的倍数又是5的倍数打印“FizzBuzz”)

    如今首先咱们能够经过属性找出打印Fizz和Buzz的数:

    val Int.isFizz: Boolean
        get() = this % 3 == 0
    
    val Int.isBuzz: Boolean
        get() = this % 5 == 0
    复制代码

    再用咱们上面定义的可空拼接String的扩展方法++,咱们能够用一行代码实现FizzBuzz:

    fun Int.fizzBuzz(): String = "Fizz".takeIf { isFizz } `++` "Buzz".takeIf { isBuzz } ?: toString()
    复制代码

    简单分析一下:Int.fizzBuzz()Int类的扩展函数,函数接收者是一个Int;"Fizz".takeIf { isFizz }一个Int类型调用isFizz方法若是返回true就返回字符串"Fizz",不然null;"Buzz".takeIf { isBuzz }一个Int类型调用isBuzz方法若是返回ture就返回字符串"Buzz",反则null;++拼接左右的结果;?:toString()若是是null就调用toString.

    下面是测试代码:

    val res = (1..15).map { it.fizzBuzz()}.joinToString()
    
    val expected = "1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz"
    
    assertThat ( res ).isEqualTo(expected)
    复制代码
  • 小结

    何时应该用扩展何时不该该用扩展?
    下面是个人我的见解.这个总结彻底是我我的的意见,你可能有不一样的见解和理解.不过这些总结也是我经过review其余人的代码总结出来的,因此应该仍是有必定价值的.
    据我所知,目前还可有官方的规范去说明何时应该用扩展不过我参照了kotlin标准库总结的下面的使用模式:

    扩展属性的使用模式

    好的使用场景:

    • 替换setters和getters
    • 经过映射重命名属性的字段
    • 单一字段的简单方法调用(例如isFizz)

    很差的使用场景:

    • 仅仅为了在DSL中去掉方法的括号:
      例如转换一个Int类型成为Duration类型 5.toHours()5.hours更让人容易理解

    • 对用到多个字段的重要方法里面的属性作映射

    扩展函数的使用模式:

    好的使用场景:

    • 一个参数的纯函数
      例 String.reverse()

    • 类型转换
      例 Map.toList(), Int.toPrice()

    • 接口转换
      例 Map.asSequence(), User.asPerson()

    • 链式编程
      例 T.apply{…}, T.let{…}

    • 两个参数的中缀表示法
      例 A to B, HttpRoute bind {}

    • 非侵入式的给现有类来点语法糖
      例 User.fullName()

    • 规避泛型的协变和逆变问题
      例 Iterable.flatMap {…}

      class MyContainerClass<in T> {
          fun <U> map(f: (T) -> U): MyContainerClass<U>{...}
      }
      复制代码

      编译器会报错,由于T 前面被in修饰符限定了,可是在map方法里面又出如今out的位置.咱们能够经过把map方法改为扩展函数来解决:

      class MyContainerClass<in T> {
          ...
      }
      fun <U, T> MyContainerClass<T>.map(f: (T) -> U): 
        MyContainerClass<U>{...}
      复制代码

这里属于Kotlin泛型的知识若是有不太熟悉的朋友看的不太明白的话,后期我会出一章专门讲泛型的文章

很差的使用场景

  • 涉及IO线程和单例的复杂方法
    例 User.saveToDb(), 8080.startHttpServer()
  • 会改变全局状态的方法
    例 “12:45”.setTime()
  • 多参数方法
    例 “Joe”.toUser(“Smith”, “joe@gmail.com”)
  • 经常使用类型上扩展特殊(单一范围)的方法 例 Date.isFredBirthday(), Double.getDiscountedPrice()
相关文章
相关标签/搜索