Kotlin The Right Way

作者: 鸽子1995 | 来源:发表于2017-03-09 18:30 被阅读0次

    Blog

    现在越来越多的人开始跟风使用Kotlin作为他们Android开发的第二语言,但在使用的时候却完全是Java的风格,给我的感觉就像连官方文档都没有看就拿起来开发了,真是可惜了这门语言,所以写这篇文章的目的就是列举使用Kotlin的正确姿势,帮助Android程序员写代码时Thinking in Kotlin。

    这篇文章会列举实战中的例子,并不会像官方文档一样一步步详细介绍Kotlin,想学习Kotlin请移步这里

    我找了一个Github上用Kotlin写的项目,wechat_no_revoke(下面称作wnr),这个项目作为负面教材简直完美,下面的文章主要就指出其中的问题(只是语言使用上的问题,不涉及它实现的功能--防微信撤销)

    use let instead of if(sth != null)

    wnr中充斥着if (sth != null) { doSomething() }(看WechatRevokeHook.kt),Kotlin提供的标准库,可以完美解决这种冗余代码。

    The Right Way:所有用if判断对象是否为null的都用let代替:

    sth?.let { doSomething() }
    

    let体内的代码只有在sth不为null时才会运行。实际上这个let不是什么Kotlin编译器里的特殊语法,它只是一个普通的函数,你自己都可以实现一个let,下面是标准库里的实现:

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

    let是一个任意类型T上的扩展函数,参数是一个Lambda,然后再在Lambda上调用自己。这样说可能理解起来不那么容易,其实let是一个scoping function,它保护了let体内的变量不泄露到外界,null检查只是附带的功能,举个例子:

    DbConnection.getConnection().let { connection ->
    }
    // connection到这里就访问不到了
    

    use apply to simplify your code

    WecahtDatebase.kt中,有这样一段代码:

    val v = ContentValues()
    v.put("msgid", msgId)
    ......
    v.put("content", msg)
    if (talkerId != -1) {
        v.put("talkerid", talkerId)
    }
    insert("message", "", v)
    

    同样Kotlin标准库中提供了apply函数,专门用来应付这种命令式的代码。

    The Right Way:

    ContentValues().apply {
        put("msgid", msgId)
        ......
        put("content", msg)
        if (talkerId != -1) {
            put("talkerid", talkerId)
        }
        insert("message", "", this)
    }
    

    同样apply也不是什么神奇的东西,它只是个普通的函数:

    fun <T> T.apply(f: T.() -> Unit): T { f(); return this }
    

    apply定义了一个所有类型上的扩展方法,调用apply的时候,会调用传进去的闭包,并返回在闭包上运行过的receiver对象。其实不是那么复杂,看下面的例子你就懂了:

    //把string转为File对象,对此对象调用mkdirs()方法,最后返回此对象
    File(dir).apply { mkdirs() }
    
    //下面是等同的Java代码
    File makeDir(String path) {
      File result = new File(path);
      result.mkdirs();
      return result;
    }
    

    能用一行解决的就不要用多行。

    既然标准库说了这么多,就顺便说完吧:

    //如果那要对同一个对象多次调用不同的方法,就用with
    fun <T, R> with(receiver: T, f: T.() -> R): R = receiver.f()
    
    val w = Window()
    with(w) {
      setWidth(100)
      setHeight(200)
      setBackground(RED)
    }
    
    //用run表示链式调用(run是with和let的合体)
    fun <T, R> T.run(f: T.() -> R): R = f()
    
    "123".run { print(this) }   
            .run { print("hehe") }   //输出"123hehe"
            
    //用use得到与java try-with-resources一样的效果(资源会自动close),注意这里的use也只是个普通的函数而已,不像java一样要编译器用特殊的语法才能做到:
    fun readProperties() = Properties().apply {
        FileInputStream("config.properties").use { 
        fis ->
            load(fis)
        }
    }
    
    //下面是java 1.7及以上才有的try-with-resources
    Properties prop = new Properties();
    try (FileInputStream fis = new FileInputStream("config.properties")) {
        prop.load(fis);
    }
    // fis automatically closed
    

    use ? to indicate Nullable carefully

    wnr中有这样一段代码(看MessageUtil.kt):

    fun extractContent(replace: String?, str: String?): String? {
            var _replace = replace!!
            var _str = str!!
            ......
            ... do something with _replace and _str
            ......
            return _replace
    }
    

    我不知道这哥们写的时候怎么想的,!!是程序员知道对象不可能为null时才用来强转为非null变量的(如果是null程序就崩了),而既然知道不可能为null,那为什么还要用?来表示参数可能为null呢,而且返回值居然带问号,excuse me?互相矛盾...无语。这样的问题充斥这整个项目,完全是乱的。

    The Right way:

    fun extractContent(replace: String, str: String): String {
            ......
            ... do something with replace and str
            ......
            return replace
    }
    
    val str1 = "str1" //str1类型为`String`,不可能为null
    var str2: String? = null //str2类型为`String?`,现在初始化是null,以后也可能是null
    str2 = "some value"  //str2还是`String?`,只不过现在值不是null了
    val str3 = str2!! //str3类型为`String`,这里这能在你确定str2不是null的情况下才能用,编译器并不能保证str2不是null
    

    use first class function instead of object

    既然上面说到MessageUtil了,那顺便说说这个问题。在Kotlin中,函数也是第一公民,下面是wnr中的代码:

    //MessageUtil.kt:
    object MessageUtil {
        fun extractContent(......): String? {
        ......
        }
    }
    
    //Some other file:
    content = MessageUtil.extractContent(replaceMsg, content)!!
    

    就不说这个!!了,上面说过,全是乱的。我就说说最好笑的,这个object MessageUtil完全是多余的,在FP里,函数是第一公民,意味着你不必把方法写在类里,函数也是值,可以做参数,可以当返回值,可以独立于类存在(其实编译成class后函数也在类里,不过这对用户来说是透明的)。

    The Right Way:

    //MessageUtil.kt:
    fun extractContent(......): String {
    ......
    }
    
    //Some other file:
    content = extractContent(replaceMsg, content)
    

    use primary constructor instead of java style constructor

    下面的是wnr中的WechatRevokeHook.kt

    class WechatRevokeHook {
    
        var _v: WechatVersion? = null
        
        constructor(ver: WechatVersion) {
            _v = ver
        }
        ......
    }
    

    相信大多数人只要看过文档都不会写出这样的代码。

    The Right Way: 用primary constructor替代:

    class WechatRevokeHook(val ver: WechatVersion) {
        ......
    }
    

    use Delegates to initialize field

    下面是wnr中的代码

    class MainActivity : Activity(),... {
        ......
        private var tvVersion: TextView? = null
        private var tvProj: TextView? = null
        private var tvRepo1: TextView? = null
        private var tvRepo2: TextView? = null
        ......
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.main)
            tvVersion = findViewById(R.id.tvVersion) as TextView?
            tvProj = findViewById(R.id.tvProj) as TextView?
            tvRepo1 = findViewById(R.id.tvRepo1) as TextView?
            tvRepo2 = findViewById(R.id.tvRepo2) as TextView?
            ......
        }
    }
    

    在下有一万种方法优化这坨代码(笑),首先,最简单的,用lazy delegate:

    val tvVersion by lazy { findViewById(R.id.tvVersion) as TextView }
    

    这样tvVersion只有在第一个用的时候才会初始化,以前分离的声明与初始化合在的一起,只有一行,更加优美,便于理解,而且没有null的烦恼,tvVersion既是val(不会变),又是TextView(没有?,不可能是null),更加安全。

    但是这里代码还是有点长,又要写lazy,又要强转View为TextView,这些代码我都不想写,有没有更简单的写法呢?答案是肯定的,只需要自己实现一个类似lazy的Delegate就可以了,注意,这里的lazy不是编译器里什么神奇的东西,它也是一个方法。

    //ButterKnife.kt
    public fun <V : View> Activity.bindView(id: Int)
            : ReadOnlyProperty<Activity, V> = required(id, viewFinder)
            
    private val Activity.viewFinder: Activity.(Int) -> View?
        get() = { findViewById(it) }
            
    private fun <T, V : View> required(id: Int, finder: T.(Int) -> View?)
            = Lazy { t: T, desc -> t.finder(id) as V? ?: viewNotFound(id, desc) }
            
    // Like Kotlin's lazy delegate but the initializer gets the target and metadata passed to it
    private class Lazy<T, V>(private val initializer: (T, KProperty<*>) -> V) : ReadOnlyProperty<T, V> {
        private object EMPTY
    
        private var value: Any? = EMPTY
    
        override fun getValue(thisRef: T, property: KProperty<*>): V {
            if (value == EMPTY) {
                value = initializer(thisRef, property)
            }
            @Suppress("UNCHECKED_CAST")
            return value as V
        }
    }
    

    这样再在Activity里,就可以这样用:

    val tvVersion by bindView<TextView>(R.id.tvVersion)
    

    再也没有findViewById的烦恼啦。

    上面那段代码来自Jake Wharton,是的,Jake Wharton用一个文件就解决了Butterknife Java版解决的问题,我曾经深入的研究过Java版Butterknife,还写了一个类似Butterknife的工具,Butterknife要在编译期用AnnotationProcessor处理java文件中的annotation,然后利用javapoet生成代码,在生成的代码中findViewById并进行绑定,其中涉及到的apt以及代码生成会影响到性能,而Kotlin并没有这些问题。

    DSL

    就Kotlin展开的话,设计到了Functional Programming和DSL,前者这里就不在展开讨论了,大牛太多,后者我可以简单介绍下,毕竟不同语言构造DSL的方式都不大相同。

    不少人都用Retrofit,就拿Retrofit举个例子吧,下面是一个简单的Retrofit DSL,最终的效果是这样的:

    fun httpService(base: String) =
            retrofit {
                client {
                    readTimeout = sc(100)
                    connectTimeout = sc(100)
                    headers {
                        "Api-Version" with "dim"
                        "Origin" with "hehe"
                        "Token" with PreferencesUtils.getToken()
                    }
                    sslCert(Application.getInstance().applicationContext) {
                        strong = true
                        certs = listOf(R.raw.https)
                    }
                }
                hostUrl = ServerUtil.getCurrentServerBase();
                baseUrl = base
                converterFactories = listOf(GsonConverterFactory.create())
            }
    
    val services: EnumPoll<TEST, Retrofit, String>
        get() = poll {
            mapping {
                TEST.A with "baseUrlA/"
                TEST.B with "baseUrlB/"
            }
            instance = ::httpService
        }
        
    fun testRetrofitDSL() {
        testservice<ApiTest>().testBaidu().enqueue {
            onResponse { res ->
                //doSomethingWith Res
            }
    
            onFailure { throwable ->
                //doSomethingWhen failure
            }
        }
    }
    

    上面是合法的Kotlin代码,函数httpService返回一个Retrofit实例,client返回一个OkHttpClient,其中包含了header、https(SSL)等设置,services是包含Retrofit实例和baseUrl的HashMap,会重复利用有相同baseUrl的Retrofit对象,Api的baseUrl是通过annotation反射获得的,annotation的参数是个Enum,testRetrofitDSL是最终使用时的例子。完整实现代码在这里,有兴趣的可以看一看,还是蛮有意思的。

    总结

    Kotlin虽然入门简单,但是实际用起来还是需要使用者花点心思的,不然写出来的代码就是换个样子的Java。Functional Programming、DSL、Extention Function、Null Safety这些概念虽然不是什么新的点子,但Kotlin却用自己独特的实现方式,让这些特性都有很好的用武之地,不能好好利用就很可惜了。

    相关文章

      网友评论

        本文标题:Kotlin The Right Way

        本文链接:https://www.haomeiwen.com/subject/bllggttx.html