覚えたら書く

IT関係のデベロッパとして日々覚えたことを書き残したいです。twitter: @yyoshikaw

Kotlin - アクセサの可視性変更

Kotlin では、プロパティのアクセサの可視性は、デフォルトではそのプロパティの可視性と同じです。

例えば以下のようなクラスを定義したとします。シャンプーを意味するクラスで変更可能な価格(price)をプロパティとして持っています。

class Shampoo {
    var price: Int = 1200
}

このプロパティのアクセサの可視性は特に指定おらず、 public になっています。

そのため、以下のように値を変更可能です

fun main() {
    val shampoo = Shampoo()
    println("Shampoo price ->  ${shampoo.price}(yen)")

    shampoo.price = 100 // 値を変更可能
    println("Shampoo new price ->  ${shampoo.price}(yen)")
}

実行結果は以下の通りです。

Shampoo price ->  1200(yen)
Shampoo new price ->  100(yen)


アクセサの可視性は、 get または set キーワードの前に可視性修飾子を記述する事で、可視性を変更可能です。


先ほどの Shampoo クラスの価格の変更は内部からのみに限定(private)にしたい場合はクラス定義は以下のようになります。

class Shampoo {
    var price: Int = 1200
        private set
}

そうするとクラスを外部から利用するコードにおける以下部分がエラーとなり、コンパイルが通らなくなります

shampoo.price = 100


上記の例は、 publicの可視性のプロパティを、setに関しては可視性をprivateに狭めるものでしたが、逆はどうなるのか。。。

以下のクラスを定義して price プロパティの可視性に private を指定します。

class Shampoo {
    private var price: Int = 1200
}

このコードを以下のように書き換えようとしてもコンパイルエラーとなります。

class Shampoo {
    private var price: Int = 1200
        public get // コンパイルエラー 
        public set // コンパイルエラー
}

private のプロパティのアクセサの可視性を広げるようなことはできないようです。
そもそも、こんなことをする状況が生まれないのかな。。。


まとめ

プロパティのアクセサの可視性を変更する方法を学びました。
public の可視性のプロパティのアクセサを private にすることなどが可能です。

Kotlin - getter/setter からのバッキングフィールドへのアクセス

Kotlin のクラスでは、フィールドを持つことができません。
しかし、カスタムアクセサを使用するときに バッキングフィールド が必要になることがあります。 この目的のために、Kotlinでは field という識別子を使用して自動的に作成されたバッキングフィールドにアクセスすることができます。


以下、商品を表現するクラスで、その商品の価格(price)を変更した場合はログ出力するようにします。

class Product(val serialNo: Int) {
    var price: Int = 10980
        set(value) {
            println("[Logging] Product price changed. [$field -> $value]")
            field = value
        }
}

price をミュータブルなプロパティで宣言して、カスタムの setter で 変更前の値と変更後の値を出力しています。

例えば以下のような実行コードが書けます

fun main() {
    val product1 = Product(100001)
    println("Product(S/N: ${product1.serialNo}) price = ${product1.price}")

    product1.price = 9980

    println("Product(S/N: ${product1.serialNo}) price = ${product1.price}")
}

実行結果は以下の通りです

Product(S/N: 100001) price -> 10980
[Logging] Product price changed. [10980 -> 9980]
Product(S/N: 100001) price -> 9980

product1.price = 9980 の 部分でプロパティの値を変更しています。
この際、内部的には setter が呼び出されています。
このクラスでは setter が再定義されていてログ出力をするようになっているので結果的に値の変更とともにログ出力も行われます。

先に書いた通りに、setterの中で、field という識別子を使ってバッキングフィールドの値にアクセスしています。


getterの場合は、値を読むことしかできませんが、setterの内部では値の読出しと変更の両方が可能です。と「Kotlinイン・アクション」には書かれていますが、

例えば以下のよなコードはコンパイルが通ります。 get() の中で field の値を書き換えできちゃってる気がするのですが、
私の理解不足なだけですかね。よくわかってない。。。

class Product(val serialNo: Int) {
    var price: Int = 10980
        set(value) {
            println("[Logging] Product price changed. [$field -> $value]")
            field = value
        }

        get() {
            field = field * 100
            return field
        }
}


まとめ

どの程度の頻度で利用することになるのかわかっていませんが、
field 識別子を利用することでカスタムアクセサの実装でバッキングフィールドにアクセスできる事がわかりました。

Kotlin - インターフェースで宣言したプロパティの実装

Kotlin ではインターフェースに抽象的なプロパティをを宣言する事が可能です。
例えば以下のようなものです。

interface Cat {
    val kind: String
}

Cat インターフェースを実装するクラスにおいて、kind の値を取得する手段を提供しなければならないという意味になります。


インターフェースは、その値がバッキングフィールドに格納されるか、getterを使って取得されるかの指定などはしていません。
インターフェースそのもは状態を持ちません。
インターフェースを実装するクラスのみが必要に応じて値を保持可能です。


先ほどのインターフェースに対する実装クラスは以下のようにいくつも可能性があります

// 猫種不明
class UnknownCat(override val kind: String) : Cat
// 雑種
class HybridCat : Cat {
    override val kind = "Hybrid"
}
// アメリカンショートヘア
class AmericanShortHair : Cat {
    override val kind: String
        get() = this::class.java.simpleName
}


UnknownCat の場合は、プライマリコンストラクタないでプロパティを直接宣言する構文を使っています。
このプロパティは、Catの抽象プロパティを実装しているため override と指定する必要があります。

HybridCat は初期化時に kind プロパティに値を代入しています。

AmericanShortHair はカスタムプロパティで、自身のクラス名を kindプロパティの値として返すようにしています。


interface Cat {
    val kind: String    // 猫種
    val name: String    // 名前
        get() = "${kind.toLowerCase()}-Miko"
}

このインターフェースの場合、抽象プロパティ kind とカスタムgetter を持った name プロパティを宣言しています。
kind プロパティは実装クラスによってオーバーライドされる必要がありますが、 name プロパティをそのまま使用できます。


まとめ

本エントリで対象とした記述範囲の区切りが変な感じもするのでまとめを書きにくいですが、
Kotolin でインターフェース内で宣言されたプロパティを実装する方法がわかりました。

Kotlin - コンストラクタ

今日も相変わらず 「Kotlinイン・アクション」 を読みながらの写経です。

Kotlinイン・アクション

Kotlinイン・アクション

  • 作者: Dmitry Jemerov,Svetlana Isakova,長澤太郎,藤原聖,山本純平,yy_yank
  • 出版社/メーカー: マイナビ出版
  • 発売日: 2017/10/31
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログ (2件) を見る


Java のクラスは1つ以上のコンストラクを持ちます。
Kotlin も似ていますが プライマリ(primary)コンストラクタとセカンダリ(secondary)コンストラクタを区別している点が異なります。
また、初期化ブロック(initializer block)に追加の初期化用ロジックを記述する事ができます。


プライマリコンストラクタ

以下はKotlinのクラス宣言になります。

class Person(val fullName: String, val age: Int)

このクラスには波括弧なしで、丸括弧に宣言があるだけとなっています。
このような丸括弧で囲まれたコードブロックは、プライマリコンストラクタ と呼ばれます。

プライマリコンストラクタは以下の2つの役割を持ちます

  • コンストラクタの引数を指定する
  • 引数で初期化されるプロパティを定義する

当然ですがコンストラクラタにより以下のようにインスタンスを生成できます

val person = Person("Kotlin Taro", 20)

先ほどのプライマリコンストラクの内容をもっと明示的に記述すると以下のようになります

class Person constructor(_fullName: String, _age: Int) {
    val fullName: String
    val age: Int
    init {
        fullName = _fullName
        age = _age
    }
}

constructor というキーワードは、プライマリコンストラクタもしくはセカンダリコンストラクタの宣言のはじめに付けられます。
init は、初期化ブロック(initializer block)を実装するためのキーワードとなります。
初期化ブロックは、クラスが生成された時に実行される初期化コードを含んでいて、プライマリコンストラクタと一緒に利用されることが意図されています。
プライマリコンストラクタの構文では、初期化コードを含めることができないためです。


初期化ブロックを用いた例を上述しましたが、上記例であれば初期化ブロックに初期化用のコードを書く必要はありません。
fullName, age プロパティの宣言と組み合わせる事ができるためです。
プライマリコンストラクタに対してのアノテーションや可視性修飾子が付かないのであれば constructorキーワードを省略可能です。
これらの内容を適用すると以下のように書く事ができます。

class Person(_fullName: String, _age: Int) {
    val fullName = _fullName
    val age = _age
}

クラス本体で val キーワードでプロパティを宣言していますが、プロパティが対応するコンストラクタ引数で初期化されるのであれば、
コンストラクタの引数の前に val キーワードを付ける事で、コードをシンプルにする事が可能です。
それが以下になります

class Person(val fullName: String, val age: Int)

最初に書いたクラス宣言と同じ記述となりました。これがもっとも簡潔な構文になります。


コンストラクタ引数は、関数の引数と同じようにデフォルト値を設定する事が可能です。

// age プロパティにデフォルト値を設定した
class Person(val fullName: String, val age: Int = 10)
val person1 = Person("Kotlin Taro", 20)
println("${person1.fullName} is ${person1.age} years old.")    //  -> Kotlin Taro is 20 years old.

// デフォルト値が利用される
val person2 = Person("Kotlin Jiro")
println("${person2.fullName} is ${person2.age} years old.")    //  -> Kotlin Jiro is 10 years old.


ちなみに、コンストラクタ引数の全てがデフォルト値を持つ場合、コンパイラは引数を持たずに全てデフォルト値を使用するコンストラクタを追加で生成します。
こうすることで、引数なしコンストラクタでのインスタンス化を行うようなライブラリでKotlinを利用しやすくなります。

// コンストラクの引数の全てにデフォルト値を持つ
class Person(val fullName: String = "Unknown", val age: Int = 10)

fun main() {
    val person = Person()  //  デフォルトコンストラクタの実行
    println("${person.fullName} is ${person.age} years old.")    // -> Unknown is 10 years old.
}


クラスがスーパークラスを継承している場合、プライマリコンストラクタはスーパークラスも初期化する必要があります。
基底クラスのリストで、スーパークラスの参照に続けてそのコンストラクタ引数を与える事で、初期化できます。

open class Animal(val name: String)

class Cat(name: String) : Animal(name)


コンストラクタを宣言しないクラスでは、何も引数をとらないデフォルトコンストラクタが生成されます

open class Shape

// デフォルトコンストラクタが生成されている
val shape = Shape()

コンストラクタを宣言していないクラス継承し、なおかつコンストラクタを持たないクラスの場合は、
スーパークラスが引数を持たなかったとしても、スーパークラスのコンストラクタを明示的に呼び出す必要があります。

class Circle : Shape()

ちなみに、インターフェースを実装する場合は、インターフェースはコンストラクタを持たないため、
親の型のリスト内のインターフェース名の後ろに丸括弧は必要ありません。


あるクラスを他のコードからインスタンス化できないように保証するには、コンストラクタを private にするひつようがあります。
以下はプライマリコンストラクタを private にしたものです。

class SecreteClazz private constructor() {}

このクラスは、privateなコンストラクタしか持たないため、このコードの外側からはインスタンス化することはできません。


セカンダリコンストラクタ

Java に比べて、Kotlin では複数のコンストラクタを持つクラスは一般的ではありません。
なぜならば、Javaでオーバーロードによって複数のコンストラクタを必要とする状況は、Kotlin では引数のデフォルト値と名前付き引数によってカバーされるからです。


それでも、複数のコンストラクタが必要となる状況は残されていて、複数の異なる方法でクラスを初期化するコンストラクタを持つフレームワークのクラスを拡張するような状況です。
Javaで宣言された2つのコンストラクタを持つ View クラスを想像した場合に、Kotlinでは以下のように宣言されます

open class View {
    constructor(ctx: Context) {
        // initialize code
    }
    constructor(ctx: Context, attr: Attributes) {
        // initialize code
    }
}

このクラスには、プライマリコンストラクタの宣言はありません(クラス宣言のクラス名の後ろに括弧がないことがそれを意味しています)
セカンダリコンストラクタは、constructor キーワードを使って導入し、必要な数だけ宣言可能です。

このクラスを拡張する場合、同じコンストラクタを宣言可能です。
以下では super() キーワードを使って、対応するスーパークラスのコンストラクタを呼び出しています。

class MyView : View {
    constructor(ctx: Context) : super(ctx) {
        // initialize code
    }
    constructor(ctx: Context, attr: Attributes) : super(ctx, attr) {
        // initialize code
    }
}

Javaと同じく this() キーワードを利用することも可能です

class MyView : View {
    constructor(ctx: Context) : this(ctx, DEFAULT_ATTRIBUTE) {
        // initialize code
    }
    constructor(ctx: Context, attr: Attributes) : super(ctx, attr) {
        // initialize code
    }
}


クラスにプライマリコンストラクタがなければ、それぞれのセカンダリコンストラクタが基底クラスを初期化するか、
基底クラスを初期化するための別のコンストラクタを呼び出す必要があります。

Javaとの互換性が、セカンダリコンストラクタが登場する主なユースケースとなります。


まとめ

Java にはなかった、プライマリコンストラクタとセカンダリコンストラクタがKotlinには存在している事がわかりました。
主に使うのはプライマリコンストラクタなのだと思いますが、両方の使い分け等について把握しておく必要があるかなと感じました。

Kotlin - シールドクラス

明示していない場合も少なくないですが、現時点で私が書いているKotlinのエントリは、ほぼ 「Kotlinイン・アクション」 を読みながらの写経です。

Kotlinイン・アクション

Kotlinイン・アクション

  • 作者: Dmitry Jemerov,Svetlana Isakova,長澤太郎,藤原聖,山本純平,yy_yank
  • 出版社/メーカー: マイナビ出版
  • 発売日: 2017/10/31
  • メディア: 単行本(ソフトカバー)
  • この商品を含むブログ (2件) を見る


式を表現する Expr インターフェース とそれを実装した数字を表現する Num クラス, 値の和を表す Sum クラスを定義し、
when 式によって、サブクラスごとの処理を分岐させて、計算結果を導き出す eval 関数を定義すると以下のようになります。

import java.lang.IllegalArgumentException

interface Expr

class Num(val value: Int) : Expr

class Sum(val left: Expr, val right: Expr) : Expr


fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.left) + eval(e.right)
        else -> 
            throw IllegalArgumentException("Unknown expression")
    }


実行するコードは以下のようになります。

fun main() {
    // 10 + 10
    val answer1 = eval(Sum(Num(10), Num(10)))
    println(answer1)  // -> 20

    // 1 + 20 + 30
    val answer2 = eval(Sum(Num(1), Sum(Num(20), Num(30))))
    println(answer2)  // -> 51
}


when で分岐にマッチしなかった場合の処理を指定するための else分岐 を用意する必要があります。
Kotlinのコンパイラでは、 when構文を使って式を評価するときに、デフォルトの選択肢のチェックが行われます。

上記の例では、Num と Sum 以外の場合意味のある値を返却する事ができないため、例外をスローしています。

常にデフォルトの分岐が必要になるのは不便であり、かつ、新しい実装クラスを増やした場合にwhenの分岐にそれが加わっているかどうかはコンパイラはチェックしてくれません。

このような問題を解決する策として シールドクラス が存在しています。
スーパークラスに sealed修飾子を設定すると生成可能なサブクラスを制限する事ができます。

シールドクラスを継承したサブクラスとして宣言するには、シールドクラスでネストされたクラスか、同じファイル内で宣言されたクラスとしてのみです。

先ほどの例はシールドクラスを利用する事で以下のように記述できます

sealed class Expr

class Num(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()


fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.left) + eval(e.right)
    }

when 式内でシールドクラスの全てのサブクラスを処理するのであれば、デフォルトの分岐を追加する必要がありません。


シールドクラスを継承したサブクラスを追加した場合は、when 式の分岐の中にも追記して、全てのサブクラスを網羅する必要があります。
以下の例では、 Subtract というクラスを追加しています

sealed class Expr

class Num(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()
class Subtract(val left: Expr, val right: Expr) : Expr()  // 新しく増やしたサブクラス

fun eval(e: Expr): Int =
    when (e) {
        is Num -> e.value
        is Sum -> eval(e.left) + eval(e.right)
        is Subtract -> eval(e.left) - eval(e.right)  // この行を追加しないとコンパイルエラーになる
    }


まとめ

簡単な例しか取り扱っていないため、シールドクラスの便利さを表現しきれていませんが、
今回のエントリでいうと when 式での分岐において余計なデフォルト処理の記述が不要となり、
かつ、シールドクラスの全てのサブクラスに対するケースの網羅をコンパイラが強制してくれる事がわかりました。