Kotlin精髓 (2)

叁叁肆2018-11-20 15:17

此文已由作者申国骏授权网易云社区发布。

欢迎访问网易云社区,了解更多网易技术产品运营经验。


整洁Kotlin风格


在《Kotlin in Action》一书中有归纳了一些Kotlin对比Java的整洁语法如下: 

常规语法 整洁语法 用到的功能
StringUtil.capitalize(s) s.capitalize() 扩展函数
1.to("one") 1 to "one" 中缀函数 infix
set.add(2) set += 1 运算符重载
map.get("key") map["key"] get方法约定
file.use({f -> f.read}) file.use { it.read() } 括号内lambda外移
sb.append("a") sb.append("b") with(sb) { append(“a") append(“b")} 带接收者的lambda


整洁语法换句话来说也是Kotlin的一种编程风格,其他约定俗成的整洁Kotlin编程风格可见官方文档 Idioms。非常建议大家看看Idioms这个文档,里面涵盖了非常Kotlin的使用方式,包括:

  • 使用默认参数代替方法重载
  • String模板(在Android中是否推荐仍值得商榷)
  • lambda使用it代替传入值
  • 使用下标方式访问map
  • 懒初始化属性
  • 使用rangs范围遍历
  • if when表达式返回值
  • 等等


方法参数

Kotlin中的function是一等公民,拥有和变量一样的定义以及传参方式,如以下例子:

fun SQLiteDatabase.inTransaction(func: (SQLiteDatabase) -> Unit) {
  beginTransaction()
  try {
    func(this)
    setTransactionSuccessful()
  } finally {
    endTransaction()
  }
}
// 调用的时候就可以如下方法进行调用
db.inTransaction {
  it.db.delete("users", "first_name = ?", arrayOf("Jake"))
}


带接收者的lambda表达式

lambda表达式可以声明拥有接收者,例如: 

val isEven: Int.() -> Boolean = {
    this % 2 == 0
}

fun main() {
    print(2.isEven())
}

这种带接收者的lambda实际上也是一种方法定义,不过其优先级比扩展方法要低,如果同时有扩展函数(如下)拥有相同名字,则会优先调用扩展方法。

fun Int.isEven(): Boolean {
    return this % 2 != 0
}


let run with apply also

这几个关键字其实都是Kotlin的特殊方法,他们可以让lambda里面的代码在相同的接收者中运行,避免冗余代码,他们的声明如下:

public inline fun <T, R> T.let(block: (T) -> R): R {
    return block(this)
}
public inline fun <R> run(block: () -> R): R {
    return block()
}
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    return receiver.block()
}
public inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}
public inline fun <T> T.also(block: (T) -> Unit): T {
    block(this)
    return this
}

从声明中可以看出他们有以下区别,假设在以下代码中运行 

class MyClass {
    fun test() {
        val str: String = "..."
        val result = str.xxx {
            print(this) // 接收者this
            print(it) // lambda参数it
            42 // 返回结果
        }
    }
}
方法 接收者this lambda参数it 返回结果
let this@MyClass String("...") Int(42)
run String("...") N\A Int(42)
with(*) String("...") N\A Int(42)
apply String("...") N\A String("...")
also this@MyClass String("...") String("...")


DSL构建

以下是DSL和API调用方式的区别

// DSL
dependencies {
    compile("junit")
    compile("guice")
}
// API
project.dependencies.add("compile", "junit")
project.dependencies.add("compile", "guice")

对比下DSL方式更为简洁且易读。通过上述对lambda的介绍可以发现Kotlin可以完美地支持DSL方式编程,只要少量的扩展方法以及lambda定义既可实现以下方式来构建一段html表格

html {
    table {
        tr (color = getTitleColor()){
            this.td {
                text("Product")
            }
            td {
                text("Price")
            }
            td {
                text("Popularity")
            }
        }
        val products = getProducts()
        for ((index, product) in products.withIndex()) {
            tr {
                td(color = getCellColor(index, 0)) {
                    text(product.description)
                }
                td(color = getCellColor(index, 1)) {
                    text(product.price)
                }
                td(color = getCellColor(index, 2)) {
                        text(product.popularity)
                }
            }
        }
    }
}

具体定义如下:

import java.util.ArrayList

open class Tag(val name: String) {
    val children: MutableList<Tag> = ArrayList()
    val attributes: MutableList<Attribute> = ArrayList()

    override fun toString(): String {
        return "<$name" +
            (if (attributes.isEmpty()) "" else attributes.joinToString(separator = "", prefix = " ")) + ">" +
            (if (children.isEmpty()) "" else children.joinToString(separator = "")) +
            "</$name>"
    }
}

class Attribute(val name : String, val value : String) {
    override fun toString() = """$name="$value" """
}

fun <T: Tag> T.set(name: String, value: String?): T {
    if (value != null) {
        attributes.add(Attribute(name, value))
    }
    return this
}

fun <T: Tag> Tag.doInit(tag: T, init: T.() -> Unit): T {
    tag.init()
    children.add(tag)
    return tag
}

class Html: Tag("html")
class Table: Tag("table")
class Center: Tag("center")
class TR: Tag("tr")
class TD: Tag("td")
class Text(val text: String): Tag("b") {
    override fun toString() = text
}

fun html(init: Html.() -> Unit): Html = Html().apply(init)

fun Html.table(init : Table.() -> Unit) = doInit(Table(), init)
fun Html.center(init : Center.() -> Unit) = doInit(Center(), init)

fun Table.tr(color: String? = null, init : TR.() -> Unit) = doInit(TR(), init).set("bgcolor", color)

fun TR.td(color: String? = null, align : String = "left", init : TD.() -> Unit) = doInit(TD(), init).set("align", align).set("bgcolor", color)

fun Tag.text(s : Any?) = doInit(Text(s.toString()), {})


属性代理

Kotlin提供对属性代理的支持,可以将属性的get set操作代理到外部执行。代理的好处有三个:

  • 懒初始化,只在第一次调用进行初始化操作
  • 实现对属性的观察者模式
  • 方便对属性进行保存等管理 

下面来看比较常用的懒初始化例子:

val lazyValue: String by lazy {
    println("computed!")
    "Hello"
}

fun main() {
    println(lazyValue)
    println(lazyValue)
}

以上代码会输出

computed!
Hello
Hello

证明懒加载模块只在第一次调用被执行,然后会将得到的值保存起来,后面访问属性将不会继续计算。这也是在Kotlin中实现单例模式的方式。这种懒初始化的过程也是线程同步的,线程同步方式有以下几种:

public enum class LazyThreadSafetyMode {
    /**
     * 加锁单一线程初始化Lazy实例
     */
    SYNCHRONIZED,

    /**
     * 初始化代码块会被多次调用,但只有首次初始化的值会赋值给Lazy实例
     */
    PUBLICATION,

    /**
     * 没有线程安全,不保证同步,只能在确保单线程环境中使用
     */
    NONE,
}


解构

解构是非常实用的Kotlin提供的将一个对象属性分离出来的特性。 内部实现原理是通过声明为componentN()的操作符重载实现。对Kotlin中的data类会自动生成component函数,默认支持解构操作。以下是解构比较实用的一个例子:


for ((key, value) in map) {
   // 使用该 key、value 做些事情
}


协程Coroutine

先占个位,等我看懂了再来补充 :) 先po一个协程和Rxjava的对比吸引下大家

// RxJava
interface RemoteService {
    @GET("/trendingshows")
    fun trendingShows(): Single<List<Show>>
}
service.trendingShows()
    .scheduleOn(schedulers.io)
    .subscribe(::onTrendingLoaded, ::onError)
// Coroutine
interface RemoteService {
    @GET("/trendingshows")
    suspend fun trendingShows(): List<Show>
}
val show = withContext(dispatchers.io) {
    service.trendingShows()
}


在Android中使用

findViewById

通过引入import kotlinx.android.synthetic.main.实现直接获取xml中ui组件。


anko

anko提供了很多工具类,帮助开发者在Android中更好地使用Kotlin。anko提供了以下实用工具:

  • 快捷Intent:startActivity(intentFor<SomeOtherActivity>("id" to 5).singleTop())
  • 快捷toast、dialog:toast("Hi there!")
  • 快捷log:info("London is the capital of Great Britain")
  • 快捷协程:bg()
  • layout DSL构建
  • 等等

    ktx

    android-ktx 提供了一系列Andrdoid方法的简洁实现。


与Java不太一样的地方

  • static 与 伴生对象
    在Kotlin中并没有static这个关键字,如果想要实现类似于Java中static的用法,需要声明伴生对象companion object。使用object声明的类实际上是一个单例,可以支持直接调用其中的属性与方法。使用了companion修饰的object实际上是可以放在其他类内部的单例,因此可以实现类似于Java中static的效果。至于为什么Kotlin要这样设计,原因是Kotlin希望所有属性都是一个类对象,不做差异化处理,这也是为什么Java中的int、long等基本数据类型在Kotlin中也用Int、Long处理的原因。

  • 默认都是final,除非声明为open
    在Kotlin中所有方法默认都是禁止覆盖的,这样的好处是规范了接口设计的安全性,仅开放那些确实在设计中希望子类覆盖的方法。

  • 默认是public,多了internal
    在Java中,如果不加可见性修饰的话默认是包内可见,Kotlin中默认都是public。同时Kotlin加入了internal关键字,代表着是模块内可见。这个可见性弥补了使用Java进行模块设计的过程中,可见性设计的缺陷。如果要想在Java中实现仅开放某些方法给外部模块使用,但是这些方法又能在内部模块自由调用,那只能是把这些方法都放到一个包内,显然是一个很不好的包结果设计。Kotlininternal关键字可以完美解决这个问题。要想在Java调用的时候完全隐蔽Kotlin的方法,可以加上@JvmSynthetic

  • 泛型
    Java中使用extendssuper来区分泛型中生产者和消费者,俗称PEST,在Kotlin中对应的是outin。同时Java与Kotlin都会对泛型进行运行时擦除,Kotlin不一样的是可以对inline方法使用reified关键字来提供运行时类型。
  • 本地方法
    由于在Kotlin语言中方法是一等公民,因此可以声明局部生命周期的本地方法,如下例子:

    fun dfs(graph: Graph) {
      val visited = HashSet<Vertex>()
      fun dfs(current: Vertex) {
          if (!visited.add(current)) return
          for (v in current.neighbors)
              dfs(v)
      }
    
      dfs(graph.vertices[0])
    }
    


学习资源

Kotlin online try
Kotlin官方文档
kotlin in action
Android Development with kotlin
Kotlin for Android Developers


问题

在Java项目中引入kotlin在大多数情况下都是无痛的,且可以马上带给我们不一样的快捷高效体验。如果硬是要说出一点Kotlin的问题,我觉得会有几个:

  • Kotlin加入会增加方法数以及不多的代码体积,这在大多数情况下不会产生太大的问题
  • 写法太灵活,较难统一。由于Kotlin允许程序员选择传统的Java风味或者Kotlin风味来编写代码,这种灵活性可能导致混合风味的代码出现,且较难统一。
  • 过多的大括号层级嵌套。这是因为lambda以及方法参数带来的,其初衷是希望大家可以用DSL的代码风格,如果没掌握DSL方式的话可能会写出比较丑陋的多层级嵌套Java风味代码,影响代码可读性。


最后

Kotlin是一门优秀的语言,值得大家尝试。


免费体验云安全(易盾)内容安全、验证码等服务

更多网易技术、产品、运营经验分享请点击