本人目前主要做 iOS 相关的开发工作,但在做 iOS 之前曾经用 Java 写过一些 Android 上的应用,所以基本上算是个半吊子的 Android 开发工程师。在经历 Swift 的洗礼之后,看到 Kotlin 就感觉看到了久违的朋友一般。在这里我会结合自己的一些感受,以及之前在线下开发者分享会的时候整理的资料,说一下自己的一些心得体会。希望可以给想了解这门语言的同学带来一些帮助和新的启发。

这篇文章不会对 Kotlin 的语法细节进行讨论,我会通过一些代码片段对比Kotlin、Swift、和Java,之后重点会对 Kotlin 中的Delegated properties(属性委托,Kotlin中许多重要的特性都是基于该特性)进行说明,最后再进行简单的总结。

Java、Kotlin、Swift,都拉出来溜溜

我们先来看一段Java代码

Java

public class Student {
	public Student(String name) {
		this.name = name;
	}
	public Student(String name, int gender) {
		this(name);
		this.gender = gender;
	}
	private String name;
	private int gender;
	private String email;
	private int age;
	private float bodyHeight;

	@Override
	public String toString() {
		HashMap<String, String> map = new HashMap<>();
		map.put("name", name);
		map.put("email", email);
		map.put("age", String.valueOf(age));
		map.put("bodyHeight", String.valueOf(bodyHeight));
		return map.toString();
	}
	public float getBodyHeight() {
		return bodyHeight;
	}
	public void setBodyHeight(float bodyHeight) {
		this.bodyHeight = bodyHeight;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public int getGender() {
		return gender;
	}
	public void setGender(int gender) {
		this.gender = gender;
	}
	public String getEmail() {
		return email;
	}
	public void setEmail(String email) {
		this.email = email;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
 }

上面这段代码非常简单,我们声明了一个Student的类,它有两个构造方法,分别是单参和多参,为了控制访问,我们需要对所有的成员变量设置gettersetter,这种做法在Java代码里应该已经司空见惯了。为了方便调试,我们重写了toString方法,如果调用toString则会把当前对象的一些基本信息作为字符串返回。

接下来我们可以看一下在Kotlin下的实现:

Kotlin

//kotlin特有的构造方法声明语法
class Student(var name: String) {
	var gender: Int = 0 //I’m property, not a member or field
	var email: String? = null //支持optional类型
	var age: Int = 0
	lateinit var firstName: String
	val isBoy by lazy { gender == 1 }
	//如果需要声明构造方法,需要使用constructor关键字
	constructor(name: String, gender: Int): this(name){
		 this.email = email
		 this.gender = gender
		 firstName = name
	}
	var bodyHeight = 0f
		set(value) { field = value }
		get() { return field }   

	override fun toString() = mapOf(
		"name" to name, 
		"email" to email,
		"age" to age, 
		"bodyHeight" to bodyHeight).toString()
	}
}

不熟悉的同学可能会疑惑,在Kotlingettersetter都去了哪里?不用担心,Kotlin并没有帮我们把访问限制去掉。只是我们在编码的时候不用在代码显式地写出来(你说这些都可以用快捷键自动生成啊?好吧我承认这位同学你的代码量比我的又多了不少,我认输。。。)。如果需要添加自己的访问控制的时候才需要进行声明。

还有一个需要提到很重要的特性的是,Kotlin支持 懒加载。也就是说当某些成员被声明为lazy的时候,他们只在第一次被访问的时候才进行初始化。设想一下,在Java中我们需要写自己的懒加载逻辑,而在Kotlin里在语法中就原生支持了这个现代语言特性。

除了上文中提到的这些,作为一个现代的语言,Kotlin强大的类型推导虽然算不上什么新鲜玩意儿,但在使用的时候也是功不可没。比如我们在初始化变量的时候如果直接进行赋值,var email = ”hello@alipay.com“,那么email在被编译的时候会自动声明为String类型。在成功安装Kotlin的插件之后,在Android Studio下通过Tool->Kotlin->Show Kotlin Bytecode

我们可以轻松看到Kotlin其实在编译成class的时候和Java并无差异,但是由于我们在编码的时候因为使用了Kotlin,生产力大大提升。

最后出场的是Swift

Swift

class Student {
	var name: String
	var email = ""
	var age = 0
	var gender = 0
	lazy var isBoy: Bool = { return self.gender==1 }()
	var bodyHeight: Float{
		set {
			_bodyHeight = newValue
		}
		get {
			return _bodyHeight
		}
	}
	init(name: String) {
		self.name = name
	}
	convenience init(name: String, gender: Int) {
		self.init(name: name)
		self.gender = gender
	}
	
	var description: String {
		return name + email + String(age) + String(gender) + String(bodyHeight)
	}
}

从语法特征来看,SwiftKotlin简直神似。但如果正真使用过这两种语言编码之后,我发现Kotlin在类型推导上做得更好。在声明一些闭包类型的时候,我就遇到过在Kotlin语义下是可以被识别的而在Swift下无法编译通过的例子。这也是我在用这两种语言做业务开发过程中遇到过最为头疼的事。

接下就让我们一起来看看Kotlin里有哪些黑魔法吧。

黑魔法篇

Function extension

在很多地方,我们都可以看到Kotlin原生支持了类似Objective-c下的Category特性,在Kotlin下我们把这个特性叫做Function extension。也就是说我们可以给任何已存在的类添加方法,包括在Java中的一些基本类型,而不是只能通过继承他们在子类中声明我们的方法。这个特性对我来说简直是killing feature一般的存在。这里我举一个具体的例子来说明Function extension到底有多重要。

Java的世界里,我们经常会写各种Util工具类,声明很多静态方法用于类型或者字符串转换。例如我们想对一个基本类型(比如Float类型)转换为字符串,并进行一定的控制,比如下图中的这几种情况:

Screen_Shot_2016_10_09_at_2_35_44_PM

Screen_Shot_2016_10_09_at_2_35_35_PM

Screen_Shot_2016_10_09_at_2_35_30_PM

Kotlin的世界里,使用Function extension来实现的话,我们可以这么做:

fun Double.Yuan(): String{
	val formatter = DecimalFormat("0.00")
	formatter.roundingMode = RoundingMode.DOWN
	return "¥"+formatter.format(this)
}

这种链式写法在使用的时候非常方便,尤其是需要进行多次类型转换的时候,我们再也不用写类似AUtil.convert(BUtil.convert(value))一层层嵌套的代码了。相比之下链式调用也更符合OO语言习惯。

或许有人会问了,那既然可以给已存在的类添加方法,那是否可以添加属性(成员)呢?答案是肯定的。但Kotlin 官网的文档里并没有提及,我们在下面的篇幅里会详细介绍Delegated properties这个黑魔法,进而引申出Property extension的一种实现。

Delegated properties

Delegated properties(委托属性)Kotlin里是一种非常重要的实现机制,许多重要的特性可以基于Delegated properties来实现的。除了上文中提及的Function Extension之外,像Lazylateinit特性也都可以视作基于Delegated properties的一种特性实现,并且我们还可以基于它来做一些属性观察(实时监听property的变化,类似Objective-C中的KVO)的事情。Delegated properties使用起来非常简单,在申明变量的时候,跟上by关键字,后面声明的就是我们的代理类。废话不多说,上码

class Example {
	var member: String by PropertyAttachedDelegation()
}

这里的PropertyAttachedDelegation就是我们声明的委托类。PropertyAttachedDelegation的具体实现如下:

class PropertyAttachedDelegation{
	operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
		return ""
	}
	operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String){
	
	}
}

这里简单说明一下,在外部访问上文中Example对象的member属性时,会触发PropertyAttachedDelegation getValue方法,相应地如果在对member赋值时,会触发setValue方法。其中thisRef代表的是被访问对象的this引用,property参数是KProperty类型,会体现当前PropertyAttachedDelegation所修饰member成员对象的一些运行时特性(类似Java中的反射机制),例如成员变量名、类型等。最后的value则是通过set方法需要传入的值。

那我们在实际开发过程中Delegated properties有什么用处呢?

Property extension

Kotlin官网中并没有提到Property extension,这只是我基于Delegated propertiesDelegated properties特性的一个延伸。简单说,我们可以在Kotlin里轻松实现给已有的类添加新的成员变量这个特性(在Objective-C里我们可以基于Category和运行时库的objc_associate特性实现)。Takling is cheap, show me the code,上码:

data class User(val name: String? = null, val age: Int = 0, val gender:Int =0)

我们新建了一个叫做Userdata class,现在只有nameagegender三个成员属性。如果我想在不修改User类的情况下,添加一个叫做email的成员,可以这么做:

var User.email: String by PropertyAttachedDelegation()

接下去我们对上文中提到的PropertyAttachedDelegation,进行进一步完善:

class PropertyAttachedDelegation<T>{
	//variablesMap是一个字典类型,类似HashMap
	operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
		return thisRef?.variablesMap?.get(property.toString()) as T
	}


	operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String){
		thisRef?.variablesMap?.set(property.toString(), value)
	}
}

我们将PropertyAttachedDelegation声明为一个泛型类,这样保证了getValue方法的返回值与委托类修饰的变量类型保持一致。在setValuegetValue里通过variablesMap对变量进行动态读取与存储,property保证读取与存储键值的一致性。说到这里,variablesMap 又是从哪儿冒出来的呢?接着看下面的代码:

private val Any.variablesMap by lazy {
	val mutableMap: MutableMap <String, Any> = mutableMapOf()
	mutableMap//在Lazy表达式里,会将最后一个表达式的值隐式地作为返回值,不用显式声明retrun
}

Kotlin里,所有类型都集成自Any(Any不是java.lang.Object,当Kotlin里引入Java的类型时,所有的java.lang.Object类型都会动态地转换为Any),我们给Any动态声明一个variablesMaplazy属性,这样能保证variablesMap在所有类型里都能被访问到,private保证了数据安全,无法被外部成员访问或者修改。因为是lazy的,因此variablesMapMutableMap仅在第一次被访问时才创建,避免了内存上的开销。做完这些事情,我们之后就可以快速地给任何对象添加任何类型的成员属性(property)了。比如我还想给上文中的User类型添加一个tel字段,可以这样:

var User.tel: String by PropertyAttachedDelegation()

是不是很轻松?一劳永逸。自我感觉比Objective-C下的做法还优雅不少。

总结

除了上文中提到的一些实战技巧,附件有我之前在线下分享时做的一个 Keynote,有兴趣的同学也可以下载下来看看,内容比这篇短文也要丰富些。总的来说Kotlin非常容易上手,对有swift经验的iOS开发者来说尤其友好。如果你有过一些Android开发经验或者说你正想学习Android开发又不想写Java的话,在我看来Kotlin是一个非常好的选择。

参考文章:

Kotlin Reference





blog comments powered by Disqus

Published

26 October 2016

Tags