- Kotlin
Kotlin by 委譲の使い方 クラス委譲とby lazy・監視
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期連続の黒字決算。