2019年3月4日

Kotlin Basics

kotlin 基本的 functions, variables, classes, enums, properties,以及型別轉換,及 exception 處理。

functions and variables

這是最基本的 HelloWorld.kt

fun main(args: Array<String>) {
    println("Hello, world!")
}

fun 是宣告 function,參數的型別寫在參數名稱後面,fun不需要放在 class 裡面,可直接放在檔案中,以 println 取代 System.out.println,可省略每一行程式最後面的分號。

kotlin 沒有 array 這種資料型別,必須要使用 Array 這個 class。


  • if 是 expression 不是 statement
fun max(a: Int, b: Int): Int {
    return if (a > b) a else b
}

fun main(args: Array<String>) {
    println(max(1, 2))
}

以 return 指定 fun 的回傳值

kotlin 的 if 是 expression 不是 statement,差別是 expression 一定有一個 value,可用在另一個 expression 裡面,而 statement 是程式區塊的頂層,沒有 value。所以程式中的 if 可以放在 return 後面,這種寫法在 Java 是不能用的。

kotlin 中,除了 for, do, do/while 以外,都是 expressions,expression 可用在另一個 expression 的功能,可組成 control structures。

另外 assignments 在 Java 是 expressions,但在 kotlin 是 statement。


變數可以不指定資料型別,要刻意指定也可以

val answer = 42
val answer: Int = 42

如果宣告時,沒有指定 value,則宣告變數時就一定要指定資料型別。

val answer: Int
answer = 42

val 是 immutable reference 的變數 var 是 mutable reference 的變數

在 kotlin 應該盡量使用 val,必要時才用 var

要注意的是,immutable 是指 object reference 不變,但是參考到的變數,還是可以改變 value

val languages = arrayListOf("Java")
languages.add("Kotlin")

var 雖然可以改變 object reference,但是仍然要改變為相同資料型別的變數 reference,以下的程式,會出現 type mismatch 的 error

var answer = 42
answer = "no answer"

簡化 string formatting: string templates

fun main(args: Array<String>) {
    val name = if (args.size > 0) args[0] else "Kotlin"
    println("Hello, $name!")
}

可以在 string 裡面,用 $name 參考到上面宣告過的變數,但如果是 character,前面要加上 escape,例如 println("\$x")

可用 \({} 加入一個 expression,例如: ```\){args[0]}```

fun main(args: Array<String>) {
    if (args.size > 0) {
        println("Hello, ${args[0]}!")
    }
}

因為 if 是 expression,所以可直接放到 ${} 裡面

fun main(args: Array<String>) {
    println("Hello, ${if (args.size > 0) args[0] else "someone"}!")
}

Classes and Properties

class Person(
    val name: String,
    var isMarried: Boolean
)

fun main(args: Array<String>) {
    val person = Person("Bob", true)
    println(person.name)
    println(person.isMarried)
}

以 class 宣告類別,預設就是 public,這個可省略。在 class 後面直接加上 val 參數,表示 name 是 read only 的 field,而宣告為 var 的 isMarried 則是可以改變的,自動就產生了 getter 及 setter。

如果由 Java 使用 Person 這個 class,可直接使用 getter/setter method。

Person person = new Person("Bob", true);
System.out.println(person.getName());  // Bob
System.out.println(person.isMarried()); // true

但是在 kotlin 的 getter/setter,就沒有 get 或 set 的前置,直接用 person.name person.isMarried 就可以了。


可在 class 中,自訂新的 property,並以 get() 撰寫自訂的 getter method,在 Java 是呼叫 isSquare

class Rectangle(val height: Int, val width: Int) {
    val isSquare: Boolean
        get() {
            return height == width
        }
}

fun main(args: Array<String>) {
    val rectangle = Rectangle(41, 43)
    println(rectangle.isSquare)
}

Directories and packages

每一個 kotlin .kt 原始檔在開頭可有一個 package 宣告,不同 package 的 kotlin class 必須要 import 後才能使用。

package geometry.shapes

import java.util.Random

class Rectangle(val height: Int, val width: Int) {
    val isSquare: Boolean
        get() = height == width
}

fun createRandomRectangle(): Rectangle {
    val random = Random()
    return Rectangle(random.nextInt(), random.nextInt())
}

可以直接 import top-level function

package geometry.example

import geometry.shapes.createRandomRectangle

fun main(args: Array<String>) {
    println(createRandomRectangle().isSquare)
}

可以將多個 classes 放到同一個 kotlin file 中,kotlin source file name 可以任意自訂,不需要跟 class 名稱一樣。但最好還是遵循 Java 的目錄原則,將同一個 package 的檔案,放到跟 package 相同的目錄中,這樣才能順利找到正確的原始檔。

處理多重選擇: enum 及 when

以 enum class 定義 enum(在 Java 是用 enum)

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

enum 並不是單純的 list of values,可以在 enum 中定義 properties 及 methods。宣告了 enum properties 後,每一個 enum value 都必須指定 properties 的數值,在 enum value 定義後面,一定要加上一個分號,在分號後面才能加上 fun methods

enum class Color(
        // 宣告 enum 的 properties
        val r: Int, val g: Int, val b: Int
) {
    RED(255, 0, 0), ORANGE(255, 165, 0),
    YELLOW(255, 255, 0), GREEN(0, 255, 0), BLUE(0, 0, 255),
    INDIGO(75, 0, 130), VIOLET(238, 130, 238);

    fun rgb() = (r * 256 + g) * 256 + b
}

fun main(args: Array<String>) {
    println(Color.BLUE.rgb())
}

when 是用來取代 Java 的 switch 語法

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

fun getMnemonic(color: Color) =
    when (color) {
        Color.RED -> "Richard"
        Color.ORANGE -> "Of"
        Color.YELLOW -> "York"
        Color.GREEN -> "Gave"
        Color.BLUE -> "Battle"
        Color.INDIGO -> "In"
        Color.VIOLET -> "Vain"
    }

fun main(args: Array<String>) {
    println(getMnemonic(Color.BLUE))
}

可將多個 enum 對應到同一個 value

enum class Color {
    RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}

fun getWarmth(color: Color) = when(color) {
    Color.RED, Color.ORANGE, Color.YELLOW -> "warm"
    Color.GREEN -> "neutral"
    Color.BLUE, Color.INDIGO, Color.VIOLET -> "cold"
}

fun main(args: Array<String>) {
    println(getWarmth(Color.ORANGE))
}

在 import 時,必須要 import Color 這個 class,另外再 import enum constants

import ch02.colors.Color
import ch02.colors.Color.*

fun getWarmth(color: Color) = when(color) {
    RED, ORANGE, YELLOW -> "warm"
    GREEN -> "neutral"
    BLUE, INDIGO, VIOLET -> "cold"
}

fun main(args: Array<String>) {
    println(getWarmth(Color.ORANGE))
}

可將 when 用在任意的 object(Java 的 switch 只能用在 enum constants, strings, number literals)。

when 裡面是 setOf 集合,後面每一個條件,都必須同樣檢查為 setOf,最後用 else 設定其他的條件。

import ch02.colors.Color
import ch02.colors.Color.*

fun mix(c1: Color, c2: Color) =
        when (setOf(c1, c2)) {
            setOf(RED, YELLOW) -> ORANGE
            setOf(YELLOW, BLUE) -> GREEN
            setOf(BLUE, VIOLET) -> INDIGO
            else -> throw Exception("Dirty color")
        }

fun main(args: Array<String>) {
    println(mix(BLUE, YELLOW))
}

上面的 code 會產生一些 Set 物件,為了程式效能,可去掉 when 的參數,直接

import ch02.colors.Color
import ch02.colors.Color.*

fun mixOptimized(c1: Color, c2: Color) =
    when {
        (c1 == RED && c2 == YELLOW) ||
        (c1 == YELLOW && c2 == RED) ->
            ORANGE

        (c1 == YELLOW && c2 == BLUE) ||
        (c1 == BLUE && c2 == YELLOW) ->
            GREEN

        (c1 == BLUE && c2 == VIOLET) ||
        (c1 == VIOLET && c2 == BLUE) ->
            INDIGO

        else -> throw Exception("Dirty color")
    }

fun main(args: Array<String>) {
    println(mixOptimized(BLUE, YELLOW))
}

smart casts: 合併了 type checks 及 casts

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

fun eval(e: Expr): Int {
    if (e is Num) {
        // 將 e 轉型為 Num,可以省略
        val n = e as Num
        return n.value
    }
    if (e is Sum) {
        return eval(e.right) + eval(e.left)
    }
    throw IllegalArgumentException("Unknown expression")
}

fun main(args: Array<String>) {
    println(eval(Sum(Sum(Num(1), Num(2)), Num(4))))
}

一開始定義了兩個 class 分別都實作 Expr 介面, eval 函數區分了 Num 與 Sum 的回傳值。

is 是在檢查資料型別的保留字,且在通過檢查後,compiler 會自動將 e 轉型為該資料型別。 as 是強制轉型的保留字。

eval(Sum(Sum(Num(1), Num(2)), Num(4))) 就是在計算 (Num(1) + Num(2)) + Num(4) 的結果。

將 if 取代為 when

在 kotlin 的 if 是 expression,會有 return value。所以可以將剛剛的 eval 改寫為 if expression。

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

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

fun main(args: Array<String>) {
    println(eval(Sum(Num(1), Num(2))))
}

剛剛的 if 裡面,都只有一個 expression,可以用 when 改寫。

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

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

fun main(args: Array<String>) {
    println(eval(Sum(Num(1), Num(2))))
}

when 的 is 裡面,超過一個 expression,就用 { } 包括在一起,最後一個 statement 就是 return value。

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

fun evalWithLogging(e: Expr): Int =
    when (e) {
        is Num -> {
            println("num: ${e.value}")
            e.value
        }
        is Sum -> {
            val left = evalWithLogging(e.left)
            val right = evalWithLogging(e.right)
            println("sum: $left + $right")
            left + right
        }
        else -> throw IllegalArgumentException("Unknown expression")
    }

fun main(args: Array<String>) {
    println(evalWithLogging(Sum(Sum(Num(1), Num(2)), Num(4))))
}

while 及 for loops

1..100 是 ranges,由 1 到 100

100 downTo 1 step 2,是由 100 到 1,每一次都 -2

fun fizzBuzz(i: Int) = when {
    i % 15 == 0 -> "FizzBuzz "
    i % 3 == 0 -> "Fizz "
    i % 5 == 0 -> "Buzz "
    else -> "$i "
}

fun main(args: Array<String>) {
    for (i in 1..100) {
        print(fizzBuzz(i))
    }
    
    for (i in 100 downTo 1 step 2) {
        print(fizzBuzz(i))
    }
}

for (x in 0 until size) 也可以寫成 for (x in 0..size-1)


如何 iterate maps

import java.util.TreeMap

fun main(args: Array<String>) {
    val binaryReps = TreeMap<Char, String>()

    for (c in 'A'..'F') {
        val binary = Integer.toBinaryString(c.toInt())
        binaryReps[c] = binary
    }

    for ((letter, binary) in binaryReps) {
        println("$letter = $binary")
    }
    
    val list = arrayListOf("10", "11", "1001")
    for ((index, element) in list.withIndex()) {
        println("$index: $element")
    }
}

執行結果

A = 1000001
B = 1000010
C = 1000011
D = 1000100
E = 1000101
F = 1000110
0: 10
1: 11
2: 1001

用 in !in 檢查 ranges,也可以用在 when 裡面

"Kotlin" in "Java".."Scala" Kotlin 是在 Java 到 Scala 範圍之間,所有字串的一員

fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'Z'
fun isNotDigit(c: Char) = c !in '0'..'9'

fun recognize(c: Char) = when (c) {
    in '0'..'9' -> "It's a digit!"
    in 'a'..'z', in 'A'..'Z' -> "It's a letter!"
    else -> "I don't know…​"
}

fun main(args: Array<String>) {
    println("q isLetter: "+isLetter('q'))
    println("x isNotDigit: "+isNotDigit('x'))

    println()
    println("8:"+ recognize('8'))

    println()
    println("Kotlin" in "Java".."Scala")

    println()
    println("Kotlin" in setOf("Java", "Scala"))
}

執行結果

q isLetter: true
x isNotDigit: true

8:It's a digit!

true

false

Exceptions

kotlin 的 throw,後面不需要加上 new 再搭某一個 Exception class

if (percentage !in 0..100) {
    throw IllegalArgumentException(
        "A percentage value must be between 0 and 100: $percentage")
}

跟 Java 類似,也適用 try ... catch ... finally 的語法,但是在 method 宣告的地方,不需要宣告該 method function 可能會 throw Exception。

readNumber 這個 method 是回傳 Int? ,增加 ? 的用意,是表示該 method 會回傳整數或是 null。

import java.io.BufferedReader
import java.io.StringReader

fun readNumber(reader: BufferedReader): Int? {
    try {
        val line = reader.readLine()
        return Integer.parseInt(line)
    }
    catch (e: NumberFormatException) {
        return null
    }
    finally {
        reader.close()
    }
}

fun main(args: Array<String>) {
    val reader = BufferedReader(StringReader("239"))
    println(readNumber(reader))
}

要改變習慣,將 try 當作 expression,以往會在 catch Exception 的部分,直接 return

import java.io.BufferedReader
import java.io.StringReader

fun readNumber(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine())
    } catch (e: NumberFormatException) {
        return
    }

    println(number)
}

fun main(args: Array<String>) {
    val reader = BufferedReader(StringReader("not a number"))
    readNumber(reader)
}

將 try 的 catch exception 部分,直接寫成 null,就表示 number 是 Int 或是 null。

import java.io.BufferedReader
import java.io.StringReader

fun readNumber(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine())
    } catch (e: NumberFormatException) {
        null
    }

    println(number)
}

fun main(args: Array<String>) {
    val reader = BufferedReader(StringReader("not a number"))
    readNumber(reader)
}

References

Kotlin in Action

沒有留言:

張貼留言