Kotlinのbyについて

採用はこちら

Kotlinの by は、処理を別のオブジェクトに委譲(delegation)するための構文です。

ソフトウェア設計では「委譲(Delegation)」というパターンがあり、これは

  • 自分で処理を実装するのではなく
  • 別のオブジェクトに処理を任せる

という設計方法です。

Kotlinでは、この委譲パターンを 非常に少ないコードで書けるように by が用意されています。

目次

Kotlinにおける by の2つの用途

by は大きく次の 2種類の用途で使われます。

種類内容
クラス委譲 (Class Delegation)インターフェース実装を別オブジェクトへ委譲
プロパティ委譲 (Property Delegation)プロパティの getter / setter を別オブジェクトへ委譲

よく使われる

  • lazy
  • observable
  • Map

などは すべてプロパティ委譲の具体例です。

クラス委譲(Class Delegation)

概要

クラス委譲とは、インターフェースの実装を別のオブジェクトに任せる仕組みです。

基本構文

class クラス名(委譲オブジェクト) : Interface by 委譲オブジェクト

interface Printer {
    fun print()
}

class PrinterImpl : Printer {
    override fun print() {
        println("print")
    }
}

class Manager(printer: Printer) : Printer by printer

ここでは

  • ManagerPrinter を実装している
  • しかし print() の実装は printer に委譲している

という構造になります。

コンパイラが行うこと

Kotlinコンパイラは、概念的には次のようなコードを生成します。

class Manager(private val printer: Printer) : Printer {

    override fun print() {
        printer.print()
    }
}

つまり委譲先オブジェクトへメソッド呼び出しを転送するコードが自動生成されます。

一部のメソッドだけオーバーライド可能

委譲しつつ、特定のメソッドだけ自分で実装することもできます。

class Manager(printer: Printer) : Printer by printer {

    override fun print() {
        println("manager print")
    }
}

この場合

  • print() → Manager の実装
  • その他のメソッド → 委譲

になります。

注意点

クラス委譲は 継承とは挙動が異なる部分があります。

委譲先オブジェクト内部で呼ばれるメソッドは、派生クラスの override を使用しない場合があります。

つまり

  • 委譲元で override したメソッド
  • 委譲先内部の呼び出し

は一致しない場合があります。

これは 委譲は継承ではないという設計によるものです。

プロパティ委譲

概要

プロパティ委譲はプロパティの getter / setter の処理を別オブジェクトに任せる仕組みです。

基本構文

var propertyName by delegate

または

val propertyName by delegate

デリゲートオブジェクト

デリゲートは次の関数を提供します。

operator fun getValue(...)
operator fun setValue(...)

import kotlin.reflect.KProperty

class Delegate {

    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "Hello"
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("value set $value")
    }
}

使用例

var name: String by Delegate()

println(name)
name = "Taro"

動作の仕組み

読み込み時

name
↓
delegate.getValue(...)

書き込み時

name = value
↓
delegate.setValue(...)

このようにgetter / setter が delegate に委譲されます。

lazy

lazyプロパティ委譲の代表例です。

val data by lazy {
    println("init")
    "Hello"
}

動作

println(data)
println(data)

出力

init
Hello
Hello

特徴

  • 初回アクセス時だけ初期化
  • 以降は同じ値を返す

なぜ便利なのか

通常

val data = loadData()

オブジェクト生成時に実行されます。

しかし lazy を使うと必要になるまで処理を遅らせることができます。

observable

プロパティ変更を監視できます。

import kotlin.properties.Delegates

var name: String by Delegates.observable("initial") { property, old, new ->
    println("$old -> $new")
}

実行

name = "Taro"
name = "Jiro"

出力

initial -> Taro
Taro -> Jiro

Map委譲

Mapの値をそのままプロパティとして扱えます。

class User(private val map: Map<String, Any?>) {

    val name: String by map
    val age: Int by map
}

使用

val user = User(
    mapOf(
        "name" to "Taro",
        "age" to 20
    )
)

println(user.name)

この場合

map["name"]

が自動的に取得されます。

別プロパティへの委譲

Kotlinでは 別のプロパティに委譲することも可能です。

class Example {

    var newName: Int = 0

    var oldName by this::newName
}

ここでは

oldName

newName

と同じプロパティになります。

Kotlinで by が重要な理由

Kotlinはボイラープレートコード削減を重視した言語です。

Javaで委譲を書くと次のようになります。

class Manager implements Printer {

    private Printer printer;

    Manager(Printer printer) {
        this.printer = printer;
    }

    public void print() {
        printer.print();
    }
}

Kotlinでは

class Manager(printer: Printer) : Printer by printer

これだけで書けます。

Kotlinの設計思想

by は次の設計思想を強く反映しています。

Composition over inheritance

(継承より委譲)

つまり

  • 継承を増やすより
  • オブジェクトを組み合わせる

という設計です。

まとめ

機能内容
クラス委譲インターフェース実装を別オブジェクトへ委譲
プロパティ委譲getter / setter を別オブジェクトへ委譲
lazy遅延初期化
observableプロパティ変更監視
Map委譲Mapの値をプロパティ化

以上、Kotlinのbyについてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次