Kotlin の by は、「この処理は別のオブジェクトに任せる」と宣言するキーワードです。 使う場所は2つあり、クラス宣言に書くとインターフェースの実装を丸ごと任せるクラス委譲、プロパティに書くと値の読み書きを任せる委譲プロパティになります。by lazy や Delegates.observable は、後者のために標準ライブラリが用意している部品です。

この記事では、クラス委譲、lazy、observable、vetoable、notNull、Map への委譲、自作デリゲート(getValue / setValue、provideDelegate)を1本にまとめ、すべてのコードに実行出力を付けました。lazy のスレッドモード3種の違いと、委譲先のメソッドを上書きしたときにどちらが呼ばれるかも、出力で確かめます。

確認環境:Ubuntu 24.04 / Kotlin 2.4.20(kotlinc)/ OpenJDK 21.0.10。実行は kotlinc A.kt -include-runtime -d a.jar && java -Dstdout.encoding=UTF-8 -jar a.jar で行いました(-Dstdout.encoding=UTF-8 は日本語の出力を UTF-8 にそろえる指定です)。

by 委譲の早見表

書き方 種類 任せる内容 主な用途
class A(b: B) : I by b クラス委譲 インターフェース I の全メンバーの実装 継承を使わずに機能を足す・一部だけ変える
val x by lazy { ... } 委譲プロパティ 最初に読んだときの初期化と、その後の保持 作るのに時間がかかる値を必要になるまで作らない
var x by Delegates.observable(初期値) { p, old, new -> } 委譲プロパティ 代入のあとに処理を呼ぶ 変更の記録・通知
var x by Delegates.vetoable(初期値) { p, old, new -> 条件 } 委譲プロパティ 代入の前に検査し、false なら取り消す 範囲外の値を受け付けない
var x: Int by Delegates.notNull() 委譲プロパティ あとから代入する非 null の値 lateinit を付けられない Int などの後入れ
val x: String by map 委譲プロパティ プロパティ名をキーに Map から読む 設定値や解析結果を型付きで読む
var x by this::other 委譲プロパティ 別のプロパティへの読み書き 名前を変えたプロパティの別名
var x by 自作クラス() 委譲プロパティ getValue / setValue に書いた処理 整形・範囲の制限など共通処理の再利用

標準デリゲートの早見表

デリゲート val / var 初期値 呼ばれるタイミング 初期化前に読むと
lazy { } val のみ ブロックの戻り値 最初の読み取り時に1回 その場で初期化される
Delegates.observable var 引数で渡す 代入のあと(同じ値でも毎回) 初期値が返る
Delegates.vetoable var 引数で渡す 代入の前(false で取り消し) 初期値が返る
Delegates.notNull var なし 処理は呼ばれない IllegalStateException
Map / MutableMap val / var Map の中身 読み書きのたびに Map を参照 キーがなければ NoSuchElementException

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Kotlin by 委譲の使い方 クラス委譲とby lazy・監視」

クラス委譲:インターフェースの実装を別のオブジェクトに任せる

class 名前(引数) : インターフェース by オブジェクト と書くと、そのインターフェースのメンバーをすべて、by の右のオブジェクトへ受け渡すコードをコンパイラが作ります。自分で書きたいメンバーだけ override します。

interface Notifier {
    fun send(message: String)
    fun sendAll(messages: List<String>)
}

class ConsoleNotifier : Notifier {
    override fun send(message: String) = println("  Console.send: $message")
    override fun sendAll(messages: List<String>) {
        println("  Console.sendAll: ${messages.size} 件")
        for (m in messages) send(m)   // 自分自身の send を呼ぶ
    }
}

// Notifier の実装をすべて inner に任せる
class PlainNotifier(inner: Notifier) : Notifier by inner

// send だけ上書きする
class PrefixNotifier(private val inner: Notifier) : Notifier by inner {
    override fun send(message: String) {
        println("  Prefix.send が呼ばれた")
        inner.send("[重要] $message")
    }
}

fun main() {
    println("1) すべて委譲")
    PlainNotifier(ConsoleNotifier()).send("こんにちは")

    println("2) 上書きした send を直接呼ぶ")
    PrefixNotifier(ConsoleNotifier()).send("こんにちは")

    println("3) 上書きしていない sendAll を呼ぶ")
    PrefixNotifier(ConsoleNotifier()).sendAll(listOf("A", "B"))
}
1) すべて委譲
  Console.send: こんにちは
2) 上書きした send を直接呼ぶ
  Prefix.send が呼ばれた
  Console.send: [重要] こんにちは
3) 上書きしていない sendAll を呼ぶ
  Console.sendAll: 2 件
  Console.send: A
  Console.send: B

上書きしたメソッドが呼ばれる場合と、呼ばれない場合

出力の 2) と 3) を比べてください。

  • 2) 外から send を直接呼ぶ:PrefixNotifier で上書きした send が呼ばれ、[重要] が付きます。
  • 3) 上書きしていない sendAll を呼ぶ:処理は委譲先の ConsoleNotifier.sendAll に渡り、その中で呼ばれる send は ConsoleNotifier 自身の send です。Prefix.send が呼ばれた は表示されず、[重要] も付きません。

委譲先のオブジェクトは、自分を包んでいるクラスのことを知りません。 継承では親クラスのメソッドから子クラスで上書きしたメソッドが呼ばれますが、委譲では呼ばれない、という点が大きな違いです。公式ドキュメントにも、上書きしたメンバーは委譲先オブジェクトのメンバーからは呼ばれず、委譲先は自分自身の実装にしかアクセスできないと書かれています。

呼び出し方 継承(open class を継承して override) クラス委譲(by)
上書きしたメソッドを外から呼ぶ 上書きした側 上書きした側
上書きしていないメソッドの中から、上書きしたメソッドを呼ぶ 上書きした側 委譲先の元の実装

sendAll でも [重要] を付けたいときは、sendAll も上書きして、その中で自分の send を呼ぶ形にします。

応用:既存のコレクションに機能を足す、複数のインターフェースを任せる

MutableList のように多くのメソッドを持つインターフェースでも、変えたいメソッドだけを書けば済みます。

class CountingList<T>(private val inner: MutableList<T> = mutableListOf()) : MutableList<T> by inner {
    var added = 0
        private set

    override fun add(element: T): Boolean {
        added++
        return inner.add(element)
    }

    override fun addAll(elements: Collection<T>): Boolean {
        added += elements.size
        return inner.addAll(elements)
    }

    // toString / equals / hashCode はインターフェースのメンバーではないので委譲されない
    override fun toString() = inner.toString()
}

interface Reader { fun read(): String }
interface Writer { fun write(text: String) }
class FileReader : Reader { override fun read() = "読み込んだ内容" }
class FileWriter : Writer { override fun write(text: String) = println("書き込み: $text") }

// 2つのインターフェースを別々のオブジェクトに委譲
class Storage(r: Reader, w: Writer) : Reader by r, Writer by w

fun main() {
    val list = CountingList<String>()
    list.add("a")
    list.addAll(listOf("b", "c"))
    println("中身 = $list / size = ${list.size} / added = ${list.added}")
    println("first = ${list.first()} / contains(b) = ${"b" in list}")

    val s = Storage(FileReader(), FileWriter())
    s.write(s.read())
}
中身 = [a, b, c] / size = 3 / added = 3
first = a / contains(b) = true
書き込み: 読み込んだ内容

size・first()・contains などは1行も書いていませんが、すべて inner に渡って動いています。add と addAll の両方を上書きしているのは、前の節で見たとおり、委譲先の addAll が内部で何を呼んでも CountingList.add には届かないためです。

toString・equals・hashCode は委譲されません。 これらはインターフェースのメンバーではないためで、上のコードから override fun toString() の行を外して実行すると、中身の表示は次のようになりました。

中身 = CountingList@372f7a8d / size = 3 / added = 3

クラス委譲と継承の選び方

観点 クラス委譲(by) 継承
相手の条件 インターフェースを実装していればよい(final クラスでもよい) open なクラスだけ
数 複数のインターフェースを別々のオブジェクトに任せられる 親クラスは1つ
任せる相手の差し替え コンストラクタ引数で渡すので差し替えやすい 宣言時に決まる
親の中の呼び出しへの影響 及ばない 及ぶ
委譲されないもの toString・equals・hashCode、インターフェースにないメソッド なし(すべて引き継ぐ)

by lazy:最初に読んだときに1回だけ初期化する

class Report {
    val summary: String by lazy {
        println("  (初期化ブロックを実行)")
        "集計結果"
    }
}

fun main() {
    val r = Report()
    println("インスタンスを作った")
    println("1回目: ${r.summary}")
    println("2回目: ${r.summary}")

    // lazy そのものを変数に持つと、初期化済みかを調べられる
    val holder = lazy { "値" }
    println("読む前 isInitialized = ${holder.isInitialized()}")
    println("value = ${holder.value}")
    println("読んだ後 isInitialized = ${holder.isInitialized()}")
}
インスタンスを作った
  (初期化ブロックを実行)
1回目: 集計結果
2回目: 集計結果
読む前 isInitialized = false
value = 値
読んだ後 isInitialized = true

初期化ブロックが動くのはインスタンスを作ったときではなく、最初に読んだときです。 2回目は保持した値が返るだけで、ブロックは動きません。by を使わず lazy { } の戻り値(Lazy 型)をそのまま持てば、isInitialized() で初期化済みかを調べられます。

lazy のスレッドモード3種の違い

lazy の第1引数に LazyThreadSafetyMode を渡すと、複数のスレッドから同時に読まれたときの動きを選べます。4つのスレッドに同時に読ませ、初期化ブロックが何回動いたか、各スレッドが何を受け取ったかを数えます。ブロックは「呼ばれた回数」を値として返します。

import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.thread

fun trial(mode: LazyThreadSafetyMode) {
    val calls = AtomicInteger(0)
    val value: Int by lazy(mode) {
        val n = calls.incrementAndGet()
        Thread.sleep(100)          // 初期化に時間がかかる想定
        n
    }
    val start = CountDownLatch(1)
    val results = java.util.Collections.synchronizedList(mutableListOf<Int>())
    val threads = (1..4).map {
        thread {
            start.await()
            results += value
        }
    }
    start.countDown()
    threads.forEach { it.join() }
    println("%-15s 初期化ブロックの実行回数=%d  各スレッドが受け取った値=%s  その後の値=%d"
        .format(mode, calls.get(), results.sorted(), value))
}

fun main() {
    trial(LazyThreadSafetyMode.SYNCHRONIZED)
    trial(LazyThreadSafetyMode.PUBLICATION)
    trial(LazyThreadSafetyMode.NONE)
}
SYNCHRONIZED    初期化ブロックの実行回数=1  各スレッドが受け取った値=[1, 1, 1, 1]  その後の値=1
PUBLICATION     初期化ブロックの実行回数=4  各スレッドが受け取った値=[3, 3, 3, 3]  その後の値=3
NONE            初期化ブロックの実行回数=4  各スレッドが受け取った値=[1, 2, 3, 4]  その後の値=1
モード 初期化ブロック 各スレッドが受け取る値 選ぶ場面
SYNCHRONIZED(指定しないとき) 1回だけ(ほかのスレッドは待つ) 全員同じ 迷ったらこれ
PUBLICATION 同時に複数回動くことがある 全員同じ(最初に書き込まれた1つ) ブロックが何度動いても差し支えなく、待ち時間を減らしたい
NONE 同時に複数回動くことがある スレッドごとに違う値になることがある 1つのスレッドからしか読まないと分かっている

PUBLICATION と NONE の数値は実行のたびに変わりますが、SYNCHRONIZED は必ず1回、PUBLICATION は回数が増えても全員が同じ値、NONE は値がそろわないことがある、という違いは出力のとおりです。公式ドキュメントでも、既定では同期され、値は1つのスレッドだけで計算されて全スレッドが同じ値を見ること、初期化が必ず使用と同じスレッドで起きると確信できる場合に NONE を使えることが説明されています。

Delegates.observable と vetoable:代入を監視する・取り消す

import kotlin.properties.Delegates

class Task {
    // 代入のあとに呼ばれる
    var status: String by Delegates.observable("未着手") { prop, old, new ->
        println("  ${prop.name}: $old -> $new")
    }

    // 代入の前に呼ばれ、false を返すと代入を取り消す
    var progress: Int by Delegates.vetoable(0) { _, old, new ->
        val ok = new in 0..100
        println("  progress: $old -> $new を${if (ok) "受け入れる" else "取り消す"}")
        ok
    }
}

fun main() {
    val t = Task()
    println("observable")
    t.status = "作業中"
    t.status = "作業中"      // 同じ値でも呼ばれる
    t.status = "完了"

    println("vetoable")
    t.progress = 50
    t.progress = 150
    println("  今の値 = ${t.progress}")
    t.progress = 100
    println("  今の値 = ${t.progress}")
}
observable
  status: 未着手 -> 作業中
  status: 作業中 -> 作業中
  status: 作業中 -> 完了
vetoable
  progress: 0 -> 50 を受け入れる
  progress: 50 -> 150 を取り消す
  今の値 = 50
  progress: 50 -> 100 を受け入れる
  今の値 = 100
項目 Delegates.observable Delegates.vetoable
ラムダが呼ばれるとき 代入のあと 代入の前
ラムダの戻り値 なし Boolean(false で代入を取り消す)
同じ値を代入したとき 呼ばれる(2行目の出力) 呼ばれる
取り消されたとき ― 例外は出ず、値が変わらないだけ

observable は同じ値の代入でも呼ばれます。 変わったときだけ処理したい場合は、ラムダの中で if (old != new) と比べます。vetoable で取り消されても例外は出ないので、呼び出し側に結果を知らせたいときは、代入の代わりに結果を返す関数を用意します。

Delegates.notNull:lateinit を付けられない型をあとから代入する

import kotlin.properties.Delegates

class Config {
    var port: Int by Delegates.notNull()      // プリミティブ型にも使える
    lateinit var host: String                 // Int には付けられない
}

fun main() {
    val c = Config()
    try {
        println(c.port)
    } catch (e: IllegalStateException) {
        println("notNull : ${e::class.simpleName}: ${e.message}")
    }
    try {
        println(c.host)
    } catch (e: UninitializedPropertyAccessException) {
        println("lateinit: ${e::class.simpleName}: ${e.message}")
    }
    c.port = 8080
    c.host = "localhost"
    println("${c.host}:${c.port}")
}
notNull : IllegalStateException: Property port should be initialized before get.
lateinit: UninitializedPropertyAccessException: lateinit property host has not been initialized
localhost:8080

lateinit var port: Int と書くと、次のコンパイルエラーになります。

LateInt.kt:2:5: error: 'lateinit' modifier is not allowed on properties of primitive types.
    lateinit var port: Int
    ^^^^^^^^
項目 lateinit var by Delegates.notNull()
Int・Long・Boolean・Double など 使えない(上のエラー) 使える
String やクラス型 使える 使える
初期化前に読んだときの例外 UninitializedPropertyAccessException IllegalStateException
初期化済みかの確認 ::host.isInitialized 用意されていない

参照型は lateinit、プリミティブ型は Delegates.notNull() と使い分けます。

Map への委譲:プロパティ名をキーにして読む

class User(map: Map<String, Any?>) {
    val name: String by map
    val age: Int by map
    val nickname: String? by map
}

class Settings(val map: MutableMap<String, Any?>) {
    var theme: String by map
    var fontSize: Int by map
}

fun main() {
    val u = User(mapOf("name" to "佐藤", "age" to 30, "nickname" to null))
    println("${u.name} / ${u.age} / ${u.nickname}")

    val s = Settings(mutableMapOf("theme" to "light", "fontSize" to 14))
    s.theme = "dark"
    s.fontSize += 2
    println("プロパティ: ${s.theme} / ${s.fontSize}")
    println("Map の中身: ${s.map}")

    println("--- キーが無いとき ---")
    try {
        println(User(mapOf("name" to "鈴木")).age)
    } catch (e: Exception) {
        println("${e::class.simpleName}: ${e.message}")
    }

    println("--- 型が合わないとき ---")
    try {
        println(User(mapOf("name" to "鈴木", "age" to "30")).age + 1)
    } catch (e: Exception) {
        println("${e::class.simpleName}: ${e.message}")
    }
}
佐藤 / 30 / null
プロパティ: dark / 16
Map の中身: {theme=dark, fontSize=16}
--- キーが無いとき ---
NoSuchElementException: Key age is missing in the map.
--- 型が合わないとき ---
ClassCastException: class java.lang.String cannot be cast to class java.lang.Number (java.lang.String and java.lang.Number are in module java.base of loader 'bootstrap')
  • val は Map、var は MutableMap に委譲します。 var に代入すると Map の中身も書き換わります。
  • キーが無い場合や型が合わない場合は、インスタンスを作ったときではなく、そのプロパティを読んだときに例外になります。 入力の形が定まらないデータを読むときは、先に必要なキーを確かめておくと原因を追いやすくなります。
  • 値が null のキー(nickname)は、プロパティの型を String? にしておけば null として読めます。

別のプロパティへの委譲:名前を変えたプロパティの別名を残す

by this::プロパティ名 と書くと、読み書きをそのまま別のプロパティへ渡せます。プロパティの名前を変えたあとも、以前の名前で書かれたコードを動かしておきたいときに使えます。

class Member(var displayName: String) {
    @Deprecated("displayName を使います", ReplaceWith("displayName"))
    var name: String by this::displayName
}

var counter = 0
var total: Int by ::counter

fun main() {
    val m = Member("田中")
    @Suppress("DEPRECATION")
    m.name = "高橋"
    println("displayName = ${m.displayName}")

    total += 5
    println("counter = $counter")
}
displayName = 高橋
counter = 5

自作デリゲート:getValue と setValue を書く

委譲プロパティの仕組みは単純で、by の右のオブジェクトが operator fun getValue(var なら setValue も)を持っていればよい、というだけです。プロパティを読むと getValue、代入すると setValue が呼ばれます。

import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

// 1) operator fun を直接書く
class Trimmed {
    private var value = ""
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        println("  get ${property.name}")
        return value
    }
    operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: String) {
        println("  set ${property.name} = \"$newValue\"")
        value = newValue.trim()
    }
}

// 2) ReadWriteProperty を実装し、関数で包んで使いやすくする
fun <T : Comparable<T>> clamp(initial: T, min: T, max: T) =
    object : ReadWriteProperty<Any?, T> {
        private var value = initial.coerceIn(min, max)
        override fun getValue(thisRef: Any?, property: KProperty<*>) = value
        override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
            this.value = value.coerceIn(min, max)
        }
    }

class Form {
    var title: String by Trimmed()
    var volume: Int by clamp(50, 0, 100)
}

fun main() {
    val f = Form()
    f.title = "  週次レポート  "
    println("title = \"${f.title}\"")

    f.volume = 130
    println("volume = ${f.volume}")
    f.volume = -5
    println("volume = ${f.volume}")

    // 関数の中のローカル変数にも使える
    var local: Int by clamp(0, 0, 10)
    local = 99
    println("local = $local")
}
  set title = "  週次レポート  "
  get title
title = "週次レポート"
volume = 100
volume = 0
local = 10
引数 意味 型の条件(公式ドキュメントより)
thisRef プロパティを持つオブジェクト(トップレベルやローカルなら null) プロパティの持ち主と同じ型か、その上位の型
property プロパティの情報。property.name で名前を読める KProperty<*> か、その上位の型
value(setValue のみ) 代入しようとしている値 プロパティと同じ型か、その上位の型

書き方は2通りあります。

  • operator fun を直接書く(Trimmed):インターフェースなしで書けます。
  • ReadWriteProperty / ReadOnlyProperty を実装する(clamp):引数の並びをインターフェースが決めてくれるので書き間違いにくく、object : 式で関数からその場で返せます。

var に setValue のないデリゲートを指定したとき、また by lazy の値に代入しようとしたときのエラー文は次のとおりです。

ValSet.kt:8:22: error: type 'ReadOnly' has no method 'setValue(Sample, KMutableProperty1<*, *>, String)', so it cannot serve as a delegate for var (read-write property).
    var text: String by ReadOnly()
                     ^^
ValSet.kt:14:7: error: 'val' cannot be reassigned.
    s.lazyText = "z"
      ^^^^^^^^

provideDelegate:by が書かれた時点で検査する

getValue が呼ばれるのはプロパティを読んだときです。インスタンスを作った時点で検査したい場合は provideDelegate を定義します。 by の右のオブジェクトが provideDelegate を持っていると、プロパティの初期化時にそれが1回呼ばれ、戻り値が実際のデリゲートになります。

import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty

val env = mapOf("APP_NAME" to "demo", "APP_PORT" to "8080")

class EnvValue(private val key: String) {
    // by が書かれた時点(インスタンス生成時)に1回呼ばれる
    operator fun provideDelegate(thisRef: Any?, property: KProperty<*>): ReadOnlyProperty<Any?, String> {
        println("  provideDelegate: ${property.name} <- $key")
        val value = env[key] ?: error("環境値 $key がありません(プロパティ ${property.name})")
        return ReadOnlyProperty { _, _ -> value }
    }
}

class AppConfig {
    val name: String by EnvValue("APP_NAME")
    val port: String by EnvValue("APP_PORT")
}

class BrokenConfig {
    val name: String by EnvValue("APP_NAME")
    val token: String by EnvValue("APP_TOKEN")
}

fun main() {
    println("AppConfig を作る")
    val c = AppConfig()
    println("作り終わった: ${c.name}:${c.port}")

    println("BrokenConfig を作る")
    try {
        BrokenConfig()
    } catch (e: IllegalStateException) {
        println("  ${e::class.simpleName}: ${e.message}")
    }
}
AppConfig を作る
  provideDelegate: name <- APP_NAME
  provideDelegate: port <- APP_PORT
作り終わった: demo:8080
BrokenConfig を作る
  provideDelegate: name <- APP_NAME
  provideDelegate: token <- APP_TOKEN
  IllegalStateException: 環境値 APP_TOKEN がありません(プロパティ token)

Map への委譲では「読んだとき」に例外が出ましたが、こちらは BrokenConfig() を呼んだ時点で、足りない設定がプロパティ名つきで分かります。provideDelegate の引数は getValue と同じなので、property.name を使ったメッセージを作れます。

どれを使うかの判断表

やりたいこと 使うもの
既存の実装を包んで一部だけ変えたい クラス委譲 : I by obj + 必要な override
重い初期化を、使うときまで遅らせたい by lazy { }
1つのスレッドからしか使わない値を軽く遅延初期化したい by lazy(LazyThreadSafetyMode.NONE) { }
値が変わったら記録・通知したい Delegates.observable
条件に合わない代入を受け付けたくない Delegates.vetoable、または範囲に収める自作デリゲート
Int や Boolean をあとから代入したい Delegates.notNull()
文字列や自作クラスをあとから代入したい lateinit var
Map の値を型付きのプロパティとして読みたい by map
同じ get / set の処理を複数のプロパティで使い回したい 自作デリゲート(ReadWriteProperty)
生成時にまとめて検査したい provideDelegate

つまずきやすい点のチェックリスト

症状 原因 直し方
クラス委譲で上書きしたメソッドが呼ばれない 委譲先の内部からの呼び出しは委譲先自身の実装へ行く 呼び出し元のメソッドも上書きする
println でクラス名と記号が表示される toString は委譲されない override fun toString() = inner.toString() を書く
'val' cannot be reassigned by lazy は val 専用 代入が必要なら var と別のデリゲートにする
has no method 'setValue(...)' var に読み取り専用のデリゲートを指定した setValue を足すか val にする
Property ... should be initialized before get. notNull のプロパティを代入前に読んだ 読む前に代入する順序にする
Key ... is missing in the map. Map 委譲でキーが無い キーの有無を先に確かめる、または provideDelegate で生成時に検査する
observable が同じ値でも呼ばれる 仕様(代入のたびに呼ばれる) ラムダで old != new を比べる
lazy のブロックが複数回動く PUBLICATION / NONE を複数スレッドで使っている 既定の SYNCHRONIZED に戻す

よくある質問

Q. by lazy と lateinit はどう使い分けますか? A. 値の作り方を宣言の場所に書けて、一度決まったら変わらないなら by lazy(val)です。あとから外部の処理が値を入れる、または入れ直すことがあるなら lateinit var です。Int などのプリミティブ型をあとから入れる場合は Delegates.notNull() を使います。

Q. クラス委譲と継承はどちらを選べばよいですか? A. 相手が open でないクラスの場合や、複数のインターフェースの実装を別々のオブジェクトに任せたい場合はクラス委譲が合います。親クラスの内部の呼び出しまで含めて動きを変えたい場合は継承が合います。

Q. 委譲プロパティはローカル変数にも使えますか? A. 使えます。自作デリゲートの例の最後で、関数の中の var local: Int by clamp(0, 0, 10) が範囲内に収まることを確かめました。公式ドキュメントにも、ローカル変数を委譲プロパティとして宣言でき、たとえばローカル変数を lazy にできると書かれています。

Q. by の右に書けるのはどんなオブジェクトですか? A. クラス委譲では、そのインターフェースを実装したオブジェクトです。委譲プロパティでは、getValue(var なら setValue も)を operator として持つオブジェクト、または provideDelegate でそれを返すオブジェクトです。Lazy・Map・プロパティ参照(::name)は、標準ライブラリがこれらの関数を用意しているので by に書けます。

まとめ

  • by には2つの使い方がある。クラス宣言に書くクラス委譲と、プロパティに書く委譲プロパティ
  • クラス委譲で上書きしたメソッドは、委譲先の内部からは呼ばれない。toString・equals・hashCode も委譲されない
  • by lazy は最初に読んだときに1回だけ初期化する。既定の SYNCHRONIZED は複数スレッドでもブロックが1回だけ動く
  • observable は代入のあと、vetoable は代入の前に呼ばれ、vetoable は false で代入を取り消す
  • Delegates.notNull() は lateinit を付けられないプリミティブ型のあと入れに使う
  • Map への委譲はプロパティ名がキー。キーが無いと読んだ時点で NoSuchElementException
  • 自作デリゲートは getValue / setValue を書くだけ。生成時に検査したいなら provideDelegate を足す

参考・出典

  • Kotlin 公式ドキュメント「Delegation」: https://kotlinlang.org/docs/delegation.html
  • Kotlin 公式ドキュメント「Delegated properties」: https://kotlinlang.org/docs/delegated-properties.html

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Kotlin by 委譲の使い方 クラス委譲とby lazy・監視」

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?