Kotlin使用指南之类篇基础部分二

属性与字段


基础


  • Kotlin中类的属性默认是final public的并且默认带有get和set方法,使用时只需要直接.调用就可以无需使用get和set方法

  • Kotlin中的属性在声明时必须要进行赋值,当然这其实是不可能的。有一下两种方法可以不用为属性赋值

1. 使用lateinit来标识属性,该属性表示要延后初始化行为

2. 使用可空类型,并初始化属性为空


那我们来讨论一下两种方法哪种更为合适呢?


1. 使用lateinit时如果在使用该属性时并没有进行初始化操作会抛出一个特定异常,当然我们可以使用.isInitialized进行判断,但是这样会造成很多的冗余。


2. 使用可空类型的方式,不会造成异常,但是需要我们之后使用属性时都需要考虑为空的情况。


综上看来除非我们十分确定我们的属性的初始化行为在调用行为之前,我们可以使用lateinit,否则个人建议使用第二种方式


Get和Set


自定义get和set


有的时候我们需要在get和set中添加一些我们自己的代码(当然这点个人觉得最好不要加逻辑到get和set中)。这时需要我们自定义get和set。

var stringRepresentation: String

get = this.toString

set(value) {

setDataFromString(value) // 解析字符串并赋值给其他属性

}

  • 同时我们可以通过get函数来推断出属性的类型,进而省略属性的类型声明


  • 我们同样可以修改get和set方法的可见性,只需要在get或set前面加入可见性声明,同样可以为get和set添加注解。


幕后字段


kotlin为我们提供了幕后字段field以方便我们重写get和set方法

var counter = 0 // 注意:这个初始器直接为幕后字段赋值

set(value) {

if (value >= 0) field = value

}

上面的代码想必大家一看就懂我就不多哔哔了,幕后字段只能在set中使用


可见性修饰符


Kotlin中的可见性修饰符包括private, protected, internal, public


  • 其中public为默认的可见性修饰符

  • protected不能用作顶层成名,即顶层变量和类

  • internal为模块内可见,模块的概念是编译在一起的一套 Kotlin 文件包括:


1. 一个 IntelliJ IDEA 模块

2. 一个 Maven 项目

3. 一个 Gradle 源集(例外是 test 源集可以访问 main 的 internal 声明)

4. 一次 <kotlinc> Ant 任务执行所编译的一套文件


泛型


泛型基础


kotlin与java一样可以使用泛型进行开发

基本使用方法与java类似

class Box<T>(t: T) {

var value = t

}

在讲泛型之前我们要先明确几个概念


  • 型变:这个概念就是java中通配符的概念,java的泛型是不型变,也就是说如果你传入的泛型是A,那么之后使用的时候泛型就只能是A,Ade父类或者A的子类不能代替A,比如你传入泛型为String,取的时候就只能是String,而不能是Object,虽然他看上去没有问题。所以java引入了通配符<?>

  • 协变:我们知道java通配符中有两种其中<? extends Object>表示可以接收Object的子类,如果按照继承链来看,这也就表示我们可接收的对象的上限为Object,这样的通配符称之为协变的

  • 逆变:反之<? super String>这种类型的通配符,它规定了类型的下限,表示只接收String的父类型,这种类型为逆变的。

  • 生产者:你只能从中读取的对象,也就是一个类,或者接口,他所有的方法都只返回值使用到泛型但是没有入参使用到泛型。那么我们称他是生产者。

  • 消费者:与上面相反,你只能写入的对象,也就是只有入参使用到泛型的类或接口我们称为消费者。

  • 我们都知道在java上我们建议使用PECS,即生产者使用协变的通配符,消费者使用逆变的通配符。


基于以上的知识我们开始介绍kotlin的泛型,kotlin中并没有通配符机制,但是他有俩个其他机制来达到更好的效果:声明处型变和类型投影


声明处型变


针对生产者我们可以使用out修饰符,这样我们可以使用泛型的子类型来代替泛型,因为只需要产出的地方,子类可以替换父类

interface Source<out T> {

fun nextT: T

}


fun demo(strs: Source<String>) {

val objects: Source<Any> = strs // 这个没问题,因为 T 是一个 out-参数

// ……

}

类似的道理,消费者我们可以使用相反的修饰符in

interface Comparable<in T> {

operator fun compareTo(other: T): Int

}


fun demo(x: Comparable<Number>) {

x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型

// 因此,我们可以将 x 赋给类型为 Comparable <Double> 的变量

val y: Comparable<Double> = x // OK!

}

这里说明一下使用<T:Any>的形式表示泛型的继承关系,即泛型的上界。

如果一个泛型有多个上界,可以使用where修饰符来表示

fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String>

where T : CharSequence,

T : Comparable<T> {

return list.filter { it > threshold }.map { it.toString }

}

同一个类或接口有多个泛型时使用,分割,例如:

interface Function <in T, out U>

类型投影


上面讲的声明处型变是在声明时表明我们的泛型类型,而类型投影则是在使用时,我们看下面的例子

class Array<T>(val size: Int) {

fun get(index: Int): T { …… }

fun set(index: Int, value: T) { …… }

}


fun copy(from: Array<Any>, to: Array<Any>) {

assert(from.size == to.size)

for (i in from.indices)

to[i] = from[i]

}

这是一个copy数组的方法,from和to都使用的是Any作为泛型,通过上面的学习我们知道,由于Array既不是生产者也不是消费者,我们没有办法使用声明时型变,也就是说我们的两个入参都必须是Any类型,这显然不是我们想要的。


那么如果我们让from传入的类型是Any的子类,而非只能是Any,要如何做呢,这时就需要使用类型投影,修饰符为out,表示接受的参数类型的上限

fun copy(from: Array<out Any>, to: Array<Any>) { …… }

同样修饰符in表示接受参数类型的下限

fun fill(dest: Array<in String>, value: String) { …… }

星参数


星参数是在使用时清楚泛型的具体类型


  • 对于 Foo <out T : TUpper>,其中 T 是一个具有上界 TUpper 的协变类型参数,Foo <*> 等价于 Foo <out TUpper>。 这意味着当 T 未知时,你可以安全地从 Foo <*> 读取 TUpper 的值。

  • 对于 Foo <in T>,其中 T 是一个逆变类型参数,Foo <*> 等价于 Foo <in Nothing>。 这意味着当 T 未知时,没有什么可以以安全的方式写入 Foo <*>。

  • 对于 Foo <T : TUpper>,其中 T 是一个具有上界 TUpper 的不型变类型参数,Foo<*> 对于读取值时等价于 Foo<out TUpper> 而对于写值时等价于 Foo<in Nothing>。


泛型函数


不只是类和接口,函数同样可以使用泛型

fun <T> singletonList(item: T): List<T> {

// ……

}


fun <T> T.basicToString: String { // 扩展函数

// ……

}

泛型擦除


Kotlin中的泛型都是在编译时进行安全性校验,编译结束欧都会擦除为<*>即无上下限的泛型,所以编译器禁止运行时用is进行类型校验




本章就先介绍到这,将来会陆续整理Kotlin中其他的部分。