[Kotlin]クラスにカスタムゲッター/セッターを定義する

プロパティの値を取り出すときに動くメソッドがゲッター(getter)、値を代入するときに動くメソッドがセッター(setter)です。
今回はこのゲッターやセッターをクラスに定義し、プロパティの入出力をカスタマイズしたいと思います。

クラスを準備する

まずはクラスを準備します。

class Player{
    val name = "rui hachimura"
}

プロパティにも変数と同じようにvalで宣言する読み取り専用プロパティと、varで宣言する変更可能プロパティの2種類があります。ここではnameという読み取り専用プロパティのみ作成しています。

そしてこれをmain関数内でインスタンス化します。

fun main() {
    val rui = Player()
    println(rui.name)
}
//rui hachimura

NBA選手「八村塁」のプロパティが出力されました。

カスタムゲッターを定義する

Kotlinではプロパティを設定すると、自動的にゲッターと(varで宣言されたプロパティなら)セッターが追加されます。しかしこれらは僕たちには見えません。自動的に追加されるゲッターやセッターは特に処理をしていないからです。

ゲッターが僕たちにも見えるように、このクラスにカスタムゲッターを定義してみましょう。

class Player{
    val name = "rui hachimura"
        get() = field.toUpperCase()
}

プロパティの下にget()=処理とすることで、そのプロパティにカスタムゲッターを定義することができます。ゲッターの処理内では「field」という名の変数でプロパティに代入されている値を参照します。(これについては後述)

もう一度main関数内でプロパティを呼び出してみると、

println(rui.name)     //RUI HACHIMURA

プロパティはカスタムゲッターにより、大文字に書き換えられて出力されます。

複数の処理を含むゲッター

この構文はゲッターやセッター特有のものではありません。ゲッターやセッターは単なるメソッドです。つまり通常の関数定義構文が使えます。

上の構文は単一式関数(処理が1つの関数)と同じものです。次は複数行にまたがる通常の関数定義構文を使って、このゲッターをさらにカスタマイズしてみます。

class Player{
    val name = "rui hachimura"
        get(){
            val s = field.split(" ")  //空白文字で区切り、Listにする
                    .map{it.capitalize()}   //Listの要素の最初の文字を大文字に
                    .joinToString(separator = " ")  //Stringに変換
            return s
        }
}

明示的なreturnを持つgetメソッドが出来上がりました。呼び出してみましょう。

println(rui.name)     //Rui Hachimura

これで名字も名前も、最初の文字だけが大文字になります。

カスタムセッターを定義する

次にセッターです。セッターはプロパティに値が代入された時に働くメソッドです。そのため書き換え可能な(varで宣言された)プロパティが必要になります。クラスに新たなプロパティを付け加えましょう。

class Player{
    val name = "rui hachimura"
        get(){
            val s = field.split(" ")
                        .map{it.capitalize()}
                        .joinToString(separator = " ")
            return s
        }
//以下でteamプロパティを初期化
    var team = ""
}

main関数側でこのプロパティに値を代入してみます。

rui.team = "ワシントン"
println(rui.team)     //ワシントン

teamプロパティはvarで定義されているため、後から値を書き換えることができます。値の代入には変数定義と同じ=演算子を使います。

セッターもゲッターと同じように、このままでは姿が見えません。クラスに戻ってカスタムセッターを追加しましょう。

class Player{
    val name = "rui hachimura"
        get(){
            val s = field.split(" ")
                .map{it.capitalize()}
                .joinToString(separator = " ")
            return s
        }

    var team = ""
        set(value){     //ここからセッターの定義部分
            field = if(value.contains("ワシントン")){
                "ワシントン・ウィザーズ"
            }else{value}
        }
}
rui.team = "ワシントン"
println(rui.team)     //ワシントン・ウィザーズ

main関数内の記述は同じです。しかしカスタムセッターによって、“ワシントン”を含む文字列の代入は”ワシントン・ウィザーズ“に書き換えられます。

set(value)のように、セッターには必ず引数があります。プロパティに値を代入するということは、セッターに値を渡すことと同義だからです。

バッキングフィールド

カスタムゲッターが使用した変数「field」はゲッターやセッター(総称してアクセサメソッドともいいます)だけが使える特殊な変数です。これによってアクセサメソッドはクラスのバッキングフィールドに存在する値にアクセスします。

バッキングフィールドとは、プロパティに代入された値を格納するためのスペースだとイメージしておいてください。

呼び出し側はこのバッキングフィールドには直接アクセスできません。アクセサメソッドはその受付となり、バッキングフィールドの値を書き換えたり、値を整形して出力したりしています。

バッキングフィールドを生成しない「算出プロパティ」

バッキングフィールドはほとんどのプロパティで生成されますが、例えばこんなゲッターが設定されたプロパティに関しては生成されません。

class Player(){
    val strength
        get() = (1..5).shuffled().first()
}

Playerクラスのstrengthプロパティは、呼び出される度にランダムに算出されてgetterからreturnされます。このようなプロパティを算出プロパティと呼びます。

この場合、このゲッターはバッキングフィールドの値を参照していません。参照する必要が無ければバッキングフィールドは生成されることはありません。

ゲッター/セッターを定義できない場合

ここからは今回の補足です。
通常のクラス定義のヘッダにはコンストラクタの役割によって、3種類の記述方法があります。(実際にはこれらを組み合わせてクラスを定義します)

  1. コンストラクタが引数を取らない
  2. コンストラクタによってプロパティを初期化する
  3. コンストラクタは引数を取り、クラス内部でプロパティを初期化する
class Example{...}     //引数を取らない
class Example(val name: String){...}   //引数を取り、プロパティを初期化する
class Example(name: String){val name = name}  //引数を取り、初期化は内部で行う

このうち2番目の構文は最もKotlinらしく簡潔です。しかしこの記述ではカスタムのアクセサメソッドを定義することができません

ゲッターやセッターを定義するなら1か3の構文を使用し、クラス内部でプロパティを初期化する必要があります。

class Player(val name: String, age: Int){...}

このようなヘッダを持つクラスの場合、nameプロパティにはカスタムのアクセサメソッドは定義できないので注意しましょう。

タイトルとURLをコピーしました