(Blog)
[Presentation][]
前言
我目前的工作是Android开发,没有FP的抽象能力,使用Java写业务逻辑是一个十分繁琐和痛苦的事情,所以就去找其他能在Android上开发的语言。虽然JVM上的语言很多,但Android上的选择真不多,看了很久,选择了Kotlin,静态、强类型、面向对象还有FP,符合要求。而且有JetBrain一帮大佬开发维护,有良好的Intellij支持,和Java无缝连接,我就想:就是它了。现在用了快一年多了,so let's talk about Kotlin(Got this catch phrase from here)。
Good Parts
学习成本
- 首先,在Android项目里配置Kotlin很简单,相信如果你在配置环境时耗费了太多精力,那接下来也就没什么兴趣使用它了。
- 其次,Kotlin上手很快,不像学院风的Scala全是难懂的概念,Kotlin就像在Java上简单包了一层,写起来就像带花括号的Python。就算你完全没学过,你也能直接拿起来用写Java的方式开写,比如这个项目,里面完全是Java风格的Kotlin代码,Intellij自带的“Convert Java File to Kotlin File”功能生成的代码都比这好(具体分析请看这篇文章)。
- 最后,Kotlin学起来很简单,官方文档很详细(还有好心人翻译了中文Doc),如果你有Java基础看起来真是很快,认真看的话一天差不多了,如果你有Scala或者其它函数式语言的基础,看看别人Kotlin代码再看看Kotlin特有的写DSL的方法就差不多了。
下面的文章会偏向官方文档介绍不到的东西,对于官方文档有的我会直接链接到官方文档。
类型系统
- Kotlin是带类型推导的,这意味着不用显示的声明变量类型,这让代码简洁太多了。
- Kotlin不像Java,它不区分原始数据类型和引用数据类型,所有的对象都是类。
- 类型区分可Null和不可Null,null-safety让我摆脱了很多的NullPointerException,所有不带
?
的类都是不能为Null的,你要是想在可能为Null的类上直接调用方法是不可能的。 - 泛型和子类型也比Java强多了,Kotlin用的是declaration site variance,而不是Java的use site variance。
而这些好处都得益于一样东西--Kotlin简单的类型系统,而官方文档却没有文章介绍它,除非你学习了一部分Kotlin,不然你是看不到整个类型系统的。但是,有人看到了这个问题,写了一篇详细介绍Kotlin类型系统的文章:A Whirlwind Tour of the Kotlin Type Hierarchy 。
函数式编程(FP)
- 有了FP,再也不用搞一堆Util类来放静态方法了,因为函数是第一公民,可以独立类存在。
- Android不能用Java8的Lambda?(可比跟我说Retrolambda),Kotlin有啊。
- 而且有了inline function,Lambda不只写起来比Java简单,性能还比Java好。
而且再说用Java8的stream简直是种煎熬(明明都是些map和fold就能解决的问题(摊手)):
- Java区分原始数据类型和引用数据类型,使得Stream Api变得很复杂
- Lambda中没法抛出Checked exception
- 函数没有类型(只有SAM、Function/BiFunction,Predicate/BiPredicate)
- Optional是不能stream的
- stream只能被消费一次,而且没有Streamble<T>这种类似Iterable<T>的这种概念
下面讲到多态时会详细介绍Kotlin中FP的问题。很多刚接触FP的人觉得有高阶函数有curry化什么的就是函数式了,这是很浅的,这种不带类型的在抽象顶端只能算是untyped lambda calculus,加了类型成为typed lambda calculus后函数式才刚刚开始。不过话说回来,关于函数式编程我实在不想扯太多,坑太深实在扯不完,后面我会写一篇单独的文章详细介绍FP。
Data class
有了data class,也不用写一堆只有getter和setter的Bean类了。而且data class和Gson无缝连接,用Retroft也不用担心序列化的问题。
唯一的问题在于Android混淆会影响序列化,还好data class是可以加annotation的:
data class User(@SerializedName("name") val name: String,
@SerializedName("email") val email: String)
扩展方法
你可以在已有的类型上写扩展方法而不污染到类本身,这种扩展方法只是独立函数的语法糖,但却能带来简洁有用的代码:
//在ImageView上直接“添加”方法,而不是用一个`GlideUtils`类里的静态方法
fun ImageView.loadImage(url: String) {
Glide.with(context).load(url).into(this)
}
val image = findViewBy(R.id.your_imageview_id)
image.loadImage("http://your_image_url") //直接在image上使用自定义方法
//在Activity上扩展出token字段,这样在所有的Activity都能用User的token啦
//这个方法其实不是很好,利用代理属性可以有更好的实现方法
var Activity.token: String
get() = prefs.getToken()
Kotlin的标准库里就有很多在String和Iterable上的扩展方法,这样就能在Java的类上直接调用扩展出来的方法,而不是在包一层。Imagine the possibilities,这种解耦程度正是我们面对复杂业务需求时需要的东西。
代理属性
代理属性如果应用的好的话能大大的简化垃圾代码,官方文档是这样说的:对于那些每次都要用相同套路获取到的属性,我们可以用代理属性一劳永逸的实现它们。为了解释这个概念,我会举个Android实际开发中的例子:用代理属性实现ButterKnife
。
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?
......
}
}
这些垃圾代码中的findViewById
就是“每次都用相同套路”的很好的例子。当然你可以用ButterKnife
帮你简化这些垃圾代码,但Butterknife
要在编译期用AnnotationProcessor
处理java文件中的annotation,然后利用javapoet
生成代码,在生成的代码中findViewById
并进行绑定,其中涉及到的apt以及代码生成会影响到性能和编译时间。使用Kotlin我们能有更好的解决办法,不需要annotation,不需要生成代码,用代理属性就好了。首先,最简单的,用官方提供的lazy代理:
val tvVersion by lazy { findViewById(R.id.tvVersion) as TextView }
//onCreate里就不需要findViewById了
利用lazy
代理,tvVersion
只有在第一次被用时才会初始化(懒加载),以前分离的声明与初始化合在的一起,变成只有一行,更加优美,不仅便于理解,而且没有空指针的烦恼:tvVersion
既是val(不会变),又是TextView
(没有?
,不可能是null
),更加安全。但是这里还是太复杂了,没有ButterKnife
那么简洁,我们需要的毕竟只有id和类型,并不需要关心什么是lazy
。更好的办法就要实现自己的代理属性了:一个类似lazy(需要懒加载,不然activity会是null
,就崩溃了),但隐藏了findViewById
和类型转换的代理。按照我们的思路,先实现一个类似lazy
的代理:
//与lazy不同,我们的实现把Activity:`T`和View类型`V`都当泛型传进去了
public 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 MyLazy<Activity, TextView> { t, p -> t.findViewById(R.id.tvVersion) as TextView }
怎么越写越多,别急,按照我们的思路接着走,现在还差隐藏findViewById
和类型转换,那就写个方法把把它们包起来:
//如果你看了上面关于类型系统的文章,那你就明白这里为什么返回`Nothing`了
fun viewNotFound(id: Int, desc: KProperty<*>): Nothing =
throw IllegalStateException("View ID $id for '${desc.name}' not found.")
@Suppress("UNCHECKED_CAST")
//第一个参数是id,第二个参数是一个给定id返回View的高阶方法(我们的findViewById方法)
fun <T, V : View> required(id: Int, finder: T.(Int) -> View?) =
MyLazy { t: T, desc -> t.finder(id) as V? ?: viewNotFound(id, desc) }
这里第二个参数利用到了匿名扩展方法,并且处理了找不到id的异常。还记得扩展方法吧,接下来需要两个东西,一个是在Activity上扩展出一个调用required方法的方法,一个是在Activity上扩展出required
方法需要的第二个参数:
public fun <V : View> Activity.bindView(id: Int)
: ReadOnlyProperty<Activity, V> = required(id, viewFinder)
private val Activity.viewFinder: Activity.(Int) -> View?
get() = { findViewById(it) }
最终的结果:
val tvVersion by bindView<TextView>(R.id.tvVersion) //只有类型的id,完美
利用扩展方法我们只需要实现bindView
和viewFinder
就可以在自己的类型上实现自己的ButterKnife,比如你的adapter的ViewHolder
。
上面实现来自Jake Wharton,是的,Jake Wharton用一个文件就解决了Butterknife Java版解决的问题。利用这种思路,我们能处理很多的垃圾代码,类似的问题比如SharePreference存取,数据库存取,都可以用代理属性解决。
运算符重载
Kotlin也有一个Java一直没有的功能:运算符重载。不过不像Scala里那么自由,你只能重载简单的运算符(比如算数、比较运算符),不能重载控制代码流向的运算符(比如逻辑运算符中的短路),而且最重要的是你不能自定义自己的符号。不过这样也好,安全简洁多了,省的看别人一堆奇奇怪怪的符号。
DSL
利用匿名扩展方法,可以愉快的写DSL(从Groovy学来的),而且由于Kotlin是强类型的,这些DSL也是强类型的,比Groovy安全多了。JetBrain还专门写了一个面向Android的DSL:Anko(Android-Kotlin),Anko借助扩展方法和匿名扩展方法让Android可以直接用代码方便的写出UI,注意这并不是在Android源代码上包了一层,而是扩展了一层。
利用扩展方法和匿名扩展方法,我们可以写出很有意思的代码:
class MConverter<T>(var value: T) {
val old = value
override fun toString(): String = "[$old] => [$value]"
}
fun <T> convert(value: T, init: MConverter<T>.() -> Unit): MConverter<T> {
val converter = MConverter(value)
converter.init()
return converter
}
// convertersString.kt
fun MConverter<String>.toLower() {
value = value.toLowerCase()
}
fun MConverter<String>.toUpper() {
value = value.toUpperCase()
}
// convertersDouble.kt
fun MConverter<Double>.toLower() {
value = Math.floor(value)
}
因为强类型,下面只有前两个方法是合法的:
convert("ABC") {
toLower() //ABC => abc
toUpper() //abc => ABC
}
convert(2.2) {
toLower() //2.2 => 2.0
}
convert(2) {
toLower() //编译器:没有这个方法
}
Frustrations
现在说说蛋疼的地方,语法习惯之类的小问题我就不说了,比如,Companion objects(从Scala学的)写起来麻烦;还有函数定义和Lambda不一样:
val f1 = {i:Int -> i+1}
fun f2(i:Int) = i + 1
val f2_ = ::f2 //f2要加`::`
val f1_ = f1 //不像f1能直接引用
现在我说说最蛋疼的:残念的多态能力。如果你学过函数式的语言(比如Haskell),你说不定听过higher-rank、higher-kinded、type class这些概念,对于那些没有的,我们先复习一下什么是多态(Polymorphism)。
多态
同一段代码可以应用不同的类型就叫做多态。多态一般分下面几种类型:
- 参数多态(Parametric polymorphism),对一段代码做“笼统的”类型检查,用类型变量代替实际类型做参数,然后在需要使用的时候初始化相应的类型。注意其中参数定义是一致的,所有的代码实例表现都一样。大多数语言都提供这一类型的多态,比如Java的泛型和C++的模板。
- 特设多态(Ad-hoc polymorphism),与参数多态一致的表现不同,特设多态允许代码在参数为不同类型时表现出不同的行为,一个常见的例子是重载,一个方法名字根据参数类型的不同可以有多种实现,编译器或者运行时环境会根据参数的类型选取相应合适的函数实现。常见的例子是算数运算,比如
+
在整数、浮点数上都能使用。 - 子类型多态(subtype polymorphism),对某种数据类型能定义其子类型,子类型在某种意义上是父类型的替换,意味着能在父类型上应用的方法在子类型上也能用。如果S是T的子类型,那么在需要T的上下文中也可以用S替换它。子类型的具体实现就和这个程序语言上下文很有关联了。不同的语言一般会设计自己特定的子类型系统。注意subtyping和subclassing是不同的概念,比如Java中的接口也是subtyping的一部分,而不是subclassing的一部分。
一般大家说的“多态”都是比较笼统的概念,比如,在面向对象语言中,人们说的“多态”一般默认指子类型多态,而其中的参数多态则被叫做泛型;在函数式语言中,“多态”则一般默认指参数多态。一个程序语言可以包含多种类型的多态。一个程序语言所能提供的多态类型的丰富程度很大程度上决定了自身的抽象程度,也就决定了自身的“好坏”。下面就一个个介绍这些多态到底是什么东西,Kotlin到底缺少了什么。
参数多态
如果你知道Java中的泛型,那解释起来就简单多了,参数多态就是这类东西的行话。参数多态也是大多数语言所能支持到的多态层级。举个例子,下面是一个随机返回第一个或第二个参数的函数(Kotlin代码):
//对于只有一个表达式的函数可以直接用=代替{}
fun randomString(a: String, b: String): String =
if (Math.random() < 0.3) a else b
有个笑话讲得好,让一个程序员毁灭地球,绝对不会写一个destroyEarth
的方法,而是写一个destroyPlanet
方法然后把地球当参数传进去。是的,上面的方法只能选字符串,如果让它能选任意类型的话就要用泛型了:
fun <T> randomValue(a: T, b: T): T =
if (Math.random() < 0.3) a else b
泛型让我们从类型取到了值,无论什么类型这段代码的功能是一定的,这就是参数多态。然而当我们的代码复杂到一定程度时,只有参数多态往往是不够的,我们需要更高级的方法来抽象出更精练的代码。
Higher-rank类型
在上面的例子中,randomValue
是个多态函数,它接受一个类型参数T
(隐式的)、两个类型为T
的参数,然后返回一个类型T
的值。如果你稍微了解过函数式语言,你肯定知道在这种语言中,函数是第一公民,函数是值,能当参数,能存在变量里,也能当返回值。但是,在Kotlin中(实际上是大多数的静态类型语言),多态函数不是第一公民,意味着你不能把randomValue
当参数传给其它函数,也不能把它先存变量里后再初始化类型T
,要想用它,你得指明T
的类型,也就是把它转化为单态函数。
Higher-rank类型就是这么来的:和普通单态函数一样,把多态函数当成第一公民。因为Kotlin不支持Higher-rank类型,尽管下面的代码你看起来很合理,但它是通不过编译的:
fun <T, U> weirdChoice(random: (T, T) -> T, c1: U, c2: U): U {
if (random(true, false)) { //type mismatch
return random(c1, c2) //type mismatch
} else {
return random(c2, c1) //type mismatch
}
}
val test = weirdChoice(::randomValue, "emacs", "vim") //type inference failed
T在编译时就要确定类型,不能在运行时动态确定类型。扩展方法也是如此,他们是静态绑定的,在编译的时候就要确定类型。为了解决上面的问题,有几种妥协的办法:
最简单的就是放弃高阶函数,不把random当成参数而是外部独立的方法,也就是直接调用randomValue
:
fun <U> weirdChoice(c1: U, c2: U): U {
if (randomValue(true, false)) {
return randomValue(c1, c2)
} else {
return randomValue(c2, c1)
}
}
上面的方法中weiredChoice
和randomValue
耦合度很高,不易维护扩展。好一点的方法是把random
抽象成接口:
interface BinarySameType {
operator fun <T> invoke(a: T, b: T): T
}
//这里利用了重载`invoke`方法,使得接口可以像方法一样调用
fun <U> weirdChoice(random: BinarySameType, c1: U, c2: U): U {
if (random(true, false)) {
return random(c1, c2)
} else {
return random(c2, c1)
}
}
这样的缺点是要么新建个类实现这个接口,要么在使用的时候新建一个匿名实例:
val impl = object : BinarySameType {
override fun <T> invoke(a: T, b: T): T {
return if (Math.random() < 0.3) a else b
}
}
weirdChoice(impl, "emacs", "vim")
OOP真是对我们没什么帮助,我们还要关心什么是BinarySameType
,代码越写越多,这可不是我们想要的结果。是时候试试用FP的思路来解决问题了。泛型方法不是不能当高阶函数么,那我们就退一步,把泛型函数抽象成低阶的单态函数,不就可以当高阶函数了吗?回头看看(T, T) -> T
类型,他真的能保证给定任意的类型T,都能有返回值吗?控制一切的不还是if
语句中的判断条件,我们需要做的就是把这个条件暴露给weirdChoice
就可以了,这个条件的类型是什么呢?很简单,一个不需要参数并返回布尔值的方法() -> Boolean
,现在能写出下面的半成品了:
fun <U> weirdChoice(random: () -> Boolean, c1: U, c2: U): U {
if (random???(true, false)) {
return random???(c1, c2)
} else {
return random???(c2, c1)
}
}
var choice = weirdChoice({ Math.random() < 0.3 }, "emacs", "vim")
看到???
了吧,我们只缺少它这么一个神奇的东西了,它能把下面{ Math.random() < 0.3 }
“插入”到具体实现的地方并让random
和两个参数“作用”后返回其中一个。但是这可能吗?random
自己就是个Lambda,它并没有这种功能呀?其实很简单,当说起一个东西没有一个方法的时候,你就应该想起扩展方法了,是的,我们只需要在random
上扩展出这个具体实现就行了:
//在类型为() -> Boolean的Lambda上扩展出一个泛型方法--asChoice
fun <T> (() -> Boolean).asChoice(a: T, b: T): T =
if (this()) a else b
//这里优化了一下参数位置,Kotlin允许在调用时把最后一个参数为Lambda的放在括号外面,好看一点
fun <U> weirdChoice(c1: U, c2: U, random: () -> Boolean): U {
if (random.asChoice(true, false)) {
return random.asChoice(c1, c2)
} else {
return random.asChoice(c2, c1)
}
}
//没有接口,没有奇怪的类,no bullshit,只是简单的函数调用
var choice = weirdChoice("emacs", "vim") {
//这里是多么的自由,你可以写任何代码,只要最后返回布尔值就行了
print("freedom at its finest")
Math.random() < 0.3
}
上面的代码如果用Java实现会变成啥样,想想就很麻烦。总之,虽然Kotlin不支持higher-rank,但我们还是能写出很精练的代码。而且考虑到higher-rank在大多数语言中都没有(因为会让类型推导变得不确定,连Haskell都要加个language-extention才解决),自然还能接受。higher-rank解决的是对所有类型T
的泛型问题,下一节我们看看关于特定范围类型--higher-kinded的泛型问题。
-> 2017.03.02更新,今天Kotlin发布了版本1.1.0,添加了typealias,类似c语言中的typedef
,可以给类型起其它的名字,特别是对含有复杂泛型的类型(比如集合)和函数类型的简化有很好的效果。给()->Boolean
添加方法太突兀了?那就起个其它名字吧:
typealias BinarySameType = () -> Boolean
fun <T> BinarySameType.asChoice(a: T, b: T): T =
if (this()) a else b
fun <U> weirdChoice(c1: U, c2: U, random: BinarySameType): U {
if (random.asChoice(true, false)) {
return random.asChoice(c1, c2)
} else {
return random.asChoice(c2, c1)
}
}
//调用的用法是一样的,就不写了
Higher-kinded type
看完了参数多态里能把类型变为值的函数(泛型),有没有把类型变为类型的函数呢,当然是有的,这种方法叫做类型运算符(type operator),举个例子,可以把Java里的Stack
想象成这种函数:
Stack<String> mStack;
Stack
可以看成一个接受一个类型(String
)并返回一个类型的函数(如果在FP里的话)。有了Higher-kinded类型的话,这种类型运算符也是一阶类型,同higher-rank类型一样可以当成第一公民。简单说就是,类型运算符可以当做其它类型运算符的参数(泛型的泛型)。同样,蛋疼的是Java和Kotlin都不支持Higher-kinded类型,是的,下面的代码是肯定通不过编译的:
class GraphSearch<T> {
T<Node> nodes; //编译器:T能这么用?黑人问号.jpg
}
如果可以的话,那这里的GraphSearch
和它的T
都是类型运算符,可以写出下面神奇的类型:
GraphSearch<Stack> depthFirstSearch; //T是Stack
GraphSearch<Queue> breadthFirstSearch; //T是Queue
同样,在Kotlin里是写不出类型的类型的,不仅如此,还不能使用参数化后的泛型参数:
class Foo<T>
fun <T : Foo> bullshit(v1: T<String>, v2: T<Int>) {
//编译器:你开心就好
}
fun <Foo> nonsense(v1: Foo<String>, v2: Foo<Int>) {
//编译器:因吹斯听
}
这种类型的类型叫做kind,普通类型通常表示为*
,type构造器(类型运算符)的类型是* -> *
(为函数,参数为*
,返回一个*
,比如Stack
),GraphSearch
的类型为(* -> *) -> *
(为高阶函数,参数为前面的type构造器,返回一个*
),这里的表示涉及到curry化等其它奇奇怪怪的东西,我会在以后的文章介绍(hope so)。GraphSearch
叫做higher-kinded就是因为它是类型层面上的高阶函数,他的类型就是kind。
与higher-rank不同,higher-kinded类型在函数式语言中还是挺常见的,Haskell里的Functor和Monad就是很好的例子(希望能帮助到你理解Monad)。学院风的Scala当然也是有的(敢问Scala什么没有😭),下面就是一个例子:
trait Container[M[_]] {
def put[A](x: A): M[A]
def get[A](m: M[A]): A
}
val container = new Container[List] {
def put[A](x: A) = List(x)
def get[A](m: List[A]) = m.head
}
//注意到下面Container是kind了吧,List是type构造器,*是String和Int等普通类型
container.put("hey") // => List[java.lang.String] = List(hey)
container.put(123) // => List[Int] = List(123)
只可惜Kotlin是没有了(不过有declaration site variance,还是比Java舒服)。
特设多态
讲了这么多,其实还有个重要的东西没说:ad-hoc polymorphism,有了前面的基础,理解这个应该很简单了。回忆一下上面关于程序员毁灭地球的笑话,如果你仔细思考,这个笑话里有个重要的问题:
尽管你抽象出了destroyPlanet方法,但可以想象,这个方法其实是涉及到有个Destroyable接口的,传进去的参数,无论是地球还是哪个星球,你都要先实现这个Destroyable里的destroy方法才能完成自毁。不懂?这么说吧,你没法写一个下面对任意类型都有用的方法:
fun <T> destroyPlanet(planet: T) = planet.destroy()
“这不是废话吗?”聪明的小明同学立刻抢着说:“Kotlin是静态类型的,你这肯定通不过编译啊,destroy方法都找不到,你要这样写”:
fun <T : Destroyable> destroyPlanet(planet: T) = planet.destroy()
interface Destroyable {
fun destroy()
}
class Earth : Destroyable {
override fun destroy() {
print("RUA! Earth Destroyed!")
}
}
fun test() {
val home = Earth()
destroyPlanet(home)
}
“小明同学,你OOP学的很好嘛,我先问你个问题,如果再有其它的星球,是不是还要实现你的Destroyable
接口呢?”
“是...不过就是这样啊,不然泛型就找不到destroy方法了。”
“那如果其它星球的destroy方法和地球不一样,是需要参数的呢?”
“额...那就只能再在接口加抽象方法再实现一遍了。”
“最后一个问题,姑且认为这个Earth
是你的,你能写出它的摧毁方法,那如果这是外星人的星球,你不能直接改动它们的星球(不能改动星球的代码,不能实现接口,这种情况经常出现在语言自带类型或其它人的库上),那你要怎么摧毁它们呢?”
“......”
是的,小明同学也是实在没有办法,你没法在不影响原数据类型(Earth)的情况下抽象出一个只在特定类型(星球kind)上作用的泛型函数。就连Kotlin的源代码里也做不到:Kotlin标准库里有很多在不同类型上的扩展方法--forEach、map、flatMap、fold等,虽然你可以方便的使用这些高阶函数操作你的集合,但Kotlin实现里却没有任何类似mappable(可map)、foldable(可fold)等概念,就算有,也没有办法把这些概念添加到已有的数据类型上,并写出作用在特定不相干类型上(kind)的泛型函数(而不是所有类型T
),和我们的摧毁星球笑话一个道理呢。特设多态就是这类问题的答案,但是,是的,蛋疼的是Kotlin的FP即没有Higher-kinded类型,更没有特设多态(摊手)。
举个比较典型的其它语言的特设多态实现,Rust里的Traits:
//Rust 官网关于Traits第一句话就是A trait is a language feature that tells the Rust compiler about functionality a type must provide.(看到a type must provide没( ̄へ ̄、)
use std::ops::Mul;
//类似接口
trait HasArea<T> {
fn area(&self) -> T;
}
//纯ADT定义,不涉及接口、方法
struct Square<T> {
x: T,
y: T,
side: T,
}
//在Square上“扩展”HasArea“接口”
//注意这里并没有改变Square的定义和类型
//注意泛型T是怎么约束在一定类型里的
//注意泛型T是怎么重载了乘法运算符的
impl<T> HasArea<T> for Square<T>
where T: Mul<Output=T> + Copy {
fn area(&self) -> T {
self.side * self.side
}
}
fn main() {
//定义一个Square
let s = Square {
x: 0.0f64,
y: 0.0f64,
side: 12.0f64,
};
//s的定义只是个数据类型,但现在可以在s上调用area方法
println!("Area of s: {}", s.area());
}
上面短短的代码展示了:扩展方法、运算符重载、包含type class的参数多态、包含接口的子类型多态。这里最重要的思想就是接口、数据、方法是解耦的、互不影响的,也就是在函数(function)和类型(type)层面进行抽象,而不是在面向对象的类(class)上抽象。
总结
总体来讲,对于Android端的开发,用Kotlin写出的代码要比等同的Java简洁易懂的多。特别是繁多的网络接口回调这种任务,用Lambda和DSL能化简不少冗余的Java代码,也易于维护业务逻辑;在数据方面,有了Kotlin的类型系统和null-safety的保证,真是减少了不少空指针的bug;还有方便的扩展方法,Activity和Context没有这个方法?没关系,加一个就好了。
其它的具体来讲,这就和代码风格很有关系了:
- 如果你像我一样是个重度FP使用者,那Kotlin所能提供的多态系统可能满足不了你一直想抽象代码的心。但对大多数写一写单态函数做高阶函数的应该足够了,更何况比Java要强多了(摊手)。
- 如果你对Java这种一堆getter、setter、constructor的Bean文件很烦的话,那Kotlin提供的简洁的定义类的方法会大大帮助你少些垃圾代码,比如类代理。
- 对于每次get、set都有相同套路的属性,可以用代理属性,比如上面简化findViewById的例子,减少垃圾代码。
- 用内部DSL代替外部DSL,代码即是数据,比如Anko用Kotlin代码直接写UI,而不用XML。
- 等等......
上面的代码风格都设计到一个重点,它叫做减少垃圾代码,当我看到任何两坨代码长得差不多,我就会想办法把它们抽象成一坨(比如OOP层面用子类型多态、FP层面用高阶函数和参数多态、有macro系统就用macro生成代码),而这用Java很难做到,如果你和我一样坚信DRY原则,那选一个抽象能力更强的语言总是没错的。I mean, who want's to write the same shit again and again, right?
P.S 还不满足?语言官方的specification在这。