89
69

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Kotlin文法 - アノテーション、リフレクション、型安全なビルダー、動的型

Last updated at Posted at 2016-02-02

はじめに

Kotlin文法 - this、等価性、演算子オーバーロード、Null安全、例外の続き。

Kotlin ReferenceのOther章Annotations, Reflection, Type-Safe Builders, Dynamic Typeの大雑把日本語訳。適宜説明を変えたり端折ったり補足したりしている。

アノテーション

アノテーション宣言

アノテーションはコードにメタデータを付加する手段。アノテーションを宣言するには、class の前に annotation 修飾子を付ける。

// Fancyアノテーションを宣言する
annotation class Fancy

アノテーションクラスをメタアノテーションで修飾することで、アノテーションの付加属性を指定できる。

  • @Target はアノテーションを付けられる要素のあり得る種類(クラス、関数、プロパティ、式など)を指定する。
  • @Retention は、アノテーションをコンパイルされたクラスファイルに含めるかと、実行時にリフレクションを通して可視化するかどうかを指定する。デフォルトはどちらも true
  • @Repetableは1つのエレメントに同じアノテーションを複数回使うことを許可する。
  • @MustBeDocumented はアノテーションを公開APIの一部とするか、生成されるAPIドキュメントに表示されるクラスやメソッドのシグネチャに含めるべきかどうかを指定する。
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION,
        AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
public annotation class Fancy

使い方

@Fancy class Foo {
  @Fancy fun baz(@Fancy foo: Int): Int {
    return (@Fancy 1)
  }
}

もしクラスのプライマリコンストラクタを修飾したいなら、コンストラクタ宣言で constructor を付ける必要がある。アノテーションはその前に付ける。

class Foo @Inject constructor(dependency: MyDependency) {
  // ...
}

アクセサにも付けられる。

class Foo {
    var x: MyDependency? = null
        @Inject set
}

コンストラクタ

アノテーションはパラメータを取るコンストラクタを持つかもしれない。

annotation class Special(val why: String)

@Special("example") class Foo {}

パラメータの型として許されるのは以下。

  • Javaのプリミティブ型に対応する型(Int, Longなど)
  • 文字列
  • クラス(Foo::class)
  • enum
  • 他のアノテーション
  • 上に挙げた型の配列

アノテーションを他のアノテーションのパラメータとして使う場合、その名前の前に @ は付けない。

public annotation class ReplaceWith(val expression: String)

public annotation class Deprecated(
        val message: String,
        val replaceWith: ReplaceWith = ReplaceWith(""))

@Deprecated("This function is deprecated, use === instead", ReplaceWith("this === other"))

ラムダ

アノテーションはラムダにも使うことができる。ラムダのボディが生成される先の invoke() メソッドに適用される。これはQuasar(並列性の制御にアノテーションを使う)のようなフレームワークで便利。

annotation class Suspendable

val f = @Suspendable { Fiber.sleep(10) }

アノテーション利用場所ターゲット

プロパティやプライマリコンストラクタのパラメータを修飾するとき、Kotlinの要素に対応するJava要素が複数生成され、それゆえ生成されるJavaバイトコードにアノテーションが配置される場所が複数生じてしまう。アノテーションの生成を正確に指定するため、以下の書式が使える。

// コンストラクタの引数にvalを付けるとプロパティも一緒に生成される。
// プロパティにはバッキングフィールドとゲッターが生成される。
// アノテーションは引数に付くの?プロパティのフィールド?それともゲッター?
class Example(@field:Ann val foo,    // Javaフィールドを修飾する
              @get:Ann val bar,      // Javaのgetterを修飾する
              @param:Ann val quux)   // Javaコンストラクタ引数を修飾する

同じ書式はファイル全体の修飾にも使える。それにはファイルのトップレベルで、package 命令の前か、デフォルトパッケージなら全ての import の前に、file ターゲット付きのアノテーションを置く。

@file:JvmName("Foo")

package org.jetbrains.demo

同じターゲットに複数のアノテーションが必要なら、括弧を使って中にアノテーションを並べることで、ターゲットを繰り返し書くのを避けられる。

class Example {
     // @set:Inject @set:VisibleForTesting と書かなくても以下のように書ける
     @set:[Inject VisibleForTesting]
     public var collaborator: Collaborator
}

利用場所ターゲットの全てのリストは以下。

  • file
  • property (このターゲット付きのアノテーションはJavaからは見えない)
  • field
  • get(プロパティのゲッター)
  • set(プロパティのセッター)
  • receiver(拡張関数やプロパティのレシーバパラメータ)
  • param(コンストラクタ引数)
  • setparam(プロパティのセッター引数)

拡張関数のレシーバパラメータを修飾するには以下の書式を使う。

fun @receiver:Fancy String.myExtension() { }

利用場所ターゲットを指定しない場合、利用されているアノテーションの @Target に従って選択される。もし複数のターゲットがあり得るなら、次のリストから最初の適用可能なターゲットが使われる。

  • param
  • property
  • field

Javaアノテーション

JavaアノテーションはKotlinと100%互換性がある。

import org.junit.Test
import org.junit.Assert.*

class Tests {
  @Test fun simple() {
    assertEquals(42, getTheAnswer())
  }
}

Javaで書かれたアノテーションのパラメータは順番が定義されていないので、引数を渡すのに通常の関数呼び出しの書式は利用できない。代わりに名前付き引数を使う必要がある。

// Java
public @interface Ann {
    int intValue();
    String stringValue();
}
// Kotlin
@Ann(intValue = 1, stringValue = "abc") class C

Javaと同様に value パラメータは特別で、名前を明示しなくていい。

// Java
public @interface AnnWithValue {
    String value();
}
// Kotlin
@AnnWithValue("abc") class C

もしJavaの value パラメータが配列なら、Kotlinでは vararg (可変長)パラメータになる。

// Java
public @interface AnnWithArrayValue {
    String[] value();
}
// Kotlin
@AnnWithArrayValue("abc", "foo", "bar") class C

もしアノテーションの引数としてクラスを指定する必要があるなら、Kotlinクラス(KClass)を使う。Kotlinコンパイラは自動的にそれをJavaクラスに変換するので、Javaコードは普通にアノテーションと引数を見ることができる。

import kotlin.reflect.KClass

annotation class Ann(val arg1: KClass<*>, val arg2: KClass<out Any?>)

@Ann(String::class, Int::class) class MyClass

アノテーションインスタンスの値はKotlinコードにはプロパティに見える。

// Java
public @interface Ann {
    int value();
}
// Kotlin
fun foo(ann: Ann) {
    val i = ann.value
}

リフレクション

リフレクションは実行時にプログラムの構造を覗き見ることを可能とする言語やライブラリの仕組みである。Kotlinは関数やプロパティを第1級市民にしており、それらを覗き見る(つまり実行時に関数やプロパティの型や名前が分かる)ことは関数型やリアクティブスタイルを簡単に用いることと密接に結びついている。

注:Javaプラットフォームでは、リフレクション機能を使うランタイムコンポーネントは別のJARファイル(kotlin-reflect.jar)として配布される。これによってリフレクションを使わないアプリケーションに要求されるランタイムライブラリサイズを削減している。もしリフレクションを使うなら、プロジェクトのクラスパスにその .jar ファイルを追加するのを忘れないこと。

クラス参照

最も基本的なリフレクション機能は、Kotlinクラスへの実行時参照を取得すること。静的に分かっているKotlinクラスへの参照を取得するには、クラスリテラル書式が利用できる。

val c = MyClass::class

参照はKClass型の値。

Kotlinクラス参照はJavaクラス参照と同じではないことに注意。Javaクラス参照を得るには、KClass インスタンスの .java プロパティを使う。

関数参照

以下のような名前付き関数があるとき、

fun isOdd(x: Int) = x % 2 != 0

これを簡単に直接呼び出せる( isOdd(5) )。けどこれを値として渡すこともできる。つまり他の関数に。それには :: 演算子を使う。

val numbers = listOf(1, 2, 3)
println(numbers.filter(::isOdd)) // [1, 3]を表示する

ここで ::isOdd(Int) -> Boolean 関数型の値である。

:: 演算子はオーバーロード関数には使えないことに注意。将来的には、関数の特定のオーバーロードが選択可能なようにパラメータ型を指定する書式を提供することを計画している。

例:関数合成

次の関数を考えてみる。

fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C {
    return { x -> f(g(x)) }
}

これは渡された2つの関数を合成したものを返す。つまり compose(f, g) = f(g(*)) 。これを呼び出し可能な参照に適用できる。

fun length(s: String) = s.size

// isOdd(length(s)) の動作をする関数を作って oddLength に格納する
val oddLength = compose(::isOdd, ::length)
val strings = listOf("a", "ab", "abc")

// 渡した文字列のリストのうち、長さが奇数のものだけ表示する
println(strings.filter(oddLength)) // "[a, abc]"と表示される

プロパティ参照

Kotlinで第1級オブジェクトとしてプロパティにアクセスするのにも、:: 演算子が利用できる。

var x = 1

fun main(args: Array<String>) {
    println(::x.get()) // "1"が表示される
    ::x.set(2)
    println(x)         // "2"が表示される
}

::xKProperty<Int> 型のプロパティオブジェクトと等価である1。これを使うと get() で値を読みだしたり、name プロパティでプロパティ名を取得したりできる。詳細はKPropertyクラスのドキュメントを参照。

変更可能なプロパティ(例えば var y = 1)には、::yset() を持つ KMutableProperty<Int> を返す。

プロパティ参照は引数のない関数が期待される場所で使うことができる。

val strs = listOf("a", "bc", "def")
// mapの処理として、各String要素のlengthプロパティを指定する
println(strs.map(String::length)) // [1, 2, 3] を表示する

クラスメンバのプロパティにアクセスするには修飾する。

class A(val p: Int)

fun main(args: Array<String>) {
    val prop = A::p         // Aクラスのpプロパティ参照をpropに代入
    println(prop.get(A(1))) // "1"を表示する
}

拡張プロパティには以下のようにする。

// StringにlastChar拡張プロパティを用意
val String.lastChar: Char
  get() = this[size - 1]

fun main(args: Array<String>) {
  println(String::lastChar.get("abc")) // "c"を表示する
}

Javaリフレクションとの相互運用性

Javaプラットフォームでは、標準ライブラリがリフレクションクラスの拡張を用意している。その拡張がJavaリフレクションオブジェクトとの間のマッピングを提供する。例えば、バッキングフィールドやKotlinプロパティのgetterとして提供されるJavaメソッドを見つけるために、こんな風に書ける。

// 標準ライブラリが提供しているJavaとのリフレクション相互運用拡張を使う
import kotlin.reflect.jvm.*
 
class A(val p: Int)
 
fun main(args: Array<String>) {
    // KotlinクラスであるAのプロパティpのJavaでのGetterメソッドを表示
    println(A::p.javaGetter) // "public final int A.getP()" と表示される
    // KotlinクラスであるAのプロパティpのJavaでのバッキングフィールドを表示
    println(A::p.javaField)  // "private final int A.p" と表示される
}

Javaクラスに対応するKotlinクラスを取得するには、.kotlin 拡張プロパティを使う。

fun getKClass(o: Any): KClass<Any> = o.javaClass.kotlin

コンストラクタ参照

コンストラクタもメソッドやプロパティと同じように参照できる。コンストラクタと引数が同じで適切な型の戻り値を持つ関数型オブジェクトが期待される場所なら、どこでも利用できる。コンストラクタは :: 演算子の後にクラス名を加えることで参照できる。「引数なしで Foo を返す関数」を引数として期待する、次の関数を考えてみよう。

class Foo

fun function(factory : () -> Foo) {
    val x : Foo = factory()
}

::Foo を使うことで、Foo の引数なしコンストラクタを、こんな風に簡単に呼び出せる。

function(::Foo)

型安全なビルダー

ビルダーの概念はGroovyコミュニティではかなりポピュラー。ビルダーを用いると準宣言的にデータを定義できる。ビルダーはXML生成UIコンポーネントのレイアウト3Dシーンの描写などで便利。

多くの利用ケースで、Kotlinは型安全なビルダーを提供できる。型安全性であることでGroovyでの動的型付け実装よりも魅力的なものになる。

残りの事例では、Kotlinは動的片付けビルダーをサポートする。

型安全なビルダーの例

次のコードを考えてみよう。

import com.example.html.*

fun result(args: Array<String>) =
  html {
    head {
      title {+"KotlinでのXMLエンコーディング"}
    }
    body {
      h1 {+"KotlinでのXMLエンコーディング"}
      p  {+"このフォーマットはXMLマークアップの代替として利用できる。"}

      // 属性とテキストを持つ要素
      a(href = "http://kotlinlang.org") {+"Kotlin"}

      // 複合コンテンツ
      p {
        +"これは幾つかの"
        b {+"複合された"}
        +"テキスト。詳細は"
        a(href = "http://kotlinlang.org") {+"Kotlin"}
        +"プロジェクトを参照。"
      }
      p {+"いくらかのテキスト"}

      // 生成される内容
      p {
        for (arg in args)
          +arg
      }
    }
  }

これは完全に正当なKotlinコードである。ここで(変更してブラウザで実行して)このコードをオンラインで試すことができる。

どうやって動作しているか

Kotlinでの型安全なビルダーの実装メカニズムを順に見ていこう。まず最初に組み立てたいモデルを定義する。ここの例ではHTMLタグのモデルが必要だ。これは一連のクラスを用意すれば済む。例えば <html> タグを表すのは HTML クラスで、これは <head><body> を子供として定義する。(宣言は下を参照。)

コードの中でこんな風に書けるのは何故なのか思い出してみよう。

html {
 // ...
}

html は実際にはラムダ式を引数に取る関数だ。この関数は次のように定義されている。

fun html(init: HTML.() -> Unit): HTML {
  val html = HTML()
  html.init()
  return html
}

この関数はinit という名前の1つの引数を取り、その引数自身が関数である。(引数の)関数の型は HTML.() -> Unit で、これはレシーバ付きの関数だ。これは関数に HTML 型のインスタンス(レシーバ)を渡さなければならず、関数の中でそのインスタンスのメンバを呼び出せることを意味する。レシーバは this キーワードを通してアクセスできる。

html {
  this.head { /* ... */ }
  this.body { /* ... */ }
}

headbodyhtml のメンバ関数。)

ここで this は普通は省略できる。もうすでにすごくビルダーっぽい感じに見える。

html {
  head { /* ... */ }
  body { /* ... */ }
}

それで、この呼び出しは何をやってるんだろう?上で定義した html 関数のボディを見てくれ。新しい HTML インスタンスを生成してる。それから引数として渡された関数を呼び出して初期化する(この例では HTML インスタンスの headbody を呼び出しを行ってる)。それからそのインスタンスを返す。これこそビルダーがやるべきことだ。

HTML クラスの headbody 関数は html に似た感じで定義されている。唯一の違いは構築したインスタンスを HTML インスタンスの children に追加することだけ。

fun head(init: Head.() -> Unit) : Head {
  val head = Head()
  head.init()
  children.add(head)  // 内部のコレクションに追加する
  return head
}

fun body(init: Body.() -> Unit) : Body {
  val body = Body()
  body.init()
  children.add(body)  // 内部のコレクションに追加する
  return body

実際これら2つの関数は同じことをやっているので、ジェネリックバージョン initTag を用意できる。

  protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T {
    tag.init()
    children.add(tag)
    return tag
  }

するとさっきの関数はすっきりとこうなる。

fun head(init: Head.() -> Unit) = initTag(Head(), init)

fun body(init: Body.() -> Unit) = initTag(Body(), init)

これで headbody タグを構築するのにこれらが使えるようになった。

もう一個ここで考察するのは、どうやってテキストをタグの中に入れるかだ。上の例でこんな感じで書いた。

html {
  head {
    title {+"XML encoding with Kotlin"}
  }
  // ...
}

基本的にはタグの中に文字列を書くだけなのだが、その前に + がある。これは unaryPlus() 演算子関数を呼び出す。この演算子は(Title の親の)TagWithText 抽象クラスのメンバである拡張関数 unaryPlus によって定義されている。

// title内で + をStringに対して呼び出すと、StringのunaryPlusメソッドまたは拡張関数を探す。
// するとTitleの親クラスであるこのTagWithTextクラスに、String.unaryPlus拡張関数が見つかるので、
// 文字列をレシーバにしてこれを呼び出す。
abstract class TagWithText(name: String) : Tag(name) {
    operator fun String.unaryPlus() {
        children.add(TextElement(this))  // ここでのthisはレシーバで、型はString
    }
}

ここで前置の + がやっているのは、文字列を TextElement のインスタンスにラップして、それを children コレクションに追加している。これで文字列はタグ木構造の正式な一部になれた。

これら全ては上の方にあるビルダーの例のトップで import している com.example.html パッケージに定義されている。

com.example.htmlパッケージの定義全部

package com.example.html

interface Element {
    fun render(builder: StringBuilder, indent: String)
}

class TextElement(val text: String) : Element {
    override fun render(builder: StringBuilder, indent: String) {
        builder.append("$indent$text\n")
    }
}

abstract class Tag(val name: String) : Element {
    val children = arrayListOf<Element>()
    val attributes = hashMapOf<String, String>()

    protected fun <T : Element> initTag(tag: T, init: T.() -> Unit): T {
        tag.init()
        children.add(tag)
        return tag
    }

    override fun render(builder: StringBuilder, indent: String) {
        builder.append("$indent<$name${renderAttributes()}>\n")
        for (c in children) {
            c.render(builder, indent + "  ")
        }
        builder.append("$indent</$name>\n")
    }

    private fun renderAttributes(): String? {
        val builder = StringBuilder()
        for (a in attributes.keys) {
            builder.append(" $a=\"${attributes[a]}\"")
        }
        return builder.toString()
    }


    override fun toString(): String {
        val builder = StringBuilder()
        render(builder, "")
        return builder.toString()
    }
}

abstract class TagWithText(name: String) : Tag(name) {
    operator fun String.unaryPlus() {
        children.add(TextElement(this))
    }
}

class HTML() : TagWithText("html") {
    fun head(init: Head.() -> Unit) = initTag(Head(), init)

    fun body(init: Body.() -> Unit) = initTag(Body(), init)
}

class Head() : TagWithText("head") {
    fun title(init: Title.() -> Unit) = initTag(Title(), init)
}

class Title() : TagWithText("title")

abstract class BodyTag(name: String) : TagWithText(name) {
    fun b(init: B.() -> Unit) = initTag(B(), init)
    fun p(init: P.() -> Unit) = initTag(P(), init)
    fun h1(init: H1.() -> Unit) = initTag(H1(), init)
    fun a(href: String, init: A.() -> Unit) {
        val a = initTag(A(), init)
        a.href = href
    }
}

class Body() : BodyTag("body")
class B() : BodyTag("b")
class P() : BodyTag("p")
class H1() : BodyTag("h1")

class A() : BodyTag("a") {
    public var href: String
        get() = attributes["href"]!!
        set(value) {
            attributes["href"] = value
        }
}

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()
    html.init()
    return html
}

動的型

注:JVMをターゲットにしたコードでは動的型はサポートされない。

Kotlinは静的型付け言語でありながら、Javascriptエコシステムのような型のないまたは型付けのゆるい環境とも相互運用しないといけない。これらの利用状況を手助けするために、言語内で動的な型が利用できる。

val dyn: dynamic = ...

動的型は基本的にKotlinの型チェックを止める。

  • この型の値はどんな変数にも入れられ、パラメータとしてどこにでも渡せる。
  • どんな値も dynamic 型の変数に代入でき、パラメータとして dynamic を取る関数に渡せる。
  • これらの値でnullチェックは無効

dynamic 型の最も特徴的な機能は dynamic 変数に対してどんなプロパティや関数も呼び出せること。

dyn.whatever(1, "foo", dyn) // 'whatever' はどこにも定義されていない
dyn.whatever(*array(1, 2, 3))

Javascriptプラットフォームでは、このコードは「そのまま」コンパイルされる。つまり Kotlinでの dyn.whatever(1) は生成されたJavascriptコードでは dyn.whatever(1) となる。

動的呼び出しは常に結果として dynamic を返す。なので自由にチェイン呼び出しできる。

dyn.foo().bar.baz()

動的呼び出しにラムダを渡す時は、デフォルトで全てのパラメータの型は dynamic になる。

dyn.foo {
  x -> x.bar() // x は dynamic
}

次へ

これでKotlin文法はおしまい。お疲れ様!!

次はKotlin - Javaとの相互運用をどうぞ。

  1. 上の例でxはvarなのでKMutablePropertyになるはず。KPropertyになるのはvalのとき。

89
69
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
89
69

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?