Skip to content

中級: プロパティ

初級ツアーでは、プロパティがクラスインスタンスの特性を宣言するためにどのように使用され、それらにアクセスする方法について学びました。この章では、Kotlinでのプロパティの仕組みをさらに深く掘り下げ、コードでそれらを使用する他の方法を探ります。

バッキングフィールド

Kotlinでは、プロパティにはデフォルトのget()関数とset()関数があり、これらはプロパティアクセサーとして知られ、値の取得と変更を扱います。これらのデフォルト関数はコード内で明示的に表示されませんが、コンパイラはプロパティへのアクセスをバックグラウンドで管理するために自動的にそれらを生成します。これらのアクセサーは、実際のプロパティ値を格納するためにバッキングフィールドを使用します。

バッキングフィールドは、以下のいずれかの条件が真の場合に存在します。

  • そのプロパティにデフォルトのget()またはset()関数を使用する場合。
  • コード内でfieldキーワードを使用してプロパティ値にアクセスしようとする場合。

TIP

get()関数とset()関数は、ゲッターとセッターとも呼ばれます。

例えば、このコードにはカスタムのget()関数やset()関数を持たず、したがってデフォルトの実装を使用するcategoryプロパティがあります。

kotlin
class Contact(val id: Int, var email: String) {
    val category: String = ""
}

内部的には、これはこの擬似コードに相当します。

kotlin
class Contact(val id: Int, var email: String) {
    val category: String = ""
        get() = field
        set(value) {
            field = value
        }
}

この例では:

  • get()関数はフィールドからプロパティ値""を取得します。
  • set()関数はvalueをパラメータとして受け取り、それをフィールドに割り当てます。ここでvalue""です。

バッキングフィールドへのアクセスは、無限ループを引き起こすことなく、get()関数またはset()関数に追加のロジックを追加したい場合に役立ちます。例えば、nameプロパティを持つPersonクラスがあるとします。

kotlin
class Person {
    var name: String = ""
}

nameプロパティの最初の文字が大文字であることを保証したいとします。そこで、.replaceFirstChar().uppercase()拡張関数を使用するカスタムset()関数を作成します。しかし、set()関数内でプロパティを直接参照すると、無限ループが発生し、実行時にStackOverflowErrorが発生します。

kotlin
class Person {
    var name: String = ""
        set(value) {
            // This causes a runtime error
            name = value.replaceFirstChar { firstChar -> firstChar.uppercase() }
        }
}

fun main() {
    val person = Person()
    person.name = "kodee"
    println(person.name)
    // Exception in thread "main" java.lang.StackOverflowError
}

これを修正するには、fieldキーワードで参照することにより、代わりにset()関数でバッキングフィールドを使用できます。

kotlin
class Person {
    var name: String = ""
        set(value) {
            field = value.replaceFirstChar { firstChar -> firstChar.uppercase() }
        }
}

fun main() {
    val person = Person()
    person.name = "kodee"
    println(person.name)
    // Kodee
}

バッキングフィールドは、ロギングを追加したり、プロパティ値が変更されたときに通知を送信したり、古いプロパティ値と新しいプロパティ値を比較する追加のロジックを使用したりする場合にも役立ちます。

詳細については、「バッキングフィールド」を参照してください。

拡張プロパティ

拡張関数と同様に、拡張プロパティもあります。拡張プロパティを使用すると、既存のクラスのソースコードを変更することなく、新しいプロパティを追加できます。しかし、Kotlinの拡張プロパティはバッキングフィールドを持ちません。これは、get()関数とset()関数を自分で記述する必要があることを意味します。さらに、バッキングフィールドがないということは、状態を保持できないことを意味します。

拡張プロパティを宣言するには、拡張したいクラス名に続けて.とプロパティ名を記述します。通常のクラスプロパティと同様に、プロパティのレシーバー型を宣言する必要があります。例えば:

kotlin
val String.lastChar: Char

拡張プロパティは、継承を使用せずに、プロパティに計算された値を含ませたい場合に最も役立ちます。拡張プロパティは、レシーバーオブジェクトという1つのパラメータのみを持つ関数のように機能すると考えることができます。

例えば、firstNamelastNameという2つのプロパティを持つPersonというデータクラスがあるとします。

kotlin
data class Person(val firstName: String, val lastName: String)

Personデータクラスを変更したり、そこから継承したりすることなく、人のフルネームにアクセスできるようにしたいとします。これを行うには、カスタムget()関数を持つ拡張プロパティを作成します。

kotlin
data class Person(val firstName: String, val lastName: String)

// フルネームを取得するための拡張プロパティ
val Person.fullName: String
    get() = "$firstName $lastName"

fun main() {
    val person = Person(firstName = "John", lastName = "Doe")

    // 拡張プロパティを使用する
    println(person.fullName)
    // John Doe
}

NOTE

拡張プロパティは、クラスの既存のプロパティをオーバーライドできません。

拡張関数と同様に、Kotlin標準ライブラリは拡張プロパティを幅広く使用しています。例えば、CharSequencelastIndexプロパティを参照してください。

委譲プロパティ

クラスとインターフェースの章で、委譲についてすでに学びました。プロパティでも委譲を使用でき、そのプロパティアクセサーを別のオブジェクトに委譲できます。これは、データベーステーブル、ブラウザセッション、マップなどに値を格納するなど、単純なバッキングフィールドでは処理できない複雑なプロパティの格納要件がある場合に役立ちます。委譲プロパティを使用すると、プロパティの取得と設定のロジックが委譲先のオブジェクトにのみ含まれるため、ボイラープレートコードも削減されます。

構文はクラスでの委譲の使用に似ていますが、異なるレベルで動作します。プロパティを宣言し、その後にbyキーワードと委譲したいオブジェクトを続けます。例えば:

kotlin
val displayName: String by Delegate

ここで、委譲プロパティdisplayNameは、そのプロパティアクセサーのためにDelegateオブジェクトを参照します。

委譲するすべてのオブジェクトは、Kotlinが委譲プロパティの値を取得するために使用するgetValue()演算子関数を持たなければなりません。プロパティが可変の場合、Kotlinがその値を設定するためにsetValue()演算子関数も持たなければなりません。

デフォルトでは、getValue()関数とsetValue()関数は以下の構造を持っています。

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

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

これらの関数では:

  • operatorキーワードは、これらの関数を演算子関数としてマークし、get()関数とset()関数をオーバーロードできるようにします。
  • thisRefパラメータは、委譲プロパティを含むオブジェクトを参照します。デフォルトでは型はAny?に設定されていますが、より具体的な型を宣言する必要がある場合があります。
  • propertyパラメータは、値がアクセスまたは変更されるプロパティを参照します。このパラメータを使用して、プロパティの名前や型などの情報にアクセスできます。デフォルトでは型はAny?に設定されています。コードでこれを変更することを心配する必要はありません。

getValue()関数の戻り値の型はデフォルトでStringですが、必要に応じてこれを調整できます。

setValue()関数には追加のパラメータvalueがあり、これはプロパティに割り当てられる新しい値を保持するために使用されます。

では、これは実際にどのように見えるでしょうか?ユーザーの表示名のような計算されたプロパティを持ちたいとします。この操作はコストが高く、アプリケーションはパフォーマンスに敏感であるため、一度だけ計算されるようにします。委譲プロパティを使用して表示名をキャッシュすることで、一度だけ計算され、パフォーマンスに影響を与えることなくいつでもアクセスできるようにすることができます。

まず、委譲先のオブジェクトを作成する必要があります。この場合、オブジェクトはCachedStringDelegateクラスのインスタンスになります。

kotlin
class CachedStringDelegate {
    var cachedValue: String? = null
}

cachedValueプロパティにはキャッシュされた値が含まれます。CachedStringDelegateクラス内で、委譲プロパティのget()関数から望む動作をgetValue()演算子関数の本体に追加します。

kotlin
class CachedStringDelegate {
    var cachedValue: String? = null

    operator fun getValue(thisRef: Any?, property: Any?): String {
        if (cachedValue == null) {
            cachedValue = "Default Value"
            println("Computed and cached: $cachedValue")
        } else {
            println("Accessed from cache: $cachedValue")
        }
        return cachedValue ?: "Unknown"
    }
}

getValue()関数は、cachedValueプロパティがnullであるかどうかをチェックします。nullの場合、関数は"Default value"を割り当て、ロギングのために文字列を出力します。cachedValueプロパティがすでに計算されている場合、そのプロパティはnullではありません。この場合、ロギングのために別の文字列が出力されます。最後に、関数はエルビス演算子を使用して、キャッシュされた値を返すか、値がnullの場合は"Unknown"を返します。

これで、キャッシュしたいプロパティ(val displayName)をCachedStringDelegateクラスのインスタンスに委譲できます。

kotlin
class CachedStringDelegate {
    var cachedValue: String? = null

    operator fun getValue(thisRef: User, property: Any?): String {
        if (cachedValue == null) {
            cachedValue = "${thisRef.firstName} ${thisRef.lastName}"
            println("Computed and cached: $cachedValue")
        } else {
            println("Accessed from cache: $cachedValue")
        }
        return cachedValue ?: "Unknown"
    }
}

class User(val firstName: String, val lastName: String) {
    val displayName: String by CachedStringDelegate()
}

fun main() {
    val user = User("John", "Doe")

    // 最初のアクセスで値を計算し、キャッシュする
    println(user.displayName)
    // Computed and cached: John Doe
    // John Doe

    // 以降のアクセスではキャッシュから値を取得する
    println(user.displayName)
    // Accessed from cache: John Doe
    // John Doe
}

この例では:

  • ヘッダーにfirstNamelastNameの2つのプロパティを、クラス本体にdisplayNameという1つのプロパティを持つUserクラスを作成します。
  • displayNameプロパティをCachedStringDelegateクラスのインスタンスに委譲します。
  • userというUserクラスのインスタンスを作成します。
  • userインスタンスのdisplayNameプロパティにアクセスした結果を出力します。

なお、getValue()関数では、thisRefパラメータの型がAny?型からオブジェクト型であるUserに絞られています。これは、コンパイラがUserクラスのfirstNameおよびlastNameプロパティにアクセスできるようにするためです。

標準の委譲

Kotlin標準ライブラリは、便利なデリゲートをいくつか提供しているため、常にゼロから作成する必要はありません。これらのデリゲートのいずれかを使用する場合、標準ライブラリが自動的にgetValue()関数とsetValue()関数を提供するため、それらを定義する必要はありません。

遅延プロパティ

プロパティが最初にアクセスされたときにのみ初期化するには、遅延プロパティを使用します。標準ライブラリは委譲のためにLazyインターフェースを提供しています。

Lazyインターフェースのインスタンスを作成するには、lazy()関数を使用し、get()関数が初めて呼び出されたときに実行するラムダ式を渡します。get()関数のそれ以降の呼び出しは、最初の呼び出しで提供されたものと同じ結果を返します。遅延プロパティは、ラムダ式を渡すために末尾ラムダ構文を使用します。

例えば:

kotlin
class Database {
    fun connect() {
        println("Connecting to the database...")
    }

    fun query(sql: String): List<String> {
        return listOf("Data1", "Data2", "Data3")
    }
}

val databaseConnection: Database by lazy {
    val db = Database()
    db.connect()
    db
}

fun fetchData() {
    val data = databaseConnection.query("SELECT * FROM data")
    println("Data: $data")
}

fun main() {
    // databaseConnection に初めてアクセスする
    fetchData()
    // Connecting to the database...
    // Data: [Data1, Data2, Data3]

    // 以降のアクセスでは既存の接続を使用する
    fetchData()
    // Data: [Data1, Data2, Data3]
}

この例では:

  • connect()関数とquery()メンバー関数を持つDatabaseクラスがあります。
  • connect()関数はコンソールに文字列を出力し、query()関数はSQLクエリを受け取ってリストを返します。
  • databaseConnectionプロパティは遅延プロパティです。
  • lazy()関数に提供されるラムダ式は:
    • Databaseクラスのインスタンスを作成します。
    • このインスタンス(db)上でconnect()メンバー関数を呼び出します。
    • インスタンスを返します。
  • fetchData()関数は:
    • databaseConnectionプロパティ上でquery()関数を呼び出すことにより、SQLクエリを作成します。
    • SQLクエリをdata変数に割り当てます。
    • data変数をコンソールに出力します。
  • main()関数はfetchData()関数を呼び出します。最初に呼び出されると、遅延プロパティが初期化されます。2回目には、最初の呼び出しと同じ結果が返されます。

遅延プロパティは、初期化がリソースを大量に消費する場合だけでなく、プロパティがコード内で使用されない可能性がある場合にも役立ちます。さらに、遅延プロパティはデフォルトでスレッドセーフであり、これは並行環境で作業している場合に特に有利です。

詳細については、「遅延プロパティ」を参照してください。

監視可能プロパティ

プロパティの値が変更されたかどうかを監視するには、監視可能プロパティを使用します。監視可能プロパティは、プロパティ値の変更を検出し、この知識を使用して反応をトリガーしたい場合に役立ちます。標準ライブラリは委譲のためにDelegatesオブジェクトを提供しています。

監視可能プロパティを作成するには、まずkotlin.properties.Delegates.observableをインポートする必要があります。次に、observable()関数を使用し、プロパティが変更されるたびに実行するラムダ式を渡します。遅延プロパティと同様に、監視可能プロパティもラムダ式を渡すために末尾ラムダ構文を使用します。

例えば:

kotlin
import kotlin.properties.Delegates.observable

class Thermostat {
    var temperature: Double by observable(20.0) { _, old, new ->
        if (new > 25) {
            println("Warning: Temperature is too high! ($old°C -> $new°C)")
        } else {
            println("Temperature updated: $old°C -> $new°C")
        }
    }
}

fun main() {
    val thermostat = Thermostat()
    thermostat.temperature = 22.5
    // Temperature updated: 20.0°C -> 22.5°C

    thermostat.temperature = 27.0
    // Warning: Temperature is too high! (22.5°C -> 27.0°C)
}

この例では:

  • 監視可能プロパティtemperatureを含むThermostatクラスがあります。
  • observable()関数は20.0をパラメータとして受け取り、それを使用してプロパティを初期化します。
  • observable()関数に提供されるラムダ式は:
    • 3つのパラメータを持ちます:
      • _はプロパティ自体を参照します。
      • oldはプロパティの古い値です。
      • newはプロパティの新しい値です。
    • newパラメータが25より大きいかどうかをチェックし、結果に応じて文字列をコンソールに出力します。
  • main()関数は:
    • thermostatというThermostatクラスのインスタンスを作成します。
    • インスタンスのtemperatureプロパティの値を22.5に更新します。これにより、温度更新のプリント文がトリガーされます。
    • インスタンスのtemperatureプロパティの値を27.0に更新します。これにより、警告のプリント文がトリガーされます。

監視可能プロパティは、ロギングやデバッグの目的だけでなく、UIの更新や、データの有効性の検証のような追加のチェックを実行するユースケースにも使用できます。

詳細については、「監視可能プロパティ」を参照してください。

練習問題

演習1

あなたは書店の在庫管理システムを管理しています。在庫はリストとして保存されており、各項目は特定の本の数量を表します。例えば、listOf(3, 0, 7, 12)は、店に最初の本が3冊、2番目の本が0冊、3番目の本が7冊、4番目の本が12冊あることを意味します。

在庫切れの本すべてのインデックスのリストを返すfindOutOfStockBooks()という関数を記述してください。

ヒント1

標準ライブラリのindices拡張プロパティを使用してください。

ヒント2

手動で可変リストを作成して返す代わりに、buildList()関数を使用してリストを作成および管理できます。buildList()関数は、以前の章で学んだレシーバーを持つラムダを使用します。

|--|--|

kotlin
fun findOutOfStockBooks(inventory: List<Int>): List<Int> {
    // ここにコードを記述してください
}

fun main() {
    val inventory = listOf(3, 0, 7, 0, 5)
    println(findOutOfStockBooks(inventory))
    // [1, 3]
}

|---|---|

kotlin
fun findOutOfStockBooks(inventory: List<Int>): List<Int> {
    val outOfStockIndices = mutableListOf<Int>()
    for (index in inventory.indices) {
        if (inventory[index] == 0) {
            outOfStockIndices.add(index)
        }
    }
    return outOfStockIndices
}

fun main() {
    val inventory = listOf(3, 0, 7, 0, 5)
    println(findOutOfStockBooks(inventory))
    // [1, 3]
}

|---|---|

kotlin
fun findOutOfStockBooks(inventory: List<Int>): List<Int> = buildList {
    for (index in inventory.indices) {
        if (inventory[index] == 0) {
            add(index)
        }
    }
}

fun main() {
    val inventory = listOf(3, 0, 7, 0, 5)
    println(findOutOfStockBooks(inventory))
    // [1, 3]
}

演習2

キロメートルとマイルの両方で距離を表示する必要がある旅行アプリがあります。キロメートル単位の距離をマイルに変換するための、Double型のasMilesという拡張プロパティを作成してください。

NOTE

キロメートルをマイルに変換する式は、miles = kilometers * 0.621371です。

ヒント

拡張プロパティにはカスタムget()関数が必要であることを忘れないでください。

|---|---|

kotlin
val // ここにコードを記述してください

fun main() {
    val distanceKm = 5.0
    println("$distanceKm km is ${distanceKm.asMiles} miles")
    // 5.0 km is 3.106855 miles

    val marathonDistance = 42.195
    println("$marathonDistance km is ${marathonDistance.asMiles} miles")
    // 42.195 km is 26.218757 miles
}

|---|---|

kotlin
val Double.asMiles: Double
    get() = this * 0.621371

fun main() {
    val distanceKm = 5.0
    println("$distanceKm km is ${distanceKm.asMiles} miles")
    // 5.0 km is 3.106855 miles

    val marathonDistance = 42.195
    println("$marathonDistance km is ${marathonDistance.asMiles} miles")
    // 42.195 km is 26.218757 miles
}

演習3

あなたはクラウドシステムの健全性を判断できるシステムヘルスチェッカーを持っています。しかし、ヘルスチェックを実行できる2つの関数はパフォーマンスを大量に消費します。コストのかかる関数が必要なときにのみ実行されるように、遅延プロパティを使用してチェックを初期化してください。

|---|---|

kotlin
fun checkAppServer(): Boolean {
    println("Performing application server health check...")
    return true
}

fun checkDatabase(): Boolean {
    println("Performing database health check...")
    return false
}

fun main() {
    // ここにコードを記述してください

    when {
        isAppServerHealthy -> println("Application server is online and healthy")
        isDatabaseHealthy -> println("Database is healthy")
        else -> println("System is offline")
    }
    // Performing application server health check...
    // Application server is online and healthy
}

|---|---|

kotlin
fun checkAppServer(): Boolean {
    println("Performing application server health check...")
    return true
}

fun checkDatabase(): Boolean {
    println("Performing database health check...")
    return false
}

fun main() {
    val isAppServerHealthy by lazy { checkAppServer() }
    val isDatabaseHealthy by lazy { checkDatabase() }

    when {
        isAppServerHealthy -> println("Application server is online and healthy")
        isDatabaseHealthy -> println("Database is healthy")
        else -> println("System is offline")
    }
   // Performing application server health check...
   // Application server is online and healthy
}

演習4

あなたはシンプルな家計管理アプリを構築しています。このアプリは、ユーザーの残り予算の変化を監視し、特定のしきい値を下回るたびに通知する必要があります。Budgetクラスがあり、初期予算額を含むtotalBudgetプロパティで初期化されます。クラス内に、remainingBudgetという監視可能プロパティを作成し、以下を出力するようにしてください。

  • 値が初期予算の20%未満になったときに警告。
  • 予算が以前の値から増加したときに励ましのメッセージ。

|---|---|

kotlin
import kotlin.properties.Delegates.observable

class Budget(val totalBudget: Int) {
    var remainingBudget: Int // ここにコードを記述してください
}

fun main() {
    val myBudget = Budget(totalBudget = 1000)
    myBudget.remainingBudget = 800
    myBudget.remainingBudget = 150
    // Warning: Your remaining budget (150) is below 20% of your total budget.
    myBudget.remainingBudget = 50
    // Warning: Your remaining budget (50) is below 20% of your total budget.
    myBudget.remainingBudget = 300
    // Good news: Your remaining budget increased to 300.
}

|---|---|

kotlin
import kotlin.properties.Delegates.observable

class Budget(val totalBudget: Int) {
  var remainingBudget: Int by observable(totalBudget) { _, oldValue, newValue ->
    if (newValue < totalBudget * 0.2) {
      println("Warning: Your remaining budget ($newValue) is below 20% of your total budget.")
    } else if (newValue > oldValue) {
      println("Good news: Your remaining budget increased to $newValue.")
    }
  }
}

fun main() {
  val myBudget = Budget(totalBudget = 1000)
  myBudget.remainingBudget = 800
  myBudget.remainingBudget = 150
  // Warning: Your remaining budget (150) is below 20% of your total budget.
  myBudget.remainingBudget = 50
  // Warning: Your remaining budget (50) is below 20% of your total budget.
  myBudget.remainingBudget = 300
  // Good news: Your remaining budget increased to 300.
}

次のステップ

中級: Null安全性