KotlinのDelegateについて

採用はこちら

Kotlinの Delegate(デリゲート) は、単なる糖衣構文ではなく、責務分離・再利用性・保守性 を高いレベルで実現するための言語機能です。

Delegateを正しく理解すると、

  • 状態管理コードの肥大化を防げる
  • ログ・監視・永続化などの横断的関心事を分離できる
  • 継承に頼らない柔軟な設計が可能になる

といった実務的メリットが得られます。

本記事では、仕様として正確な説明をベースに実務視点でKotlinのDelegateを体系的に解説します。

目次

KotlinにおけるDelegateとは何か

Delegateとは 「処理や責務を別のオブジェクトに委譲する仕組み」 です。

Kotlinでは、Delegateは主に次の2つの文脈で使われます。

  1. プロパティデリゲート
  2. クラス委譲(インターフェース委譲)

どちらも「by」を使いますが、目的と役割は明確に異なります。

プロパティデリゲートの基本構造

基本構文

var name: String by delegate

このとき、name へのアクセスは直接フィールドを触るのではなく、delegate オブジェクト経由 で処理されます。

内部的に行われること

Kotlinコンパイラは、次のような呼び出しを生成します。

delegate.getValue(thisRef, property)
delegate.setValue(thisRef, property, value)

引数の意味

  • thisRef
    • プロパティの所有者インスタンス
    • トップレベルプロパティなどでは null になる場合もある
  • property
    • 対象プロパティのメタ情報(KProperty<*>

この仕組みにより、

  • 値の保持方法
  • 取得・代入時の副作用
  • 検証・制約

プロパティ自身から完全に分離 できます。

標準ライブラリの代表的なプロパティデリゲート

Kotlin標準ライブラリには、実務で頻出するデリゲートが用意されています。

lazy:遅延初期化

val data: String by lazy {
    println("初期化処理")
    "Hello"
}

特徴

  • 初回アクセス時にのみ初期化される
  • val 専用(再代入不可)
  • デフォルトではスレッドセーフ

重要な補足(正確な仕様)

lazy はスレッドセーフかどうかを モード指定 できます。

lazy(LazyThreadSafetyMode.SYNCHRONIZED) // デフォルト
lazy(LazyThreadSafetyMode.PUBLICATION)
lazy(LazyThreadSafetyMode.NONE)         // 非スレッドセーフだが高速

つまり「lazyは常にスレッドセーフ」という理解は誤りで、デフォルトがスレッドセーフ というのが正確です。

実務での利用例

  • Repository や API クライアント
  • 重い初期化処理
  • DI コンテナと組み合わせた遅延生成

observable:変更監視

var count: Int by Delegates.observable(0) { property, old, new ->
    println("$old -> $new")
}

動作タイミング

  • 値が 代入された後 にコールバックが呼ばれる
  • 新しい値はすでに反映済み

注意点(重要)

  • 「値が変わったとき」ではなく、「代入が行われたとき」に呼ばれる
  • 同じ値を再代入しても呼ばれる可能性がある

UI更新、ログ出力、状態監視などに向いています。

vetoable:代入の拒否

var age: Int by Delegates.vetoable(0) { _, old, new ->
    new >= 0
}

特徴

  • 代入前に呼ばれる
  • false を返すと代入されない
  • 例外は投げられず、値は旧値のまま保持される

observable が「通知」用途なのに対し、
vetoable制約・バリデーション に向いています。

notNull:非null保証(遅延初期化)

var userId: String by Delegates.notNull()

仕様上のポイント

  • 初期値を持たない var に使える
  • 未初期化状態でアクセスすると例外
  • lateinit が使えない型(Intなど)にも使える

lateinit と異なり、ジェネリックとして実装されている点が特徴です。

カスタムプロパティデリゲート

最小構成

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

    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        println("${property.name} = $value")
    }
}
var text: String by MyDelegate()

実務ではジェネリックが基本

class LoggingDelegate<T>(private var value: T) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
        println("get ${property.name}")
        return value
    }

    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
        println("set ${property.name}: $value -> $newValue")
        value = newValue
    }
}

よくある用途

  • ログ付きプロパティ
  • 永続化(SharedPreferences / DataStore)
  • バリデーション付き代入
  • キャッシュ制御

プロパティの「横断的関心事」を切り出すのに非常に有効です。

クラス委譲(インターフェース委譲)

Kotlinには クラス(正確にはインターフェース)委譲 という仕組みもあります。

class MyList(
    private val list: List<String>
) : List<String> by list

重要な前提

  • 委譲できるのは インターフェースの実装
  • 任意のクラス継承を by で委譲できるわけではない

一部メソッドだけ拡張する例

class MyList(
    private val list: MutableList<String>
) : MutableList<String> by list {

    override fun add(element: String): Boolean {
        println("add: $element")
        return list.add(element)
    }
}

実務的メリット

  • Decoratorパターンを簡潔に実装できる
  • 継承より疎結合
  • テストや差し替えが容易

Delegateは設計のための機能

Delegateは単なる省略構文ではなく、設計を支援する仕組み です。

Delegateが特に向いているケース

  • ログ・監視・永続化などの横断的関心事
  • 状態管理の分離
  • 継承を避けたい場合の振る舞い拡張

継承との比較

観点Delegate継承
柔軟性高い低い
再利用性高い限定的
結合度
テスト容易難しい

まとめ

KotlinのDelegateは、

  • プロパティの振る舞いを外部化できる
  • クラスの実装を安全に委譲できる
  • 設計の見通しを大幅に改善できる

という点で、Kotlinらしさを象徴する機能です。

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

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

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