KotlinDive

プログラミング言語 Kotlin についての入門ブログです。

ラムダ式と匿名関数

匿名関数

ラムダ式では返り値の型を明示的に指定することができません。 明示的に指定できなくても、自動的に推測してくれるため、大抵の場合は問題ありません。
しかし、何らかの事情で返り値の型を明示的に指定したい場合、「匿名関数」を利用することができます。 匿名関数とは、その名前の通り関数名(関数の識別子)を持たない関数のことです。


fun(引数リスト): 返り値の型 = 式

fun(引数リスト): 返り値の型 {
    式
}

上記のように、関数名がないこと以外は普通の関数と同じです。
それでは、サンプルを見てみましょう。


fun main(args: Array) {
    val fruits = listOf("apple", "banana", "orange", "peach")

    // 関数名の前に::をつけることで関数を参照を取得できる
    val subList = filter(
            fruits,
            fun(str: String): Boolean = str.contains('p', true) )

    println(subList)
}

fun filter(source: List, condition: (String) -> Boolean): List {
    val ret = mutableListOf()

    for (i in source) {
        if (condition(i)) {
            ret.add(i)
        }
    }

    return ret;
}

[apple, peach]

関数リテラルと関数型

Kotlinにおいて、ラムダ式と匿名関数は関数リテラルとして扱われます。
数値リテラルや文字列リテラルなどと同じように、変数に代入することができます。


fun main(args: Array) {
    val fruits = listOf("apple", "banana", "orange", "peach")

    // 変数に代入できる
    val anonymous = fun(str: String): Boolean = str.contains('p', true)
    val lamda = { str: String -> str.contains('p', true) }

    val subList = filter(fruits, anonymous)
    println(subList)

    val subListLamda = filter(fruits, lamda)
    println(subListLamda)
}

fun filter(source: List, condition: (String) -> Boolean): List {
    val ret = mutableListOf()

    for (i in source) {
        if (condition(i)) {
            ret.add(i)
        }
    }

    return ret;
}

[apple, peach]
[apple, peach]

また、上記のサンプルのfilter関数のcondition引数のように、 引数の型と返り値の型から関数型を指定することができます。


fun main(args: Array) {
    val fruits = listOf("apple", "banana", "orange", "peach")

    // 型を指定して変数を宣言
    val anonymous: (String) -> Boolean
    val lamda: (String) -> Boolean

    anonymous = fun(str: String): Boolean = str.contains('p', true)
    // 引数の型が一つしかないことがわかっているので、itを利用できる
    lamda = { it.contains('p', true) }

    val subList = filter(fruits, anonymous)
    println(subList)

    val subListLamda = filter(fruits, lamda)
    println(subListLamda)
}

fun filter(source: List, condition: (String) -> Boolean): List {
    val ret = mutableListOf()

    for (i in source) {
        if (condition(i)) {
            ret.add(i)
        }
    }

    return ret;
}

[apple, peach]
[apple, peach]

クロージャ

ラムダ式や匿名関数の変数は、クロージャとして機能しています。
つまり、ラムダ式や匿名関数自身が定義されたスコープにアクセスできます。


fun main(args: Array) {
    val fruits = listOf("apple", "banana", "orange", "peach")

    var str = ""
    fruits.filter {
        it.contains('p', true)
    }.forEach {
        // このラムダ式が定義されてるスコープ、つまり「fun main」のスコープにアクセスできる。
        // そのため、fun mainスコープの変数であるstrを操作できる。
        str += it;
        str += "\n"
    }

    print(str);
}

apple
peach

ラムダ式の基本

高階関数

高階関数とは、ある関数を引数に持つような関数のことです。
関数を引数として渡すというのは、もう少し言うと関数の参照を渡すということです。 関数の参照は、関数名の前に「::」をつけることで取得できます。 例えば、次のサンプルのgetNonNullのような関数です。


fun main(args: Array) {
    val fruits = listOf("apple", "banana", "orange", "peach")

    // 関数名の前に::をつけることで関数を参照を取得できる
    val subList = filter(fruits, ::isContainsP)

    println(subList)
}

fun filter(source: List, condition: (String) -> Boolean): List {
    val ret = mutableListOf()

    for (i in source) {
        if (condition(i)) {
            ret.add(i)
        }
    }

    return ret;
}

fun isContainsP(str: String): Boolean {
    // pを含むかどうかチェック。大文字小文字は無視する
    return str.contains('p', true)
}

[apple, peach]

ラムダ式

上記のサンプルの「isContainsP」ですが、たったこれだけの機能のためにわざわざ関数を作るのはめんどくさいです。
しかも、P以外のさまざまな条件も使いたいとなったときに、その条件の数だけ単純な関数を用意しようと思うと、もっとめんどくさいです。
そこで、単純な(式が1,2個程度の)関数を簡単便利に表現する機能があります。それがラムダ式です。
ラムダ式の基本的な書き方は以下の通りです。


{ 引数 -> 式 }

引数の型は省略可能な場合もあります。 それでは、実際にサンプルを見てみましょう。


fun main(args: Array) {
    val fruits = listOf("apple", "banana", "orange", "peach")

    // ラムダ式を利用すると簡単に記述できる
    val subList = filter(fruits, { str -> str.contains('p', true) })

    println(subList)
}

fun filter(source: List, condition: (String) -> Boolean): List {
    val ret = mutableListOf()

    for (i in source) {
        if (condition(i)) {
            ret.add(i)
        }
    }

    return ret;
}

[apple, peach]

it

高階関数の引数が1つのみの場合、その引数の宣言を(->を含めて)省略して「it」で表すことができます。


fun main(args: Array) {
    val fruits = listOf("apple", "banana", "orange", "peach")

    // 引数が一つのみの場合、省略してitで代用可能 
    val subList = filter(fruits, { it.contains('p', true) })

    println(subList)
}

fun filter(source: List, condition: (String) -> Boolean): List {
    val ret = mutableListOf()

    for (i in source) {
        if (condition(i)) {
            ret.add(i)
        }
    }

    return ret;
}

[apple, peach]

型チェックとキャスト

is

ある変数の実体の型が何であるかを判定するためには「is」演算子を利用します。
is演算子でチェックされた変数は、自動的に判定された型にキャストされます。 これをスマートキャストといいます。


fun main(args: Array) {
    val str = "string"
    val obj: Any = str

    // objはAnyなので次の式はエラーになる
    // obj.length

    // is演算子で型をチェック
    if (obj is String) {
        // 明示的にキャストしなくても、objはStringに自動的にキャストされている
        println(obj.length)
    } else {
        println("obj is not String.")
    }

    // 否定も可能
    // !(obj is String)でもOK
    if (obj !is String) {
        println("obj is not String.")
    } else {
        // 明示的にキャストしなくても、objはStringに自動的にキャストされている
        println(obj.length)
    }

    // whenでも使える
    when (obj) {
        is Int -> println(obj * 2)
        is DoubleArray -> println(obj.average())
        is String -> println(obj.length)
    }
}

6
6
6

as

アンセーフキャスト

as演算子を利用すると、型キャストを行うことができます。
しかし、もしキャストに失敗した場合はClassCastExceptionという例外がスローされてしまいます。 例外は適切に処理されなければ不具合のもとになるため、Kotlinではアンセーフキャストと呼ばれています。


fun main(args: Array) {
    val obj: Any = "some string"

    // objはStringにキャストできる
    val str = obj as String
    println(str.length)

    val obj2: Any = 100

    // ClassCastExceptionが発生してしまう
    var str2 = obj2 as String
    println(str2.length)
}

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
	at ******.main(*******.kt:11)
11

セーフキャスト

一方で、「as?」演算子でnull安全の仕組みを利用して安全なキャストをすることができます.
こちらは、キャストしようとしてできなかった場合、nullが返ります。
つまり、そもそも適切に処理をしないとビルドエラーになり、実行することができません。
プログラマコンパイラによって、きちんとnullチェックをすることが強制され、 うっかりによるチェックもれを防ぐことができるので「安全」だということです。


fun main(args: Array) {
    val obj: Any = "some string"

    // objはStringにキャストできる
    val str = obj as? String
    // strはString?であるため、nullチェックをしないと関数を呼べない
    // 下記のコードはビルドエラーになる
    // println(str.length)
    if (str != null) {
        println(str.length)
    } else {
        println("str is null")
    }

    val obj2: Any = 100

    // ClassCastExceptionが発生してしまう
    var str2 = obj2 as? String
    if (str2 != null) {
        println(str2.length)
    } else {
        println("str2 is null")
    }
}

11
str2 is null

null安全

null安全について

JavaC++Objective-Cなどの言語では、プログラムを実行するまでオブジェクトがnullであるかどうかがわからない場合があります。 そのため、プログラマーは手動でオブジェクトがnullでないことをチェックするコードを作成して対策する必要があるのですが、 実際にはその対策が不十分であったことで、数多くの致命的な不具合や脆弱性の原因になっています。

この問題は、ビリオンダラーミステイクと言われたこともあります。 (以下に、クイックソートの発明などでも知られる計算機科学者であるアンソニー・ホーアという人の言葉を引用したいと思います)

それは10億ドルにも相当する私の誤りだ。 null参照を発明したのは1965年のことだった。 当時、私はオブジェクト指向言語 (ALGOL W) における参照のための包括的型システムを設計していた。 目標は、コンパイラでの自動チェックで全ての参照が完全に安全であることを保証することだった。 しかし、私は単にそれが容易だというだけで、無効な参照を含める誘惑に抵抗できなかった。 これは、後に数え切れない過ち、脆弱性、システムクラッシュを引き起こし、過去40年間で10億ドル相当の苦痛と損害を引き起こしたとみられる。

Wikipedia

プログラム実行時に誤ってNullを参照することで発生する様々な不具合を回避するため、 最近のプログラミング言語では(プログラムを実行しなくても)ビルド時にnullでないことを保証する「null安全」という仕組みが取り入れられています。
つまり、nullになる可能性になるオブジェクトを(もしオブジェクトがNullだったらまずいような)操作しようとするとビルドエラーが発生するようになっているといういことです。
これが、「null安全」と呼ばれる仕組みです。

nullableとnon-null

nullableな変数の宣言

null安全なプログラミング言語であるKotlinにおいて、 nullable(nullを代入できる)な型とnon-null(nullを代入できない)な型はまったく異なるものです。 「?」が付いている型がnullableになります。


fun main(args: Array) {
    var nullableString: String?

    // nullableな型の変数には、nullを代入できる
    nullableString = null
    println("nullableString=${nullableString}")

    // もちろん、nullでない値を代入することもできる
    nullableString = "nullableです"
    println("nullableString=${nullableString}")

    var nonnullString: String

    // nonnullな方には、nullを代入できない。ビルドエラーになる
    // nonnullString = null

    // nullでない値しか代入できない
    nonnullString = "nonnullです"
    println("nonnullString=${nonnullString}")
}

nullableString=null
nullableString=nullableです
nonnullString=nonnullです

nullableな変数は、その関数を呼び出したりすることができません。 これは、もしその変数がnullだった場合、NGだがらです。


fun main(args: Array) {
    val nullableString: String? = "nullableです"

    // nullableな変数からは、関数を呼び出すことはできません
    // ビルドエラーになります。
    // println(nullableString.length)
}

 

明示的にnullチェックする

nullableな変数を利用するためのもっとも基本的な方法は、ifを利用してnullチェックをすることです。
コンパイラが自動的にnullチェックされているかどうかを判別してくれます。 nullチェックしていれば関数を呼んだりすることができます。


fun main(args: Array) {
    val nullableString: String? = "nullableです"

    // ifで明示的にnullチェックする
    if (nullableString != null) {
        // コンパイラがnullチェック済みかどうかを判定して
        // nullチェック済みならメンバーを利用できる
        println(nullableString.length)
    } else {
        println("nullableString is null.")
    }
}

10

セーフコール

nullableを利用するための2つ目の方法は、セールコールを利用することです。
セーフコールはnullableな変数の後ろに?をつけてメンバを呼ぶことで、 もしその変数がnullでなければ通常の結果が、そうではなくnullならnullが返ってきます。


fun main(args: Array) {
    val nullableString: String? = "nullableです"
    println("nullableString.length = ${nullableString?.length}")

    // nullableなリストの要素にアクセスする
    val fruits = listOf("apple", null, "orange")
    for (fruit in fruits) {
        println(fruit?.length)
    }
}

nullableString.length = 10
5
null
6

エルビス演算子

エルビス演算子はifを利用したnullチェックの表現をシンプルにしたものです。
C++なでにある三項演算子?をnull専用にしたようなものをイメージすればいいと思います。
エルビス演算子は「?:」で表現されます。
単純なセーフコールとの違いは、実際にnullだった場合にどんな値を返すかをプログラマが指定できることです。


fun main(args: Array) {
    val nullableString: String? = "nullableです"

    // もしnullableがnon-nullだった場合はnullableString.lengthが,
    // そうでなくてnullだった場合は-1が返る
    val length = nullableString?.length ?: -1
    println("nullableString.length = ${length}")

    // nullableなリストの要素にアクセスする
    val fruits = listOf("apple", null, "orange")
    for (fruit in fruits) {
        println(fruit?.length ?: -1)
    }
}

nullableString.length = 10
5
-1
6

!!演算子

「!!」(非nullアサーション演算子)はnullableな変数の後ろにつけることで、 もし本当に変数がnullだった場合には、自動的に「null pointer exception」をスローします


fun main(args: Array) {
    val nullableString: String? = "nullableです"

    // もしnullableがnon-nullだった場合はnullableString.lengthが,
    // そうでなくてnullだった場合は-1が返る
    val length = nullableString!!.length
    println("nullableString.length = ${length}")

    // nullableなリストの要素にアクセスする
    val fruits = listOf("apple", null, "orange")
    for (fruit in fruits) {
        println(fruit!!.length)
    }
}

nullableString.length = 10
Exception in thread "main" kotlin.KotlinNullPointerException
	at ***********.main(***********.kt:13)
5

セーフキャスト

「as」演算子を利用してキャストしようとして失敗した場合、 通常はClassCastExceptionがスローされます。
セーフキャストを利用することで、例外をスローせずに安全にキャストすることができます。 セーフキャストは「as?」とすることで利用でき、もしキャストできない場合は、例外をスローする代わりにnullを返します。


fun main(args: Array) {
    val str = "string"

    // キャストに失敗するとnullが返却される
    val num: Int? = str as? Int
    println("num = ${num}")

    // キャストに失敗するとClassCastExceptionがスローされる
    val num2 = str as Int
    println("num2 = ${num2}")
}

Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
	at *********.main(HelloWorld.kt:7)
num = null

レンジ

レンジの基本的な使い方

レンジは数値の範囲を表すために利用する式です。
「..」演算子や「..」演算子に対応する関数である「rangeTo()」関数を「in」キーワードと組み合わせて利用します。 「a..b」で「aからbの間」という意味になります。 比較で利用する場合は、「a <= x && x <= b」で、繰り返しで利用する場合は「a, a + 1, ... , b - 1, b」となります。
レンジは標準ではInt型ですが、Doubleの値とも比較できます。


fun main(args: Array) {
    // .. 演算子でレンジを利用する
    for (i in 1..5) {
        println(i)
    }

    // rangeTo 関数でレンジを利用する
    for (i in 6.rangeTo(10)) {
        println(i)
    }

    // Doubleとも比較できる
    if (10.0 in 0..100) {
        println("in 0 to 100")
    }

    // 否定も可能です
    if (-123 !in 0..100) {
        println("not in 0 to 100")
    }
}

1
2
3
4
5
6
7
8
9
10
in 0 to 100
not in 0 to 100

レンジの応用的な使い方

downTo

繰り返しなどで数値を大きい方から小さい方にイテレーションしたい場合は「downTo」を使うことができます。


fun main(args: Array) {
    val start = 9
    val end = 0

    // 通常のレンジ演算子は大きい方から小さい方には使えない
    println("start..end")
    for (i in start..end) {
        print(i)
    }

    // downToを使うと大きい方から小さい方にイテレーションできる
    println("start downTo end")
    for (i in start downTo end) {
        print(i)
    }
}

start..end
start downTo end
9876543210

step

レンジで繰り返しをする場合に、数値を1ずつインクリメントするのではなく、任意の間隔で利用するには「step」を使います。


fun main(args: Array) {
    val num = 3

    // step で任意の間隔を指定する
    println("0..20 step num")
    for (i in 0..20 step num) {
       print("${i} ")
    }

    // downToにも利用できます
    println("\n20 downTo 0 step num")
    for (i in 20 downTo 0 step num) {
       print("${i} ")
    }
}

0..20 step num
0 3 6 9 12 15 18 
20 downTo 0 step num
20 17 14 11 8 5 2 

until

JavaC++では、「for (int i = 0; i < count; i++)」のようなループをよく使うと思います。 この時、繰り返し範囲は「0 <= i < count」のように終わりが開区間になっています。
一方、Kotlinのレンジは「0..count」とすると「0 <= i <= count」というようにcountも含んだ閉区間になります。
Kotlinで「0 <= i < count」のようにcountを含まない区間を取得するにはuntilを使います。


fun main(args: Array) {
    // until を使うと最後を含まない繰り返しができる
    println("0 until 10")
    for (i in 0 until 10) {
       print("${i} ")
    }
}

0 until 10
0 1 2 3 4 5 6 7 8 9 

繰り返し(for、while)

for

kotlinでのforはC#のforeachのようにコレクションの要素に順番にアクセスできます。
もちろん、C言語などでよくあるN回繰り返すというような処理も可能です。 そのような場合にはレンジを利用します。


fun main(args: Array) {
    val fruits = listOf("apple", "banana", "orange")

    // コレクションの要素を順番に取り出す
    for (item in fruits) {
        println(item)
    }

    // インデックスを取得して要素にアクセスすることもできる
    for (i in fruits.indices) {
        println(fruits[i])
    }

    // 要素とインデックスを同時に取り出すこともできます
    for ((index, item) in fruits.withIndex()) {
        println("${index}: item")
    }

    // レンジをりようし利用して指定回数繰り返す
    val num = 10
    var sum = 0
    for (i in 0 until num) {
        sum += i
        println("sum = ${sum}")
    }
}

apple
banana
orange
apple
banana
orange
0: item
1: item
2: item
sum = 0
sum = 1
sum = 3
sum = 6
sum = 10
sum = 15
sum = 21
sum = 28
sum = 36
sum = 45

while

while、do...whileループはJavaなどと同様に使うことができます。
do...whileでは、doブロック内で宣言した変数は、 whileの条件判定部のスコープになります。


val data = mutableListOf(1, 2, 3, 4)

fun main(args: Array) {
    var num = 1

    while (num < 100) {
        num += num;
        println(num)
    }

    do {
        val num = fetchData()
        println(num)
    } while (num != null) // ここでもnumを参照することができます
}

fun fetchData(): Int? =
    if (data.isEmpty()) {
        null
    } else {
        val ret = data[0]
        data.removeAt(0)

        ret
    }

2
4
8
16
32
64
128
1
2
3
4
null

条件分岐(if、when)

if

基本的な使い方

Kotlinでは、ifは式として機能しています。 つまり、ifが値を返して、その値をそのまま変数に代入したりできるということです。
もちろん、通常の条件分岐としても利用できます。


fun main(args: Array) {
    // 通常の使い方
    val num = 10

    if ((num % 2) == 0) {
        println("偶数です")
    } else {
        println("奇数です")
    }

    // 式としての使い方
    val num2 = 5
    val str = if ((num2 % 2) == 0) "偶数です" else "奇数です"

    println(str)
}

偶数です
奇数です

リターンされる値

ifでリターンされる値は、実行されたブロックの一番最後の式の値になります。


fun main(args: Array) {
    testIf(-100)
    testIf(50)
    testIf(1)
}

fun testIf(num: Int) {
    val ifReturn = if (num < 0) {
        println("Run (num < 0) block.")

        num * 2
    } else if ((num % 2) == 0) {
        println("Run ((num % 2) == 0) block.")

        "偶数です"
    } else {
        println("Run else block.")

        1234.5678
    }

    println("if expression return is ${ifReturn}.")
}

Run (num < 0) block.
if expression return is -200.
Run ((num % 2) == 0) block.
if expression return is 偶数です.
Run else block.
if expression return is 1234.5678.

when

基本的な使い方

whenはJavaC言語でいうとswitchのようなものです。
ifと同じように式として機能し、値を返すことができます。
switchのdefaultに相当するものが、whenにもelseという形であります。


fun main(args: Array) {
    val num = 4

    // 通常の条件分岐として利用
    when (num) {
        1 -> println("num is 1.")
        2 -> println("num is 2")
        3 -> println("num is 3.")
        4 -> {
            // 処理をブロックで記述することも、もちろん可能です。
            println("num is 4.")
        }
        else -> {
            println("num is something else.")
        }
    }

    // 値を返す式として利用
    val ret = when (num) {
        1 -> {
            println("num is 1.")

            123
        }
        2 -> {
            println("num is 2.")

            "456"
        }
        3 -> {
            println("num is  3.")

            123.456
        }
        else -> {
            println("num is something else")

            null
        }
    }

    println("ret is ${ret}.")
}

num is 4.
num is something else
ret is null.

さまざまな条件設定

whenでは、かなり柔軟な分岐の条件設定ができます。
例えば、カンマで区切って複数の条件を並べたり、定数ではなく式を条件にすることもできます。


fun main(args: Array) {
    val str = "100"
    val num = 100.0
    str.isEmpty()

    when (str) {
        "cat", "dog", "bird" -> println("animal")
        num.toString() -> println("100.0")
        is String -> println("String")
        else -> println("else.")
    }
}

String