13日目:プロパティとフィールド
一昨日はクラスの話をしました。クラスは、そのメンバとして関数とプロパティを持つことができます。今日はプロパティについてお話ししたいと思います。
まずはJavaの慣例のおさらい
Javaではpublicなフィールドを避けるべきであるとされています。メソッド呼び出しのオーバヘッドが無視できない状況を除けば、すべてのフィールドをprivateにし、publicなメソッドを介してフィールドにアクセスすべきです。フィールドを非公開にすることによって適切にオブジェクトはカプセル化されます。
しかし、setterやgetterの呼び出しはもちろんのこと、クラスに逐一定義していくのは非常に退屈で面倒な作業ですし、定型コードであふれ返った見通しの悪いクラスが出来てしまいます。これを上手く解決する仕組みであるプロパティをKotlinでは導入しています。
プロパティ
Kotlinにおけるプロパティは、一見フィールドのように見えます。
class Rectangle() { var width = 1.0 var height = 1.0 } fun main(args : Array) { val rect = Rectangle() rect.width = 2.0 println(rect.width) // 2.0 println(rect.height) // 1.0 }
プロパティを宣言すると、Kotlinコンパイラはフィールドと2つのアクセサ(getterとsetter)を内部的に作成します。しかし通常、フィールドに直接アクセスすることはできず、アクセサを経由することになります。上記のコードではデフォルトのアクセサを使用して width の値を読み書きしていたのです。
プロパティによって、直接フィールドを扱っているような記述になるので直感的にプログラミングできる上、記述量も減り、可読性も向上します。
アクセサの定義
アクセサは自動生成されますが、独自のアクセサを定義することが可能です。ミュータブルなプロパティ(すなわちvarキーワードの付いたプロパティ)はgetterとsetterを持ちます。イミュータブルなプロパティ(valキーワードの付いたプロパティ)はgetterのみを持ちます。アクセサの書式は次のとおりです。
var プロパティ名 : 型 = 初期値 get() { return 返す値 } set(value) { }
型や初期値は省略が可能な場合もあります。getterはgetという名前の関数のようなものです。プロパティの型と同じ型の値を返す必要があります。単一式関数のような記述も可能です。setterはgetterと同様、関数に似ています。setterは引数のようなものを取ります。引数には任意の名前が付けられますが、valueが一般的のようです。
class Rectangle() { var width = 1.0 var height = 1.0 val area : Double get() = width * height } fun main(args : Array) { val rect = Rectangle() rect.width = 3.0 rect.height = 4.0 println(rect.area) // 12.0 }
クラス Rectangle に面積用のプロパティ area を追加しました。そして、area には独自のgetterを用意しています。area 自体は値を持ちません。getterが代わりに値を返してくれるからです。そのため area はイミュータブルであるにも関わらず初期化する必要がありません。この場合、コンパイラは area のフィールドを作成しません。
バッキング・フィールドへのアクセス
Kotlinでは基本的にプロパティを使用してインスタンスの状態にアクセスしますが、直接フィールドへアクセスしたいときもあります。ちなみに、プロパティではなく「フィールド」を強調して指すために「バッキング・フィールド」と呼んだりしています。
クラス Rectangle の例を考えましょう。width と height はミュータブルなので、外部からの変更を許可しています。このクラスの利用者がうっかりマイナスの長さの長方形を作成しないように width と height に独自のsetterを定義しましょう。
class Rectangle() { var width = 1.0 set(value) { require(value >= 0) width = value } var height = 1.0 set(value) { require(value >= 0) height = value } val area : Double get() = width * height }
関数 require を各setterで使用しています。この関数は、引数のテスト結果が false の場合に IllegalArgumentException をスローします。事前条件の検査には持ってこいの関数です。ここでは代入しようとしている値が負数でないことを検査しています。
これで上手く行きそうです。実際、コンパイルも通ります。しかし待ってください。このクラスのインスタンスの width または height に値を代入しようとするとスタックオーバフローが起こります。なぜでしょうか。width や height がプロパティであることを思い出してください。上記のsetterでは値を検査した後、その値をプロパティへセットしようとしています。すると、無限ループに陥り永遠に値をセットできない上にスタックを食いつぶしてしまいます。
ここでバッキング・フィールドの登場です。上記のsetterの中で、新しい値をプロパティではなく、バッキング・フィールドへ直接セットできれば上手く行きます。バッキング・フィールドにアクセスするには、プロパティ名の先頭に $ を付けた名前を使用します。次のコードは期待通りに動いてくれます。
class Rectangle() { var width = 1.0 set(value) { require(value >= 0) $width = value } var height = 1.0 set(value) { require(value >= 0) $height = value } val area : Double get() = width * height } fun main(args : Array) { val rect = Rectangle() rect.width = 3.0 rect.height = 4.0 println(rect.area) // 12.0 }
まとめと次回予告
今日はKotlinにおけるプロパティとフィールドの扱い方についてお話ししました。Kotlinにおいてフィールドは基本的に隠れています。代わりにプロパティを使います。プロパティはフィールドを外部に晒すことなく外部とのやり取りを実現できます。それは従来のアクセサとは違いより直感的に記述できます。アクセサはコンパイラによって自動生成されますが、独自に定義することもできます。必要があれば、バッキング・フィールドに直接アクセスすることもできます。バッキング・フィールドが存在しないプロパティを作ることもできます。
明日は、オブジェクト指向を強力にしている重要な仕組みのひとつである継承について取り挙げます。
日記
アドベントカレンダー折り返し地点です...。文章を書くのが苦手なんで、よく頑張ったなぁって感じですw