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类会根据类的属性自动生成equals
、hashCode
和toString
方法,恰好类似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没有除了Num
和Sum
之外其它的实现类,但是eval
中的when
也必须有一个else
分支,来应对其它情况。这是因为Expr可能在其他文件中有其他实现类,所以Kotlin编译器认为is Num
和is 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 }
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
上面是我猜测的,还没有经过实证,不过应该没什么问题。。。这样写除了::
符号有些难看之外,代码的可读性有所提升。
还有一个问题就是,编写可委托类时要重载getValue
和setValue
。这两个方法的签名很丑,很容易写错,我刚刚就因为写错一个?
找了半天。可以通过继承ReadOnlyProperty
和ReadWriteProperty
来获得编辑器的支持,继承之后就可以让编辑器帮你填写该实现的方法的签名了。
作者:Yudoge
出处:https://www.cnblogs.com/lilpig/p/15783610.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
欢迎按协议规定转载,方便的话,发个站内信给我嗷~
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· winform 绘制太阳,地球,月球 运作规律
· 上周热点回顾(3.3-3.9)