Kotlin

Kotlinチートシート: クラス&コンストラクター編

元の記事 → Swift to Kotlinチートシート

コンストラクターの書き方は色々ある。
いくつか制約があり、適当に書くとコンパイル通らないことが多い。

基本形

class Boo() {}

クラス名の後に書く()が基本となるプライマリコンストラクター。
特に引数がなければ省略して以下のようにも書ける(引数なしコンストラクターが自動生成される?)

class Boo {}

プライマリーコンストラクターは必ず実行されなければならない。
別のコンストラクター(後述のセカンダリーコンストラクター)を書く場合は、その中でthisキーワードを使ってプライマリーコンストラクターを呼ぶ必要がある。
※プライマリーコンストラクターを明示的に記載しない場合は、セカンダリーコンストラクターで呼び出す必要はない

引数をとる

class Foo(bar: String, baz: String) {
    val hoge = bar
    val piyo = baz
}

コンストラクターに引数を設けた形。引数はメンバー変数につっこむことができる。
メンバー変数の定義は省略して以下の形でも書ける。Typescript同様。

class Foo(val hoge: String, val piyo: String) {}

初期化処理をする

class Foo(bar: String, baz: String) {
    val hoge = bar
    val piyo = baz
    init {
        // 初期化処理
    }
}

初期化ブロック(init)によって初期化処理を行うことができる。
初期化ブロックはプライマリーコンストラクタ、セカンダリーコンストラクタどちらを実行した場合でも実行される。
セカンダリーコンストラクタを実行した場合は、初期化ブロックが実行された後にセカンダリーコンストラクタの処理が実行される。

class Foo {
    init { }
    init { }
    init { }
}

ちなみに初期化ブロックは複数書けるが、あまり使い所はなさそう。

初期化ブロック(init)ではプロパティが初期化されていない場合がある

class Foo {
    init { printFlag() }
    val flag = true
    init { printFlag() }

    fun printFlag() {
        System.out.println(flag)
    }
}

上記を実行すると、以下の出力になる。

I/System.out: false
I/System.out: true

flagには初期値としてtrueをセットしているが、一つ目のinitではまだ初期化が行われておらずfalseになっている。
init のタイミングでは init より上に書かれたプロパティしか初期化されていない。
initはプロパティ宣言の下に書くのが無難。

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

class Hoge(foo: String) {
    constructor(foo: String, bar: String): this(foo) {}
}

2つめ以降のコンストラクターは constructor キーワードで書き、セカンダリコンストラクターという。
セカンダリコンストラクターでは必ずプライマリーコンストラクターを呼ぶ必要があり、this キーワードを使って呼び出す。(上記の例では、: this(foo) を書かないとエラーになる)

class Baz {
    constructor()
}

上記のようにセカンダリのみ書くこともできるが、あまり意味はない。
ちなみにセカンダリコンストラクターは何も処理がなければ {} を省略できる。

継承一番シンプル

ここから継承するパターン。

open class Bar {}
class Foo: Bar() {}

親クラスにopen修飾子をつけないと継承できない。
親のコンストラクターを呼び出す必要があるので、BarではなくBar()にする必要がある。

親のコンストラクターを呼ぶ

親のコンストラクターはクラス名の定義の後に()で書く方法の他、superで呼び出す方法がある。

open class Base {}
class Foo: Base {
    constructor(): super()
}

ただし、上記のように親のコンストラクターに引数がない場合、superを省略して書ける。

open class Base {}
class Foo: Base {
    constructor()
}

AndroidのViewを継承する場合

class CustomView: View {
    init {
       // 全コンストラクターで共通の初期化処理
    }
    constructor(context: Context): super(context)
    constructor(context: Context, attrs: AttributeSet): super(context, attrs)
    constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int): super(context, attrs, defStyleAttr)
}

コンストラクターの実行順序

class Sub: Base {
    init { /* 3 */ }
    constructor(string: String): super(1) { /* 4 */ }
}

open class Base(/* 1 */ val i: Int) {
    init { /* 2 */ }
}

上記の場合、以下の順番で処理が実行される。

  1. 親クラスのプライマリーコンストラクター
  2. 親クラスの初期化ブロック(init)
  3. 子クラスの初期化ブロック(init)
  4. 子クラスのセカンダリーコンストラクター

privateなコンストラクター

class Foo private constructor () {}

クラス名の後に private constructor が続くのが今までの書き方と比べて特殊に感じそうだが、public なコンストラクターは () の前にある constructorが省略されている形であることを知るとあまり違和感がない。

class Foo() {}

これは下記の省略形である。

class Foo constructor() {}

親クラスの初期化時に呼び出すメソッドを、子クラスでoverrideしてはいけない

クラスのコンストラクターや初期化ブロック(init)が実行されているタイミングでは、子クラスのプロパティーはまだ初期化せれておらず、nullや0やfalseが入った状態になっている。

以下の例では親クラスのinitでviewDidLoadを呼び、子クラスでoverrideしているが、viewDidLoadが実行されるタイミングではまだtextプロパティが初期化されていない。

open class UIViewController {
    init { viewDidLoad() }
    open fun viewDidLoad() {}
}

class FooViewController: UIViewController() {
    val text: String = "Hello World"

    override fun viewDidLoad() {
        super.viewDidLoad()
        println(text) // textはまだ値が入っておらず、nullになっている
    }
}