这是Kotlin Koans学习笔记的第三篇。
第一篇在这里,第二篇在这里。
这一部分一共7个任务,所有的任务都是围绕日期展开,日期对象具有年、月、日三个属性:
data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int)
这一部分的任务主要是练习Kotlin对象运算符的重载。
Kotlin有固定数目的符号运算符,我们可以很简单的将这些运算符应用到任何一个类。具体的实现方法就是:对需要实现的运算符以Kotlin保留的函数名创建一个函数,这个运算符就会自动映射到相应的函数。为了提醒编译器我们重载了一个运算符,我们需要将重载函数标记为operator
。 在后面的任务中我们可以看到这一特性将大大的提高代码的简洁性和可读性。
这里先列出全部的运算符和它对应的函数。某一个类如果要使用某一个运算符,就需要实现相应的函数。
单目运算符
表达式 | 对应的函数 |
---|---|
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
a++ | a.inc() |
a-- | a.dec() |
双目运算符
表达式 | 对应的函数 |
---|---|
a+b | a.plus(b) |
a-b | a.minus(b) |
a*b | a.times(b) |
a/b | a.div(b) |
a%b | a.mod(b) |
a..b | a.rangeTo(b) |
a in b | b.contains(a) |
a !in b | !b.contains(a) |
a+=b | a.plusAssign(b) |
a-=b | a.minusAssign(b) |
a*=b | a.timesAssign(b) |
a/=b | a.divAssign(b) |
a%=b | a.modAssign(b) |
类数组(Array-like)运算符
表达式 | 对应函数 |
---|---|
a[i] | a.get(i) |
a[i,j] | a.get(i,j) |
a[i_1, ..., i_n] | a.get(i_1, ..., i_n) |
a[i]=b | a.set(i,b) |
a[i,j]=b | a.set(i,j,b) |
a[i_1, ..., i_n]=b | a.set(i_1, ..., i_n, b) |
相等运算符
表达式 | 对应的函数 |
---|---|
a==b | a?.equals(b) ?:b === null |
a==b | !(a?.equals(b) ?:b === null) |
函数调用
表达式 | 对应的函数 |
---|---|
a(i) | a.invoke(i) |
a(i,j) | a.invoke(i,j) |
a(i_1, ..., i_n) | a.invoke(i_1, ..., i_n) |
这一部分的任务主要就是实现上面的这些运算符函数。
25.Comparsion
第一个任务要求实现日期对象大小的比较,如下代码能够返回比较结果:
fun task25(date1: MyDate, date2: MyDate): Boolean {
//todoTask25()
return date1 < date2
}
代码不做任何修改是编译不过的,编译器会提示MyDate
类没有实现相关的函数。任务的提示也很清楚,MyDate
类实现Comparable
接口即可:
data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int): Comparable<MyDate> {
override fun compareTo(other: MyDate) =
when{
other.year != year -> year - other.year
other.month != month -> month - other.month
else -> dayOfMonth - other.dayOfMonth
}
}
通过实现Comparable接口,就可以在代码中直接使用大小比较运算符。
26.InRange
这个任务的要求编码实现检查指定的日期是不是在某一个日期范围内:
fun checkInRange(date: MyDate, first: MyDate, last: MyDate): Boolean {
//todoTask26_()
return date in DateRange(first, last)
}
根据前面的表格知道:a in b
翻译以后就是b.contains(a)
,所以这里的任务就转换成实现DateRange
类的contains(d: MyDate):Boolean
函数。DateRange类的定义已经给出来了,它包含一个起始日期(start
)和一个截止日期(endInclusive
)。由于上一个任务已经实现了日期大小的比较,所以这个任务也就很好完成:
operator fun contains(d: MyDate) = (d>=start && d<=endInclusive)
注意函数定义前面的operator
修饰符。
27.RangeTo
这一个任务是要求实现MyDate
类的..
运算符。..
运算符最终会翻译成rangeTo()
函数,所以本任务就是实现MyDate.rangeTo()
,由于在上一个任务中DateRange
已经实现了operator fun contains(d: MyDate)
。所以rangeTo()
函数返回一个DateRange
对象即可:
operator fun MyDate.rangeTo(other: MyDate): DateRange = DateRange(this, other)
fun checkInRange2(date: MyDate, first: MyDate, last: MyDate): Boolean {
//todoTask27()
return date in first..last
}
28.ForLoop
任务要求可以对DateRange内的MyDate执行for循环,也就是要求DataRange
实现Iterable<MyDate>
接口。任务描述中也给出了足够的提示。日期范围的迭代以天
为迭代间隔,所以需要使用已经定义好的nextDay()
方法,一次递增一天:
fun MyDate.nextDay() = addTimeIntervals(DAY, 1)
Iterator<MyDate>
接口有两个方法next()
和hasNext()
:
lass DateRange(val start: MyDate, val endInclusive: MyDate) : Iterable<MyDate>{
...
override fun iterator(): Iterator<MyDate> = DateIterator(this)
}
class DateIterator(val dateRange:DateRange) : Iterator<MyDate> {
var current: MyDate = dateRange.start
override fun next(): MyDate {
val result = current
current = current.nextDay()
return result
}
override fun hasNext(): Boolean = current <= dateRange.endInclusive
}
29.OperatorOverloading
这一个任务包含两个任务,主要练习重载运算符。第一个小任务是重载MyDate
的+
运算符:
fun task29_1(today: MyDate): MyDate {
// todoTask29()
return today + YEAR + WEEK
MyDate
可以和一个时间间隔相加,所以需要实现MyDate.plus()
函数,以时间间隔为参数,有了上一个任务计算下一天的基础,实现和时间间隔相加就比较容易了:
operator fun MyDate.plus(timeInterval: TimeInterval) = addTimeIntervals(timeInterval, 1)
第二个小任务在第一个小任务的基础上更进一步,不再是加一个单一的间隔,要求相加多个时间间隔,如加3年,加7个星期。。。所以需要先实现时间间隔的*
运算符,将TimeInterval
的乘法结果定义成RepeatedTimeInterval
:
class RepeatedTimeInterval(val timeInterval: TimeInterval, val number: Int)
operator fun TimeInterval.times(number: Int) = RepeatedTimeInterval(this, number)
由于这里*
运算返回的是一个RepeatedTimeInterval
对象,所以还需要实现MyDate
和RepeatedTimeInterval
相加:
operator fun MyDate.plus(timeIntervals: RepeatedTimeInterval) = addTimeIntervals(timeIntervals.timeInterval, timeIntervals.number)
仍然是使用已有的addTimeIntervals
方法, 将RepeatedTimeInterval
对象的两个属性作为该方法的两个参数。这样,就可以实现以下这样的表达式运算:
fun task29_2(today: MyDate): MyDate {
//todoTask29()
return today + YEAR * 2 + WEEK * 3 + DAY * 5
}
30.Destructuring Declaration
Kotlin可以将一个对象的所有属性一次赋值给一堆变量:
val (name, age) = person
这段代码会被编译成:
val name = person.component1()
val age = person.component2()
这就是为什么DataClass
会自动生成componentN()
函数。
任务的要求是判断一个日期是否是闰年,第一步需要将时间的属性赋值给年、月、日。这就需要使用destructuring declaration
。
<pre>
data class MyDate(val year: Int, val month: Int, val dayOfMonth: Int)
fun isLeapDay(date: MyDate): Boolean {
//todoTask30()
val (year, month, dayOfMonth) = date
// 29 February of a leap year
return year % 4 == 0 && month == 2 && dayOfMonth == 29
}
</pre>
注意如果需要使用Destructuring Declaration,类必须声明为data
类型。
31.Invoke
终于到了这一部分的最后一个任务,invoke。Invoke是什么呢?如果一个类实现了invoke()
这个函数,那么该类的实体对象在调用这个函数时可以省略函数名,当然invoke
函数必须要有operator
修饰符。先看一下栗子吧:
class customClass{
operator fun invoke(param: Int){
println("print in invoke method, param is :"+param)
}
operator fun invoke(param: Int, msg: String): customClass{
println("print in invoke method, param is :"+param+",message is:" + msg)
return this
}
}
那么就可以这样使用:
val printer = customClass()
printer(2)
printer.invoke(4)
printer(12, "first")(23, "second")(46)
执行结果就是:
print in invoke method, param is :2
print in invoke method, param is :4
print in invoke method, param is :12,message is:first
print in invoke method, param is :23,message is:second
print in invoke method, param is :46
回到我们的任务,要求如下代码返回的结果为4:
fun task31(invokable: Invokable): Int {
//todoTask31()
return invokable()()()().getNumberOfInvocations()
}
所以需要实现Invokable
的invoke
方法,该方法每调用一次,内部计数器就需要加1,最后返回计数器的值:
class Invokable {
var numberOfInvocations: Int = 0
operator fun invoke(): Invokable {
numberOfInvocations++
return this
}
}