Loading

Kotlin Koans中值得记录的一些点

Nothing Type

Nothing类型适用于一个永远抛出异常的方法。当你调用了返回Nothing的方法之后,编译器就会知道,该行代码往下都不会到达。

import java.lang.IllegalArgumentException

fun failWithWrongAge(age: Int?): Nothing {
    throw IllegalArgumentException("Wrong age: $age")
}

fun checkAge(age: Int?) {
    if (age == null || age !in 0..150) failWithWrongAge(age)
    // 上面的if检查age是否不在合法范围,如果不在,调用`failWithWrongAge`函数抛出异常。
    // 如果抛出了异常,那么肯定不会走到下面,所以编译器不会在乎此时的age是一个可空类型
    println("Congrats! Next year you'll be ${age + 1}.")
}

fun main() {
    checkAge(10)
}

Data Classes

之前一直不理解为什么要有data类,比如如下情况,貌似Person不声明成data类也无妨。

data class Person(val name: String, val age: Int)

fun getPeople(): List<Person> {
    return listOf(Person("Alice", 29), Person("Bob", 31))
}

其实data类和普通类不同的是data类会根据类的属性自动生成equalshashCodetoString方法,恰好类似Pojo的类都需要这些方法。

data class Person(val name: String, val age: Int)

fun comparePeople(): Boolean {
    val p1 = Person("Alice", 29)
    val p2 = Person("Alice", 29)
    return p1 == p2  // 如果不将Person声明成data,那么这里的比较不可能返回true
}

Sealed Classes

fun eval(expr: Expr): Int =
        when (expr) {
            is Num -> expr.value
            is Sum -> eval(expr.left) + eval(expr.right)
            else -> throw IllegalArgumentException("Unknown expression")
        }

interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr

这个例子中,Expr没有除了NumSum之外其它的实现类,但是eval中的when也必须有一个else分支,来应对其它情况。这是因为Expr可能在其他文件中有其他实现类,所以Kotlin编译器认为is Numis Sum分支并不是全部可能。

密封类的作用就是让一个类不能在该类声明的文件外部被继承或实现,只要我们给Expr接口变成密封接口,Kotlin编译器就知道除了这两个实现类以外,Expr不可能再出现其它实现类了,所以when中的else分支可以省去

fun eval(expr: Expr): Int =
        when (expr) {
            is Num -> expr.value
            is Sum -> eval(expr.left) + eval(expr.right)
        }

sealed interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr

Rename on import

import kotlin.random.Random as KRandom
import java.util.Random as JRandom

fun useDifferentRandomClasses(): String {
    return "Kotlin random: " +
            KRandom.nextInt(2) +
            " Java random:" +
            JRandom().nextInt(2) +
            "."
}

Comparison

这没啥可记的主要是下一个依赖了这个,这就是个运算符重载,让MyDate具有了可以比较大小的能力。

data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int) : Comparable<MyDate> {
    override operator fun compareTo(other: MyDate) = when {
        year != other.year -> year.compareTo(other.year)
        month != other.month -> month.compareTo(other.month)
        else -> dayOfMonth.compareTo(other.dayOfMonth)
    }
}

fun test(date1: MyDate, date2: MyDate) {
    // this code should compile:
    println(date1 < date2)
}

Ranges

在Kotlin中,可以通过任意Comparable类型构建Range

fun checkInRange(date: MyDate, first: MyDate, last: MyDate): Boolean {
    return date in first..last
}

For loop

如果希望你的类型能够在for循环中使用范围,那么你可以重载rangeTo这个操作符,并返回一个Iterable对象。

然后你便可以使用它了。

class DateRange(val start: MyDate, val end: MyDate) : Iterable<MyDate> {
    override fun iterator(): Iterator<MyDate> {
        var nextDate = start;
        return object : Iterator<MyDate> {
            override fun hasNext() = nextDate <= end

            override fun next(): MyDate {
                val result = nextDate
                nextDate = nextDate.followingDate()
                return result
            }
        }
    }
}

fun iterateOverDateRange(firstDate: MyDate, secondDate: MyDate, handler: (MyDate) -> Unit) {
    for (date in firstDate..secondDate) {
        handler(date)
    }
}

Associate

List、Set等一维数据类型可以通过associate方法转换成二维表(Map)

// 构建一个从客户姓名到客户的映射
fun Shop.nameToCustomerMap(): Map<String, Customer> =
        customers.associateBy { it.name }

// 构建一个从客户到客户所在城市的映射
fun Shop.customerToCityMap(): Map<Customer, City> =
        customers.associateWith { it.city }

// 构建一个从客户姓名到客户所在城市的映射
fun Shop.customerNameToCityMap(): Map<String, City> =
        customers.associate { it.name to it.city }

Associate - Kotlin文档

Partition

Partition可以根据一个谓词对列表进行分区操作,条件成立的分为一个子列表,条件不成立的分到另一个子列表。

val numbers = listOf(1, 3, -4, 2, -11)
val (positive, negative) =
    numbers.partition { it > 0 }

positive == listOf(1, 3, 2)
negative == listOf(-4, -11)
// 返回未交付订单数多于已交付订单数的客户
fun Shop.getCustomersWithMoreUndeliveredOrders(): Set<Customer> = customers.filter {
    val (delivered, undelivered) = it.orders.partition { it.isDelivered }
    undelivered.size > delivered.size
}.toSet()

FlatMap

与map不同的是,map是将一种数据类型映射成另一种数据类型,比如:

listOf(1, 2, 3).map { it.toString() } == listOf("1", "2", "3")

如果应对下面的情况,map不太好用:

customers.map { it.orders }

上面代码的意图是获得所有客户订购的订单列表,我们想要一个List<Order>,但是由于map只能把一种类型映射成另一种类型,所以现在的结果是List<List<Order>>

flatMap的作用就是把里面的内容展开

// 返回所有客户订购的订单列表
fun Customer.getOrderedProducts(): List<Product> =
    orders.flatMap { it.products }

// 返回至少被一个客户订购了的产品
fun Shop.getOrderedProducts(): Set<Product> =
    customers.flatMap { it.orders }.flatMap { it.products }.toSet()

Properties

class PropertyExample() {
    var counter = 0
    var propertyWithCounter: Int? = null
        set(value) {
            // 这里用隐含变量field!!!否则就死循环。。。就栈溢出
            field = value
            counter++
        }
}

Delegates example

LazyProperty是一个懒加载属性,由用户传入的initializer来动态计算。

class LazyProperty(val initializer: () -> Int) {
    private var _lazy: Int? = null
    val lazy: Int
        get() {
            if (_lazy == null) _lazy = initializer()
            return _lazy!!
        }
}

可以这样使用这个类:

val property = LazyProperty {
    // 进行一些复杂运算,然后返回结果
    1
}


// 只有当实际访问时才会进行运算,并且运算只做一次
property.lazy

上面自己实现的LazyProperty已经很好了,但是在访问时还必须要使用property.lazy这样表意不清淅的代码。对于懒加载属性,Kotlin提供了语言原生的支持

val property by lazy { 
    // 进行一些复杂运算
    1
}

property

懒加载属性使用了委托技术,它的原理就是提供一个私有变量做实际值的存储,然后再提供一个公有变量,这个公有变量再去委托私有变量去做实际的储值功能。委托技术隐藏了私有变量的实现,并在上面添加了一层,我们可以在这一层中做些事情。例如上面的懒加载,我们就利用这一层保证了懒加载逻辑只被加载一次。

在Kotlin里,可以通过操作符重载让你的类支持被其它代码委托:

class Lazy<T>(private val initializer: () -> T) {
    var value: T? = null

    operator fun getValue(thisRef: T?, property: KProperty<*>): T {
        if (value == null) value = initializer()
        return value!!
    }

}

这里重载了getValue,现在我们的Lazy也可以像官方的一样使用by来工作了

val p by Lazy {
    PropertyExample()
}

上面我们的Lazy只重载了getValue运算符,这代表它只能被val委托,因为我们没有给出委托的发起者p被赋值时应该做什么。

如果想要让var也能够委托,那么需要重载setValue运算符。

Kotlin1.4支持直接将一个属性的getter和setter委托给类中的另一个属性。

class MyClass(var memberInt: Int, val anotherClassInstance: ClassWithDelegate) {
    var delegatedToMember: Int by this::memberInt
    var delegatedToTopLevel: Int by ::topLevelInt
    
    val delegatedToAnotherClass: Int by anotherClassInstance::anotherClassInt
}
var MyClass.extDelegated: Int by ::topLevelInt

思来想去,我们在写LiveData时应该可以这样写了:

private val _userSelected: MutableLiveData<User> = MutableLiveData()
val userSelected: LiveData<User> by this::_userSelected
//        get() = _userSelected

上面是我猜测的,还没有经过实证,不过应该没什么问题。。。这样写除了::符号有些难看之外,代码的可读性有所提升。

还有一个问题就是,编写可委托类时要重载getValuesetValue。这两个方法的签名很丑,很容易写错,我刚刚就因为写错一个?找了半天。可以通过继承ReadOnlyPropertyReadWriteProperty来获得编辑器的支持,继承之后就可以让编辑器帮你填写该实现的方法的签名了。

委托属性——Kotlin官方文档

posted @ 2022-01-10 11:03  yudoge  阅读(85)  评论(0编辑  收藏  举报