Kotlin速成

接触Kotlin是因为想实现一个手机APP,又不想去碰Java,于是通过tutorial尝试入门看看。

其实关于手机APP的想法已经暂时放弃了,对Kotlin的尝试也停止了。把当时记录的笔记记在这里,方便以后再捡起来的时候快速回忆起来。

hello world

fun main() {
    println("Hello, world!")
}

变量

// 声明常量用 val
val procorn = 5

// 声明变量用 var
var customers = 10

字符串模板

在字符串中引用变量与shell脚本类似

val customers = 10

println("There are $customers customers")
println("There are ${customers + 1} customers")

基本的数据类

  • Byte, Short, Int, Long

  • UByte, UShort, UInt, ULong

  • Float, Double

  • String

  • Char

  • IntRange

  • Boolean

复合数据类型

  • 列表(List)

  • 集合(Set)

  • 映射(Map)

// 声明只读的列表使用 listOf
val readOnlyShape = listOf("triangle", "square", "circle")

// 声明可变的列表使用 mutableListOf
val shapes: MutableList<String> = mutableListOf("triangle, "square", "circle)

println(shapes)
// [triangle, square, circle]

列表支持的方法

.first(), .last(), .count(), .add(anObject), .remove(anObject)

检测某个对象是不是在列表中

val readOnlyShapes = listOf("triangle", "square", "circle")
println("circle" in readOnlyShapes)

集合是无序的,只能包含非重复对象,同样分为只读和可变的集合

val readOnlyFruit = setOf("apple", "banana", "cherry", "cherry")
val fruit: MutableSet<String> = mutableSetOf("apple", "banana", "cherry", "cherry")

println(fruit)
// [apple, banana, cherry]

映射相当于字典,或者是哈希表, 同样分为只读和可变的

val readOnlyJuiceMenu = mapOf("apple" to 100, "kiwi" to 190, "orange" to 100)

var juiceMenu: MutableMap<String, Int> = mutableMapOf("apple" to 100, "kiwi" to 190, "orange" to 100)

println(juiceMenu)
// {apple=100, kiwi=190, orange=100}

访问映射成员的语法和Python的字典一样,使用[aKey]操作符。当键不存在时,得到null值。

从映射中删除某个键可以调用 .remove(aKey)方法

.count()方法返回计数

访问 .keys 和 .values 属性可以得到键和值的列表,注意,是属性而不是方法。

containsKey(aKey) 进行成员测试,返回 Boolean 值。

containsKey方法只能测试键在不在映射中,使用in配合keys, values可以测试键和值是否在映射中

aKey in aMap.keys

anObject in aMap.values

控制流

if

语法上,Kotlin的if与C系列的语言相同。语义上,Kotlin的if是表达式而不是语句。

if (aBoolean) {
    ...
} else {
    ...
}

val a = 1
val b = 2

println(if (a > b) a else b)
// 2

when

when类似于switch

val obj = "Hello"

when (obj) {
    "1" -> println("One")
    
    "Hello" -> println("Greeting")
    
    else -> println("Unknown")
}

//-> Greeting

和if一样,when也是表达式而不是语句。

when的分支条件不仅仅是简单的对象比对,它似乎支持完整的布尔运算(或模式匹配?)。

区间

使用..运算符可以生成一个区间:1..4,得到的区间包括1,2,3,4

如果要得到一个不包括结束值的区间,使用..<代替,1..<4得到的区间是1,2,3。这个操作和Python的range()函数基本等价了.

降序的区间使用downTo: 4 downTo 1得到区间4, 3, 2, 1

区间支持.random()方法,随机选择一个元素

循环

for in

for (i in 1..5) {
    print(i)
}

//-> 12345

for (cake in listOf("carrot", "cheese", "chocolate")) {
    println("Yummy, it's a $cake cake!")
}

// Yummy, it's a carrot cake!
// Yummy, it's a cheese cake!
// Yummy, it's a chocolate cake!

while && do-while

函数

fun functionName(arg1: type1, arg2: type2) : returnValueType {
    ...
    return anObject
}

关键字参数

调用函数时可以使用关键字传递参数, 使用关键字传递参数可以打乱参数顺序。

f(arg1 = anObject, arg2 = anObject)

默认值

在定义函数时可以定义默认值

fun funcName(arg1: type1, arg2: type2 = defaultValue) {
    ...
}

没有return的函数会返回一个特殊的值Unit

单行函数

fun sum(x: Int, y: Int) = x + y

所谓单行函数其实是个语法糖,语法糖过多,会提高代码阅读时的认知成本。既然有了lambda表达式,就没必要再提供单行函数了吧,只是少打了几个字而以。

val sum = {x: Int, y: int -> x + y}

lambda

lambda表达式相当于javascript的箭头函数,区别是参数不需要括号,函数体不需要花括号,整个表达式都包在花括号中。

lambda以最后一个表达式的值作为返回值,不需要的return

val upperCaseString = { text: String -> text.uppercase() }

lambda 表达式是典型的函数式编程特征。lambda表达式可以赋值给变量,可以作为参数进行传递,也可以作为函数的返回值。调用lambda的方法和调用函数一样,使用()进行调用和传递参数.

面向对象编程

类的实例变量可以在两个地方定义:

// 定义在类名称后面的参数表中
class Contact(val id: Int, var email: String)

// 定义在类后面的花括号中
class Contact(val id: Int, var email: String) {
    val category: String = ""
}

在上面的 Contact(val id: Int, var email: String)隐含地定义了一个与类同名的构造器函数,同时定义了两个实例变量以及两个初始化参数id和email。在构造器的参数表中,加了val或var前缀的参数自动成为实例属性,交者是常量,而后者是变量。并且在调用构造器时从参数中得到初值。至于定义在类体中的实例变量,则只能通过其它方式初始化。

和函数类似,类的实例变量也可以有默认值,通过默认值可以实现可选参数。通过可选参数,又可以有效避免使用复杂的构造器函数重载。

val contact = Contact(1, "mary@gmail.com")

除了默认的构造器,Kotlin还可以定义其它的构造器, 不同的构造器主要表现在参数的不同上,其实可以通过可选参数在单一构造器中达到相同的目的。

在类体中{},可以定义独立的初始化逻辑init { ... },还可以定义其它的实例变量,也可以定义方法。实例变量默认是public的,并且提供了访问器(val属性只有getter, 没有setter)。

实例变量默认是public的,可以从外部直接访问。

定义成员函数(方法)与定义成员变量是一样的,直接在花括号里定义 fun 就行了。

继承

在Kotlin中,要让一个类可以被继承,必须显式地声明为open,可继承的类里面,也只有声明的open的实例方法才能被继承。继承的语法是在主构造器的参数表后面加冒号,接着调用父类的构造器(如果父类需要初始化参数的话要传递正确的参数),接下来才是类体。Kotlin的类定义对灵活性做了很多考量,但是带来的结果是复杂度上升。

一个完整的类定义可以写成下面的形式:

// 定义一个类作为超类,该类需要两个初始化参数 arg1, arg2
open class SuperClass(var arg1: Type1, var arg2: Type2)

// arg1 和 arg2 是超类的参数,不能加 var 或 val 前辍, 加了就冲突
// 在子类的构造器参数表后面立即调用超类的构造器,并且把接收到的 arg1 和 arg2传递给它
class ChildClass(arg1 Type1, arg2: Type1, var arg3: Type3, var arg4: Type4) : SuperClass(arg1, arg2) {
    // 初始化逻辑写在下面
    init {
        ....
    }
    // 附加的构造器,必须先调用主构造器并传递必要的参数
    constructor(args) : this(args) {
        ...
    }
    // 方法定义
    fun aMehtod(args) {
        ....
    }
}

事实上,上面的写法也不完整,而是加了糖的。class ClassName(args)形式的主构造器,其实是class ClassName constructor(args)的简写. Kotlin真正的类定义语法形式是:

open class ClassName constructor(args) : SuperClass(args) {
    init { ... }
    constructor(args) : this(args) { ... }
    ...
    fun aMethod(args) { ... }
    ...
}

data类

data类自动拥有某些成员函数,比如.toString(), .equals(), .copy()

data class SomeClass(...)

Null值安全

在kotlin中有专门保护Null值安全的机制,该机制是在编译时,而不是运行时检测潜在的问题。

该机制允许你:

  • 在有可能出现null的地方需要明确地声明

  • 检查null值

  • 对可能产生null值的属性或函数使用安全调用

  • 声明在遇到null值时要采取的动作

声明允许为null的值

在类型后面加上一个问号

var neverNull: String = "This can't be null"

var nullable: String? = "You can keep a null here"
nullable = null

// 下面的代码在编译时会报错
var x = null

// 下面的函数不接受 null 值参数
fun strLength(notNull: String): Int {
    return notNull.length
}

println(strLength(neverNull)) // 18
println(strLength(nullable))  // 抛出编译错误

安全访问

要安全地访问可能包含null值的对象属性,可以使用?.操作符,如果对象或被访问的属性为空,则返回null,而不是报错。

fun stringLength(s: String?): Int? = s?.length

安全访问可以级联,只要某一级遇到null就提前返回

person.company?.address?.country

Elvis操作符

当遇到null值时,可以用Elvis操作符?:返回一个替代值

nullString?.length ?: 0

如果nullString为null,则返回0

Kotlin 的 Null 安全把原本依赖单元测去发现的问题移到了编译期,通过编译器的静态分析来检测。但是静态分析并不是完全可靠的。在某些情况下,你已经从逻辑上处理了所有可能出现空值的地方,但是编译器仍然会抱怨有空值没有处理。在这种情况下,可以在变量名后面使用非空断言!!来让编译器闭嘴。

评论

暂无评论