前言:
在上一篇文章中,我们详细的介绍了Kotlin中的基本数据类型,本篇文章我们继续来讲解Kotlin中的基础知识:属性和控制流。做了这么多年的安卓开发,回头一看,基础知识真的很重要。政治学中有经济基础决定上层建筑,笔者认为做什么行业都一样,首先就要打好基础,基础牢固,才能更好的做好一件事。下面我们开始本篇文章的介绍。
1.1属性
在Kotlin中我们使用关键字val
和var
来声明一个属性。val
声明的属性是只读的,var
声明的属性是可变的。默认情况下,属性必须设置初始值。标准的语法如下:
【可变/不可变】【属性名】 【属性类型】 【初始值】
var/val propertyName:propertyType = initValue
[getter]
[setter]
如果能从初始值或者[getter]
方法返回值中推断出类型,可以省略属性类型。 初始值、[getter]
、和[setter]
方法都是可选的。
val language:String = "kotlin"
// 推断出属性language的类型为String,可省略:String
val language = "kotlin"
val language:String
get() = "kotlin"
// 推断出[getter]方法的返回值类型为String,可省略:String
val language
get() = "kotin"
在Kotlin中[getter]
和[setter]
方法是允许自定义的。当我们自定义一个属性的[getter]
方法时,我们每次去访问这个属性,就会调用其自定义的[getter]
方法。
// 单行表达式可以完成
val info = "info"
// 单行表达式可以完成
val info get() = "info"
// 有额外的逻辑,自定义[getter]
val info:String
get() {
...相关业务逻辑
return result
}
其中用val
关键字声明的只读属性,不允许自定义[setter]
方法。
val language = "kotlin"
// 不允许自定义,会报语法错误
set(value) { }
var language = "kotlin"
// 允许自定义[setter]
set(value){ }
上面我们一直在强调用val
关键字声明的属性是只读的,这和Java中的final
关键字很类似。实事上在将val
声明的属性反编译成Java代码的时候也确实是用final
关键字修饰的。下面我们来验证一下这个问题,在Android Studio中选中当前项目,右击触摸板或者鼠标。在弹出的选择框中,New -> Kotlin Class/File选择File。首先创建一个Property.kt的File文件。然后声明两个String类型的属性language和developTool,如下:
var language:String = "kotlin"
val developTool = "Android Studio"
然后选择Tools -> Kotlin -> Show Kotlin ByteCode,在打开的视图中我们点击Decompile按钮。
最终得到反编译后的Java代码,可以看到用
val
修饰的属性developTool在Java中是使用final
关键字修饰的只读属性。
public final class PropertyKt {
@NotNull
private static String language = "kotlin";
@NotNull
private static final String developTool = "Android Studio";
// ...省略
}
1.2幕后字段field
对于一个Kotlin初学者来说,幕后字段可能不是那么好理解。笔者刚开始接触Kotlin的时候,也是有一样的感受。下面就让我们来揭开这个小秘密,在Property.kt的File文件中。编写如下代码:
var language:String = "kotlin" //注意:这个初始值直接为幕后字段赋值
set(value) {
if(value.length >0) {
field = value
} else {
field = ""
}
}
field
字段只能在我们自定义[setter]
方法的访问器中使用。可以理解是language属性的一个"幕后引用",当我们自定义一个属性的[setter]
方法时,给一个属性赋值时就会调用其自定义的[setter]
方法。如果我们在自定义[setter]
方法的访问器中直接给属性language赋值,那么代码就会进入一个死循环。如下代码,抛出StackOverflowError
的异常。
var language:String = "kotlin"
set(value) {
language = value
}
fun main() {
language = "java"
println("language = $language")
}
// 输出
Exception in thread "main" java.lang.StackOverflowError
at com.study.myapplication.PropertyKt.setLanguage(Property.kt:11)
at com.study.myapplication.PropertyKt.setLanguage(Property.kt:11)
at com.study.myapplication.PropertyKt.setLanguage(Property.kt:11)
而这正是Kotlin在[setter]
方法访问器中引入幕后字段field
的原因。我们在[setter]
方法的访问器中直接给filed
赋值,就不会再去调用属性的[setter]
方法,就能很好的规避了这个问题。
1.3可空属性
当我们需要声明一个属性为可空时,我们只需要在属性类型后添加?
// language属性可为空
var language:String? = null
// language属性不可为空
var language:String = "kotlin"
如果我们强行给一个不可空的属性赋值null
,kotlin编译器就会报语法错误。
var language:String = "kotlin"
var language2:String? = "java"
fun main() {
language = null // 错误
language2 = null // 正确
}
1.4延迟初始化属性
在kotlin中我们使用lateinit
关键字来完成一个属性的延迟初始化。
class MainActivity : AppCompatActivity() {
companion object {
const val TAG = "MainActivity"
}
private lateinit var language:String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
language = "kotlin"
if(::language.isInitialized) {
Log.d(TAG, "language = $language")
}
}
}
可以看到上面的代码中,刚开始我们并没有给属性language设置初始值,而是在onCreaet方法中完成了赋值操作。在初始化前访问一个 lateinit
修饰的属性会抛出lateinit property language has not been initialized
的异常。我们可以使用::propertyName.isInitialized
方法来判断属性是否已经初始化,::
符号则是Kotlin中反射的运用,用来获取属性的引用,后面的文章会详细讲解。
用lateinit修饰的属性类型不可设置可空类型,否则编译器会报语法错误
private lateinit var language:String //正确
private lateinit var language:String? //错误
1.5属性的空类型检查
空指针异常(NullPointException
)是我们实际开发过程中最容易发生的异常之一。对于声明一个可空类型的属性,我们最需要关注的就是在访问这个属性之前一定要判断这个属性是否已经被初始化。通常在Java中,我们都会在访问这个属性之前加上if语句的判断:
if(property != null) {
// ...逻辑
}
在Kotlin中对于访问一个可空属性,通常有两种方式:一种是断言双感叹号!!
,一种是?.
。断言!!
通常是你确定这个可空属性已经初始化,在访问可空属性的后面紧接着加上双感叹号!!
。?.
亦是如此。对于这两种方式,更推荐使用后者。在我们实际开发的过程中,当一个属性涉及的业务和逻辑足够复杂时,我们可能在某些特需的业务场景下将已经初始化的属性重新置空了,这样我们再去访问这个属性的一些方法,那就很危险了,无疑是接受空指针异常的到来。如下示例代码:
// !!不推荐使用,除非你确定在所有访问该属性之前,该属性已经初始化
var language:String? = "kotlin"
language!!.length
// ?.推荐使用
var language:String? = "kotlin"
language?.length
2.1if语句
在Kotlin中if
语句不仅仅只是做逻辑判断,它还可以是一个表达式。这可能对于一个初学Kotlin的读者来说不是很好理解,但是在Kotlin中这是一个很好用的语法糖。让我们来看下如下的代码示例:
fun main() {
val a = 1
val b = 3
val max:Int = if (a > b) {
println("a = $a")
a
} else {
println("b = $b")
b
}
println(max)
}
// 输出
b = 3
max = 3
我们声明了两个Int类型的属性a和b,用if
表达式判断来取两者的较大者。在Java中的if
语句是没有这个语法糖的,我们只能用三元运算符(条件 ? 然后 : 否则)。如果if
分支中的代码块只有一行表达式,我们还可以省略花括号,最后的表达式作为该块的值。通常Kotlin编译器可以推断出if
表达式的返回值类型,可以省略属性类型。
val max:Int = if (a > b) { a } else { b }
// 可简写为
val max:Int = if (a > b) a else b
// 推断出右侧为Int类型,可省略属性类型
val max = if (a > b) a else b
2.2when语句
和if
语句一样,在Kotlin中when
可以是普通的语句,也可以是表达式。当when
作为表达式时,每个条件可以有自己的块,如果块中只有一行表达式也可以省略花括号,而且必须要有else
块。除非编译器能识别,条件覆盖了所有的类型,可以省略else
块(比如枚举、密封类saled
class)这在后面的文章会详细讲解到。
// 作为语句,无需返回值,顺序比较每一个条件分支,执行符合条件的分支块
when(language) {
"kotlin" -> { println("the kotlin language") }
"java" -> { println("the java language") }
"ios" -> { println("the ios language") }
}
// 作为表达式,需要有else块,除非编译器能识别覆盖到所有的条件,可省略else块
val result = when(language) {
"kotlin" -> { println("the kotlin language") }
"java" -> { println("the java language") }
"ios" -> { println("the ios language") }
else -> { println("else logic") }
}
// 满足条件的块中只有一行表达式,可省略分支块的花括号
val result = when(language) {
"kotlin" -> "kotlin language"
"java" -> "java language"
"ios" -> "ios language"
else -> "kotlin language"
}
我们也可以使用(in
)或者(!in
)的区间作为条件分支,也可以使用is
。(在kotlin中关键字in是一个操作符,a in b相当于b.contains(a),关于操作符和操作符的重载后面的文章会详细介绍。is
关键字则是取代了java中的instanceof
关键字)
// 使用区间作为条件判断
when(number) {
in 0..10 -> { }
!in 10..20 -> { }
else -> { }
}
// 使用is作为条件判断
when(type) {
is String -> { }
is Int -> { }
else -> { }
}
Ktotlin1.3版本后,when
后面紧跟括号中的变量还可以是一个表达式。可以从表达式中推断出变量的类型:
when(type = getType()) {
"kotlin" -> println("the kotlin language")
"java" -> println("the java language")
"ios" -> println("the ios language")
else -> println("the else logic")
}
2.3for循环
对任何提供迭代器(iterator)的对象我们都可以使用for循环。语法如下:
for (item: Int in ints) {
// ……
}
我们也可以对一个区间进行for循环
// [0,10]闭区间
for(i:Int in 0..1) {
}
// [0,10)开区间
for(i:Int in 0 until 10) {
}
如果你想要通过索引遍历一个数组或者一个 list,可以使用_Arrays.kt文件中提供的扩展属性indices或者扩展方法withIndex()
// 扩展属性
public val <T> Array<out T>.indices: IntRange
get() = IntRange(0, lastIndex)
for(i in array.indices) {
//...
}
// 扩展方法
public fun <T> Array<out T>.withIndex(): Iterable<IndexedValue<T>> {
return IndexingIterable { iterator() }
}
for(i in array.withIndex()) {
//..
}
在Kotlin中map有如下几种常用的遍历方法:
// 仅需要遍历key
for(key in map.keys) {
println("only need key")
}
// 仅需要遍历value
for(value in map.values) {
println("only need value")
}
// key和value都需要遍历
for((key, value) in map) {
println("need key and value")
}
其中(key, value)是一种解构声名,这在后面的文章我们会详细说到。
2.4while和do..while循环
while
和do..while
与Java语言对比起来,没有任何的改变。对于while
循环,仍然是先执行判断语句,再执行循环体:
// 先执行判断条件
while (a < b) {
// ...
}
对于do..while循环,则是先执行循环体,在执行判断语句。
do {
// 先执行循环体,在执行条件判断
} while (a < b)
2.5break和continue
break
和continue
都是控制循环跳转的关键字。关于break
则是终止直接包含它的循环体,而continue
则是直接跳过本次循环,进入下一次循环:
fun main() {
println("break result")
for (i in 0..2) {
// ...
if (i == 1) {
break
}
println("i = $i")
// ...
}
println("continue result")
for (i in 0..2) {
// ...
if (i == 1) {
continue
}
println("i = $i")
// ...
}
}
// 输出
break result
i = 0
continue result
i = 0
i = 2
总结
基础知识的学习是枯燥无味的,但是只有把基础知识学扎实了。我们才能在实际开发中游刃有余。本篇文章我们主要学习了Kotlin中属性的声明和使用以及可空属性的声明和空类型的安全检测。if
、when
表达式,for
、while
、do..while
的循环,break
和continue
控制循环的终止和跳转。这在我们后续开发中会很频繁的用到,扎实的掌握了这些基础,对我们的后续开发很有帮助。
到这里本篇文章就结束了,我们下期再见。