前人未踏の領域へ Androidアプリ開発編

Androidアプリ開発に関する調査メモ置き場。古い記事にはアプリ以外も含まれます。

Kotlin ラムダ周りの標準関数について(let, also, with, apply, run)

ラムダ周りで似たような関数が多くて使い分けがさっぱり...
というかletしか使わずにいたのでこれではいかんと一念発起して整理。

結論

こちらのスタイルガイドラインを参考 Coding Conventions - Kotlin Programming Language

  • ブロックの中身で判断
    • ブロック内で複数のオブジェクトに対するアクセスをしているなら this よりも it を使うのが良いため、 letalso を使う
    • ブロック内で一切レシーバを使わない場合も also を使う。
    • ブロック内でレシーバに対するアクセスのみの場合は with , apply , run のいずれかを使う
  • 戻り値で判断
    • レシーバ(コンテキストオブジェクト)が戻り値がよい場合は applyalso を使う
    • 何らかの値を返す必要がある場合は with, let , run のいずれかを使う
  • NULL許可で判断
    • レシーバがnull可かコールチェーンの結果で判断されるなら apply , let , run のいずれかを使う
    • nullが許可されない場合は withalso を使う

あんまりスッキリしてないけど。

用語の整理

書いてある内容や用語でつまずかないための整理

型変数

  • T: Type(クラスの型)
  • R: Return (実行結果。ラムダの最後の行の式。最後が代入式などの場合はUnitが返る)

レシーバ

  • メソッドを呼び出される側のオブジェクト。呼び出し側はセンダー。

レシーバ付きラムダ

  • ラムダ内において別のオブジェクトのメソッドを追加の修飾辞を使用せずに呼び出すことができる機能。
  • 同じオブジェクトに対してその名前を繰り返し記述することなく複数の操作をできる
  • レシーバをthisで参照することもできる
  • 通常はthisを省略してメソッドやプロパティを参照できる
  • ラムダを実行した返り値を返す(ラムダの最後の行の式)。

it

  • ラムダの引数が1つのときに省略して使用できるキーワード

this

  • 関数を実行中のオブジェクト。レシーバ。

let

  • レシーバを第1引数にとる
  • 実行結果を返す
  • alsoのお仲間
/**
 * Calls the specified function [block] with `this` value as its argument and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}

also

  • レシーバを第1引数にとる
  • レシーバを返す
  • letのお仲間
/**
 * Calls the specified function [block] with `this` value as its argument and returns `this` value.
 */
@kotlin.internal.InlineOnly
@SinceKotlin("1.1")
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}

with

  • レシーバ付きラムダ
  • レシーバとラムダの2つの引数を取る
  • 第1引数をラムダのレシーバに変換する

  • alsoのお仲間

/**
 * Calls the specified function [block] with the given [receiver] as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}

apply

  • レシーバ付きラムダ
  • withのお仲間
/**
 * Calls the specified function [block] with `this` value as its receiver and returns `this` value.
 */
@kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}

run

runにはレシーバ付きラムダと通常のラムダの2パターンがある

  • レシーバを持たないブロック関数
  • 実行結果を返す
/**
 * Calls the specified function [block] and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <R> run(block: () -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}
  • レシーバ付きラムダ
  • 実行結果を返す
  • also, withのお仲間
/**
 * Calls the specified function [block] with `this` value as its receiver and returns its result.
 */
@kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

サンプル

使い分けが知りたいのに同じことしてどうする、という駄目なサンプル。 it を使用しているか、と値を返却するためにラムダの最終行がどうなっているかに注目。

class Book {
    lateinit var title: String
    lateinit var author: String
    override fun toString() = "$title : $author"
}

fun main(args: Array<String>) {

    //Block内の実行結果を返す
    val a: Book = Book().let {
        it.title = "四畳半神話大系"
        it.author = "森見登美彦"
        it
    }

    //ブロックを実行し、レシーバを返す、
    val b: Book = Book().also {
        it.title = "四畳半神話大系"
        it.author = "森見登美彦"
    }

    //レシーバ付きラムダ。ラムダを実行し、レシーバを返す
    val c: Book = Book().apply {
        title = "四畳半神話大系"
        author = "森見登美彦"
    }

    //レシーバ付きラムダ。第1引数をラムダのレシーバーとして実行し、実行結果を返す
    val d: Book = with(Book()) {
        title = "四畳半神話大系"
        author = "森見登美彦"
        this
    }

    //ブロックを実行し、実行結果を返す
    val e: Book = Book().run {
        title = "四畳半神話大系"
        author = "森見登美彦"
        this
    }

    //ブロックを実行し、ラムダ最後の行を返す
    val book = Book()
    val f: Book = run {
        book.title = "四畳半神話大系"
        book
    }
}

適切なサンプルがすぐに思いつかなかった
正直なところ無理して全部使い分ける必要はない気もするが、用途を意識できるようになれれば。
使いながら覚えていくしかないな。

参考

kotlin/Standard.kt at 1.2.70 · JetBrains/kotlin · GitHub

Coding Conventions - Kotlin Programming Language