Kotlin Koans学习笔记(1)

96
唐先僧
0.1 2017.03.04 09:21* 字数 3028

Kotlin Koans是Kotlin官方推出的一系列Kotlin语法练习。一共42个任务,分为6个模块。每一个任务都有一系列单元测试,需要完成的任务就是编码通过单元测试。本文是在学习Kotlin Koans过程中将相关语法点做一个简单的记录。

写在前面,不少童鞋在实际使用中出现了如下错误:

Process finished with exit code 1
Class not found: "i_introduction._0_Hello_World._00_Start"Empty test suite.

我本人也复现了这一个错误,最终按照kotlin-koans 官方的文档重新导入就可以:

How to build and run tests:

  • 1、Working with the project using Intellij IDEA or Android Studio:
    Import the project as Gradle project.
  • 2、To build the project and run tests use test
    task on Gradle panel.

怎么导入为 gralde 工程参考下图


选择第4个菜单导入

有网友问关于单元测试操作的问题,我使用的是 Android Studio。单元测试的操作我贴两张截图说一下吧


运行单元测试方法1

运行单元测试方法2

0.HelloWorld

和所有其他语言一样,Kotlin Koans的第一个任务名称就是Hello World,这个任务比较简单,提示也说的很清楚,就是要求task0函数返回一个字符串OK

fun task0(): String {
    return "OK"
}

这一个任务主要涉及kotlin的函数定义。在kotlin中函数通过关键字fun声明,和Java中函数的返回类型写在函数名称前不一样,Kotlin中函数的返回类型在函数名称的后面,中间以:分开。Kotlin中的函数总是返回一个值,如果不指定返回值的类型,默认返回Uint(类似Java中的Void)。如果函数体就是一个简单的语句,可以去掉大括弧,用等号表示:

fun task0(): String = "OK"

1.Java to Kotlin Convert

这个任务的要求就是将一段Java代码转换成Kotlin代码,提示可以直接将Java代码复制粘贴,然后使用Intellij提供的Convert Java File to Kotlin File功能(仅仅是这个任务允许这样做),非常便捷。

//Java
public String task1(Collection<Integer> collection) {
        StringBuilder sb = new StringBuilder();
        sb.append("{");
        Iterator<Integer> iterator = collection.iterator();
        while (iterator.hasNext()) {
            Integer element = iterator.next();
            sb.append(element);
            if (iterator.hasNext()) {
                sb.append(", ");
            }
        }
        sb.append("}");
        return sb.toString();
    }
    
//Kotlin

fun todoTask1(collection: Collection<Int>): String
        {
            val sb = StringBuilder()
            sb.append("{")
            val iterator = collection.iterator()
            while (iterator.hasNext()) {
                val element = iterator.next()
                sb.append(element)
                if (iterator.hasNext()) {
                    sb.append(", ")
                }
            }
            sb.append("}")
            return sb.toString()
        }

这一段代码两者之间没有明显的差别,但是在下一个任务中可以看到Kotlin中这一段代码可以精简成一行代码。

2.Named Arguments (命名参数)

任务的要求是使用Kotlin提供的方法joinToString()重新完成任务1,只指定joinToString的参数中的两个参数。

这里涉及到Kotlin函数的默认参数(Default Arguments)和命名参数(Named Arguments)两个语法。

Kotlin中函数参数可以有默认值,当函数被调用时,如果没有传递对应的参数,那么就使用默认值。和其他语言相比,这以功能可以大大的减少重载函数的数目。参数的默认值在参数的类型后面通过=赋值。重写函数(overriding method)使用和被重写函数相同的默认参数。也就是说当我们重写一个有默认参数的函数时,我们不允许重新指定默认参数的值

当我们在调用函数时,可以为传递的参数命名,这在当一个函数的参数很多或者函数参数具有默认值的时候非常方便。

让我们回到任务本身,该任务要求使用joinToString函数重新完成任务1,并且只能指定两个参数。

我们来看一下joinToString函数的定义:

/**
 * Creates a string from all the elements separated using [separator] and using the given [prefix] and [postfix] if supplied.
 * 
 * If the collection could be huge, you can specify a non-negative value of [limit], in which case only the first [limit]
 * elements will be appended, followed by the [truncated] string (which defaults to "...").
 */
public fun <T> Iterable<T>.joinToString(separator: CharSequence = ", ", prefix: CharSequence = "", postfix: CharSequence = "", limit: Int = -1, truncated: CharSequence = "...", transform: ((T) -> CharSequence)? = null): String {
    return joinTo(StringBuilder(), separator, prefix, postfix, limit, truncated, transform).toString()
}

该函数对分隔符,前缀,后缀等其他参数都指定了默认值,我们再参考任务1中的描述,我们只需要重新指定前缀、后缀两个参数。命名参数通过在参数值的前面指定参数名称就可以,中间需要一个=

fun task2(collection: Collection<Int>): String {
    return collection.joinToString(prefix = "{", postfix = "}")
}

3.Default Arguments(默认参数)

默认参数的语法在前面已经做了介绍,直接来看任务。任务要求是将JavaCode3中所有的函数重载用一个函数替换。

public class JavaCode3 extends JavaCode {
    private int defaultNumber = 42;

    public String foo(String name, int number, boolean toUpperCase) {
        return (toUpperCase ? name.toUpperCase() : name) + number;
    }

    public String foo(String name, int number) {
        return foo(name, number, false);
    }

    public String foo(String name, boolean toUpperCase) {
        return foo(name, defaultNumber, toUpperCase);
    }

    public String foo(String name) {
        return foo(name, defaultNumber);
    }
}

所有的重载都是解决一个问题,字符串和数字的拼接,并且需要说明字母是否转换为大写,默认是不转换。Kotlin的实现:

fun foo(name: String, number: Int = 42, toUpperCase: Boolean = false): String {
    val upCaseName = if (toUpperCase) name.toUpperCase() else name
    return upCaseName+number.toString()
}

精简一下:

fun foo(name: String, number: Int = 42, toUpperCase: Boolean = false): String  
        = (if (toUpperCase) name.toUpperCase() else name)+number

4.Lambdas

这个任务的要求是用Kotlin重写JavaCode4.task4()函数,不允许使用Iterables类,可以通过Intellij IDEA的代码补全来选择合适的方法。

Java版本的代码:

public class JavaCode4 extends JavaCode {
    public boolean task4(Collection<Integer> collection) {
        return Iterables.any(collection, new Predicate<Integer>() {
            @Override
            public boolean apply(Integer element) {
                return element % 42 == 0;
            }
        });
    }
}

就是判断列表中是否包含42整数倍的元素,先实现功能:

fun task4(collection: Collection<Int>): Boolean  {
    val devide42: (Int) -> Boolean = { x -> x % 42 == 0 }
    return collection.filter(devide42).isEmpty().not()
}

这里使用了Collection的filter函数。这个任务主要学习Kotlin中Lambda表达式的知识,简单来说:

  • lambda表达式总是用大括弧包起来
  • 它的参数(如果有的话)在->符号前面声明(参数类型可以省略)
  • 函数体写在->符号后面

在Kotlin中,如果一个函数的最后一个参数是一个函数,并且你在调用该函数时最后一个参数传递的是一个lambda表达式,那么可以将这个lambda表达式写在括弧外面:

fun task4(collection: Collection<Int>): Boolean  {
    return collection.filter(){ x -> x % 42 == 0 }.isEmpty().not()
}

如果只有lambda表达式这一个参数,那么括弧也可以省略:

fun task4(collection: Collection<Int>): Boolean  {
    return collection.filter{ x -> x % 42 == 0 }.isEmpty().not()
}

如果lambda表达式也只有一个参数,那么这个参数连同->符号也可以省略,直接将它命名为it

fun task4(collection: Collection<Int>): Boolean = 
    collection.filter { it%42 ==0 }.isNotEmpty()

5.String Templates

任务要求生成一个正则表达式,可以匹配'13 JUN 1992'这样格式的字符串。
主要是学习Kotlin的各种字符串模板,Kotlin中字符串有两种,通过 一对"包起来自字符串,这里可以支持转义字符。如:

val s = "Hello, world!\n"

或者通过一对"""包起来的字符串,如:

val text = """
    for (c in "foo")
        print(c)
"""

字符串还可以包含模板表达式(template expressions),如:

val i = 10
val s = "i = $i" // evaluates to "i = 10"

val s1 = "abc"
val str = "$s1.length is ${s1.length}" // evaluates to "abc.length is 3"

val price = """
${'$'}9.99
"""

回到我们的任务本身,任务里面已经给了足够的提示,完成起来也比较容易:

val month = "(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)"
fun task5(): String = """\d{2} ${month} \d{4}"""

6.Data Class

任务要求将JavaCode6.Person类转换成Kotlin。先来看看Java源码:

public class JavaCode6 extends JavaCode {

    public static class Person {
        private final String name;
        private final int age;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }
    }
}

Kotlin实现:

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

Kotlin中Data class对应Java中的实体类,需要在定义类时标明为data,编译器会自动根据主构造函数中定义的属性生成下面这些成员函数:

  • equals()函数和hashCode()函数

  • toString()函数,返回的形式为"Person(name=TangSir, age=28)"

  • componentN()函数,componentN()具体返回的值根据类定义时属性定义的属性决定。如:

     val name = person.component1()
     val age = person.component2()
    
  • copy()函数

但是如果上面列出来的函数已经在类的实现中显式定义或者继承了父类相应的函数,编译器则不会生成相应的函数。

为了保持一致性以及编译器所生成代码具有意义,data class必须满足以下这些条件:

  • 主构造函数至少有一个参数
  • 主构造函数中的所有参数都必须定义为val或者var
  • Data class不能是abstract, open, sealed or inner

7.Nullable Type

任务要求将ava版本sendMessageToClient用Kotlin实现,只允许使用一个if语句:

public void sendMessageToClient(@Nullable Client client, @Nullable String message, @NotNull Mailer mailer) {
        if (client == null || message == null) return;

        PersonalInfo personalInfo = client.getPersonalInfo();
        if (personalInfo == null) return;

        String email = personalInfo.getEmail();
        if (email == null) return;

        mailer.sendMessage(email, message);
    }

这是我们常见的防御式编程,处处都要考虑变量是否为null的情况。

Kotlin对null的保护总结为以下几点:

  • 如果一个变量可能为null,那么在定义的时候在类型后面加上?
    val a: Int? = null

  • 对于一个可能为null的变量,如果必须在其值不为null时才进行后续操作,那么可以使用?.操作符来保护,下面的两种表示方式是等效的,即a为null时什么都不做:

    val a: Int? = null
    ...
    //if statment
    if (a != null){
        a.toLong()
    }
    
    //?.operator
    a?.toLong()
    
  • 当一个变量为null时,如果我们想为它提供一个替代值,那么可以使用?:
    val myLong = a?.toLong() ?: 0L
    上面的语句的意思就是如果a确实为null,那么将myLong赋值为0

  • 最后一条,就是对于如果一个可能是null,如果我们可以确保它已经不是null,那么可以使用!!操作符。但是不推荐这么使用。!!是坏代码的味道。

    val a: Int? = null
    ...
    a!!.toLong() 
    

回到我们的任务,由于只允许使用一个if语句,官方的参考答案是这样的:

fun sendMessageToClient(
       client: Client?, message: String?, mailer: Mailer
) {
   val email = client?.personalInfo?.email
   if (email != null && message != null) {
       mailer.sendMessage(email, message)
   }
}

8.Smart Casts

任务要求使用Kotlin的Smart Cast和When表达式重新实现JavaCode8.eval()函数:

public class JavaCode8 extends JavaCode {
    public int eval(Expr expr) {
        if (expr instanceof Num) {
            return ((Num) expr).getValue();
        }
        if (expr instanceof Sum) {
            Sum sum = (Sum) expr;
            return eval(sum.getLeft()) + eval(sum.getRight());
        }
        throw new IllegalArgumentException("Unknown expression");
    }
}

也是我们常见的强制类型转换。

在Kotlin中,多数情况下我么不需要显式的使用强制转换,因为编译器会为不可变的变量带入is检查,并且在必要的时候自动插入(安全的)强制转换。is通常和when表达式一起搭配使用:

when (x) {
    is Int -> print(x + 1)
    is String -> print(x.length + 1)
    is IntArray -> print(x.sum())
}

when和Java中的switch/case语句相似,但是要强大的多。主要区别在于when语句的参数可以是任何类型,其所有分支的判断条件也可以是任何类型的。

when (x) {
    1 -> print("x == 1")
    2 -> print("x == 2")
    else -> { // Note the block
        print("x is neither 1 nor 2")
    }
}

when可以作为一个语句也可以作为一个表达式,如果作为一个表达式使用,它可以有返回值,所以必须覆盖所有可能的条件或者实现else分支,否则编译器会报错。

val result = when (x){
    0, 1 -> "binary"
    else -> "error"
}

除此之外,when的条件语句还有很多其他的表达方式,如判断范围等,可以参考官方文档。

回到任务的解决:

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

9.Extension Functions

Kotlin中函数扩展就是不修改一个类的源码(通常是我们没有源码,无法修改)的情况下,通过扩展函数为一个类添加一个新的功能。扩展函数在行为好像它属于被扩展的类,所以在扩展函数中我们可以使用this以及所有被扩展类的公有方法。

任务要求为IntPair<Int, Int>分别实现一个扩展函数r()r()函数的功能就是创建一个有理数。

data class RationalNumber(val numerator: Int, val denominator: Int)

fun Int.r(): RationalNumber = RationalNumber(this, 1)
fun Pair<Int, Int>.r(): RationalNumber = RationalNumber(this.first,this.second)

Pair的扩展函数r中this可以省略:

fun Pair<Int, Int>.r(): RationalNumber = RationalNumber(first,second)

10.Object Expression

Kotlin中当我们需要创建某一个类的对象并且只需要对该对象做一点小的修改,而不需要重新创建一个子类时可以使用object expression。和Java中的匿名内部类很相似。

任务的要求是创建一个比较器(comparator),提供给Collection类对list按照降序排序。先来实现功能:

<pre>
fun task10(): List<Int> {
val arrayList = arrayListOf(1, 5, 2)
Collections.sort(arrayList,object: Comparator<Int>{
override fun compare(x: Int, y: Int): Int {
return y-x
}
}
)
return arrayList
}
</pre>

加粗部分就是所谓的object expression,修改为lambda表达式(哦哦,这好像是Task11的任务要求)

<pre>
fun task10(): List<Int> {
val arrayList = arrayListOf(1, 5, 2)
Collections.sort(arrayList) { x, y -> y - x }
return arrayList
}
</pre>

这个任务还展示了Kotlin和Java之间的交互,因为task10()函数中Collections是一个Java类。

11.SAM Conversions

所谓SAM conversions就是如果一个object实现了一个SAM(Single Abstract Method)接口时,可以直接传递一个lambda表达式,代码实现在上面。

12.Extensions On Collections

在Kotlin的标准库提供了许多扩展函数使得集合的使用非常方便。由于Kotlin可以很容易的和Java代码混合使用,所有Kotlin直接是使用标准的Java集合类(做了细小的改进)。

本任务的要求就是使用扩展函数sortedDescending重写上一个任务中的代码:

fun task12(): List<Int> {
    return arrayListOf(1, 5, 2).sortedDescending()
}

好了以上就是Kotlin Koans第一部分全部13个任务。

下一篇:Kotlin Koans学习笔记(2)

技术
Web note ad 1