Infinito Nirone 7

白羽の矢を刺すスタイル

DroidKaigi 2020 撮影機材

カンファレンス撮影に関する詳しい話は @e10dokup さんが https://ameblo.jp/e10dokup/entry-12575646342.html でしてくれてるので、自分は DroidKaigi 2020 で使う予定だった撮影機材について、昨年からのアップデートを書いておこうと思います。

ボディ

  1. Canon EOS 6D MarkII: 前回も使ってたやつ。個人所有。手軽にフルサイズが楽しめてよいけど、長時間露光など、もうちょっと踏み込んだところの機能では EOS 5D に及ばないので、そろそろ 5D が欲しくなってきた。
  2. Canon EOS 5D MarkIV: 今回予定していたレンタル機材。せっかくなら使ってみないとね。絶対欲しくなる自身がある。

レンズ

  1. Canon EF 24-70mm f/2.8L II USM: キットレンズの 24-105mm がちょっと暗くて辛くなってきたので買っちった。これで会場内の様子を伝えるための引きの絵はバッチリや。
  2. Canon EF 70-200mm f/2.8L IS III USM: 前回レンタル機材としてつかったレンズ。買っちった。これでセッションの発表者をバッチリ撮るんや。

おわりに

去年からのアップデートと言うか、ただただレンズ買っちった報告になりました。次回はボディも個人所有で2台持ちになれるといいですね。

RxJava で delay など時間に関するオペレータを使ったときのテスト

例えば、次のコードのような 5秒後に "foo" という String を emit する Observable をかえす関数があるとします。

fun observeFoo(): Observable<String> = Observable.just("foo").delay(5, TimeUnit.SECONDS)

この関数をユニットテストすることを考えます。

RxJava では ObservableSingle などがどんな値を emit したかを検証する目的で TestObserver をつかってユニットテストを書きます。 ObservableSingle などには、この TestObserver に変換する test() メソッドがあるので、このあとに 1つめの値は "foo" である ことを確かめるメソッドをチェインしていきます。

@Test
fun observeFoo_getAfter5s() {
  observeFoo()
    .test()
    .assertValueAt(0) { value ->
      value == "foo"
    }
}

このテストはそのままでは動きません。なぜなら 5秒後 ということを無視しており、すぐさま assertValueAt で値が"foo"かどうか検証してしまうからです。

TestObserver には、値が X 個流れてくるまで待つために awaitCount メソッドが用意してあります。この場合は5秒後に1個値が出るので、次のようにすると正しくテストが動きます。

@Test
fun observeFoo_getAfter5s() {
  observeFoo()
    .test()
    .awaitCount(1) // 値が 1 個くるまで待つ
    .assertValueAt(0) { value ->
      value == "foo"
    }
}

ただし、この方法では、とにかく値が1個くるまでスレッドを止めて待つため、ユニットテストの実行を5秒間止めてしまいます。 同様のテストケースが増えれば増えるほど、勢いよくテストが遅くなるのは明らかです。

delay は ComutationScheduler を使って時間をはかり、指定した時間が経過したら値を emit します。 RxJava ではいろいろな Scheduler をテスト用に上書きするための RxJavaPlugins があり、さらに時間を仮想的に操作するための TestScheduler も用意してあります。 これらを用いて次のように記述すると、5秒間スレッドを止めることなく即座に値の検証が可能になります。

@Test
fun observeFoo_getAfter5s() {
   // delay は標準で computation scheduler を使うので、はじめに computation scheduler を test scheduler に上書きする
  val testScheduler = TestScheduler()
  RxJavaPlugins.setComputationSchedulerHandler { testScheduler }

  // TestObserver を得る
  val testObserver = observeFoo().test()

  // 仮想的に時間を 5 秒すすめる
  testScheduler.advanceTimeBy(5, TimeUnit.SECONDS)

  // "foo" が emit されているので検証ができる
  testObserver.assertValueAt(0) { value ->
    value == "foo"
  }
}

最後に、RxJavaPlugins で上書きしたものは、@After などで RxJavaPlugins.reset() で元に戻しておくことを忘れないでください。

Kotlin 1.4 の開発版を試す

やっていますか?

昨年の KotlinConf 2019 で Kotlin 1.4 についてのアナウンスがあり、今春のリリースを目指して開発が進んでいるそうです。 Releases · JetBrains/kotlin · GitHub を見ると、すでに現時点でも 1.4 向けの変更にタグが打たれているので、1.4-dev 版を試すことができるようになっています。

ビルドスクリプトとプロジェクトが見る Maven リポジトリを追加する

1.4-dev のタグがついた成果物は https://dl.bintray.com/kotlin/kotlin-dev/ に上がっています。 Gradle Plugin もここにあるので、Project Roon の build.gradle にある buildscript と、各モジュールの build.gradle にある repositories に次のようにして Maven リポジトリを設定しておきます。

maven {
  url "https://dl.bintray.com/kotlin/kotlin-dev/"
}

1.4-dev のビルド番号

この記事を書いた時点では一番新しいビルドとして 1.4.0-dev-1316 が Maven リポジトリにあり、ちょうど10秒くらい前に 1.4.0-dev-1325 のタグが GitHub に表示されるようになりました。そのうち 1.4.0-dev-1325 も Maven リポジトリに上がると思います。 ビルドスクリプトの dependencies には、implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.4.0-dev-1325" と記述すれば OK です。

注意

RC どころかアルファ版でもない開発版なので、ガンガン変更が入ります。1.4 でどうなるかちょっと試すくらいの気持ちで使ってください。

kotlinx.serialization でオブジェクトをシリアライズする #potatotips 67

potatotips #67 で kotlinx.serialization を使ったオブジェクトのシリアライズの手順について話してきたのでまとめておきます。いくつか発表のときにはできなかった部分を掘り下げています。

イベントリンク

potatotips #67 (iOS/Android開発Tips共有会) - connpass

資料

speakerdeck.com

kotlinx.serialization とは

Kotlin の各種オブジェクトを直列化して JSON や Protocol Buffers などのフォーマットに落とすためのしくみです。 2019年12月現在で最新版は 0.14.0 です。

導入

Project root にある build.gradle に kotlinx.serialization の classpath を追加し、各モジュールの build.gradle で apply plugin します。 Plugin のバージョンは Kotlin のバージョンと合わせます。

buildscript {
  ext.kotlin_version = '1.3.61'

  repositories {
    jcenter()
  }

  dependencies {
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
  }

}
apply plugin: "kotlin"
apply plugin: "kotlinx-serialization"

そして kotlinx-serialization-runtime を各モジュールの build.gradle の dependencies に追加します。

dependencies {
  implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0"
}

使い方

kotlinx.serialization では、オブジェクトを直列化する方法と、直列化したものを何らかのフォーマットに落とす方法が分かれていて、ここでは直列化する方法の部分を見ていきます。

まずクラスが Serializable かどうかをアノテーションで表明します。

import kotlinx.serialization.Serializable

@Serializable
data class SampleScreenState(
  val familyName: String,
  val givenName: String,
  val age: Int
)

メンバーがすべてプリミティブな型か、@Serializable がついている型の場合は自動で直列化のロジックも吐き出してくれます。これは直列化の方法を知っている KSerializer<T> の実装をプラグインが作ってくれて、そのインスタンスを返すメソッドを Companion Object に設定してくれるようになっています。

val sampleScreenState: SampleScreenState = // ...

// 直列化のロジックを持った KSerializer<SampleScreenState> の実装が得られる
val serializer = SampleScreenState.serializer()

次のように、@Serializable がついていない型をメンバーに持つ場合は、Serializer has not been found for type 'Any'. To use context serializer as fallback, explicitly annotate type or property with @ContextualSerialization というメッセージを吐き出してコンパイルエラーになります。なぜなら直列化の方法がわからないメンバーは直列化できないからです。java.io.Serializable とおなじく、@Serializable をつけたクラスのメンバーはすべてプリミティブな型 (IntDoubleString、あるいは enum class)か KSerializer<T> を定義した型 (@Serializable) でなければなりません。

シリアライズについての実装の間違いをコンパイル時に検知できてよいですね。

import kotlinx.serialization.Serializable

data class SampleData(
  val text: String
)

@Serializable
data class SampleScreenState(
  val familyName: String,
  val givenName: String,
  val age: Int,
  val something: SampleData = SampleData("sample") // ここがコンパイルエラー
)

SampleData を直列化するには、SampleData にも @Serializable をつけます。

import kotlinx.serialization.Serializable

@Serializable
data class SampleData(
  val text: String
)

@Serializable
data class SampleScreenState(
  val familyName: String,
  val givenName: String,
  val age: Int,
  val something: SampleData = SampleData("sample")
)

もしメンバーを直列化の対象外としたい場合は、@kotlinx.serialization.Transient アノテーションをつかいます。Kotlin JVM に標準の @kotlin.jvm.Transient ではないことに気をつけてください。

import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient

@Serializable
data class SampleScreenState(
  val familyName: String,
  val givenName: String,
  val age: Int,
  @Transient val something: SampleData = SampleData("sample")
)

KSerializer が生成できたら、あとはフォーマッターにシリアライズしたいオブジェクトと KSerializer を渡せば、好きなフォーマットに落とせます。

import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration

fun main() {
    val state = SampleScreenState(/* ... */)
    val json = Json(JsonConfiguration.Stable)

    val serialized: String = json.stringify(SampleScreenState.serializer(), state)

    val deserialized: SampleScreenState = json.parse(SampleScreenState.serializer(), serialized)
}

ライブラリに定義された型をシリアライズする

次のような型がライブラリに定義されているとします。

sealed class Abstract

class Sample4(
    val text: String,
    val number: Int
) : Abstract()

class class Sample5(
    val number: Int
) : Abstract()

ライブラリにあるので直接 @Serializable をつけられません。また KSerializer<T> も生成されないので自分で直列化の方法を定義する必要があり、上記の Abstract 型をメンバーとすると、Abstract 型と Sample4Sample5 の親子関係も知らせなければなりません。

kotlinx.serialization では、クラスの親子関係を後付けで定義するための仕組みとして SerializersModule を用意しています。これをつかって親子関係を知らせたうえで、Sample4Sample5 の直列化の方法も合わせて定義します。

import kotlinx.serialization.Decoder
import kotlinx.serialization.Encoder
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialDescriptor
import kotlinx.serialization.internal.SerialClassDescImpl
import kotlinx.serialization.modules.SerializersModule

@UseExperimental(ImplicitReflectionSerializer::class)
// フォーマッタに渡して使う
val abstractTypeModule = SerializersModule {
    polymorphic<Abstract> {
        addSubclass(Sample4Serializer)
        addSubclass(Sample5Serializer)
    }
}

object Sample4Serializer : KSerializer<Sample4> {
    override val descriptor: SerialDescriptor = object : SerialClassDescImpl(Sample4::class.java.name) {
        init {
            addElement("string") // 0 番目のプロパティ
            addElement("number") // 1 番目のプロパティ
        }
    }

    override fun deserialize(decoder: Decoder): Sample4 {
        val structure = decoder.beginStructure(descriptor)
        val string = structure.decodeStringElement(descriptor, 0) // 0 番目のプロパティから String をデコードする
        val number = structure.decodeIntElement(descriptor, 1) // 1 番目のプロパティから Int をデコードする
        structure.endStructure(descriptor)
        return Sample4(string)
    }

    override fun serialize(encoder: Encoder, obj: Sample4) {
        val structure = encoder.beginStructure(descriptor)
        structure.encodeStringElement(descriptor, 0, obj.string) // 0 番目のプロパティに String をエンコードする
        structure.encodeIntElement(descriptor, 1, obj.number) // 1 番目のプロパティに Int をエンコードする
        structure.endStructure(descriptor)
    }
}

object Sample5Serializer : KSerializer<Sample5> {
    override val descriptor: SerialDescriptor = object : SerialClassDescImpl(Sample5::class.java.name) {
        init {
            addElement("number")
        }
    }

    override fun deserialize(decoder: Decoder): Sample5 {
        val structure = decoder.beginStructure(descriptor)
        val number = structure.decodeIntElement(descriptor, 0)
        structure.endStructure(descriptor)
        return Sample5(number)
    }

    override fun serialize(encoder: Encoder, obj: Sample5) {
        val structure = encoder.beginStructure(descriptor)
        structure.encodeIntElement(descriptor, 0, obj.number)
        structure.endStructure(descriptor)
    }
}

これらを使って次の Sample6 を JSON にフォーマットすると、{"abstractSample":{"type":"dev.keithyokoma.Sample4","string":"foo","number":1}}が出力されます。"abstractSample"に対応する部分が JsonObject となり、この JsonObject ではもとがどの型だったかを覚えておくために "type" に FQCN が出力されます。

import kotlinx.serialization.Polymorphic
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration

@Serializable
data class Sample6(
    @Polymorphic val abstractSample: Abstract
)

fun main() {
    val json = Json(JsonConfiguration.Stable, abstractTypeModule)
    val serialized = json.stringify(Sample6.serializer(), Sample6(Sample4("foo", 1)))
    println(serialized)
}

sealed class のシリアライズ

sealed class もシリアライズできます。ただし、Kotlin と kotlinx.serialization のバージョンの組み合わせでJSON フォーマッタを使った場合の結果が変わります。

Kotlin 1.3.61 と kotlinx.serialization 0.14.0

次のように @Serializable で表明します。sealed class とその子クラスすべてに @Serializable をつけます。

import kotlinx.serialization.Serializable

@Serializable
sealed class Sealed

@Serializable
data class Sample2(
    val text: String
) : Sealed()

@Serializable
data class Sample3(
    val number: Int
) : Sealed()

@Serializable
data class Sample1(
    val text: String,
    val sealed: Sealed
)

これを次のように JSON でフォーマットすると、{"text":"foo","sealed":{"type":"dev.keithyokoma.Sample2","text":"bar"}} が出力されます。

import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration

fun main() {
    val sample = Sample1("foo", Sample2("bar"))
    val json = Json(JsonConfiguration.Stable)
    val serialized: String = json.stringify(Sample1.serializer(), sample)
    println(serialized)

Kotlin 1.3.30 以上 と kotlinx.serialization 0.11.0 から 0.13.0 まで

このバージョンでは、sealed class には @Serializable をつけずその子クラスに @Serializable をつけます。また sealed class の型のメンバーには @Polymorphic をつけます。 しかし実はこれだけでは不十分で、ライブラリの型をシリアライズするときに利用したSerializersModule で sealed class の親と子の継承関係を教えてあげる必要があります。今回は sealed class の子クラスには KSerializer がアノテーションで生成されるので、自分で記述することはありません。

import kotlinx.serialization.Polymorphic
import kotlinx.serialization.Serializable
import kotlinx.serialization.modules.SerializersModule

sealed class Sealed

@Serializable
data class Sample2(
    val text: String
) : Sealed()

@Serializable
data class Sample3(
    val number: Int
) : Sealed()

// フォーマッタに渡して使う
@UseExperimental(ImplicitReflectionSerializer::class)
val sampleTypeModule = SerializersModule {
    polymorphic<Sealed> {
        addSubclass<Sample2>()
        addSubclass<Sample3>()
    }
}

@Serializable
data class Sample1(
    val text: String,
    @Polymorphic val sealed: Sealed
)

これを次のように JSON でフォーマットすると、{"text":"foo","sealed":{"type":"dev.keithyokoma.Sample2","text":"bar"}} が出力されます。

import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration

fun main() {
    val sample = Sample1("foo", Sample2("bar"))
    val json = Json(JsonConfiguration.Stable, sampleTypeModule)
    val serialized: String = json.stringify(Sample1.serializer(), sample)
    println(serialized)

kotlinx.serialization 0.10.0 まで

このバージョンでは次の記述で問題なくシリアライズできます。

import kotlinx.serialization.Polymorphic
import kotlinx.serialization.Serializable

sealed class Sealed

@Serializable
data class Sample2(
    val text: String
) : Sealed()

@Serializable
data class Sample3(
    val number: Int
) : Sealed()

@Serializable
data class Sample1(
    val text: String,
    @Polymorphic val sealed: Sealed
)

これを次のようにフォーマットすると {"text":"foo","sealed":["dev.keithyokoma.Sample2",{"text":"bar"}]} が出力されます。"sealed"の構造に注目すると、kotlinx.serialization 0.10.0 では JsonArray にして出力することがわかります。JsonArray の最初の要素が元の型の FQCN で、2番目の要素にメンバーをフォーマットした JsonObject が入ります。

import kotlinx.serialization.json.Json

fun main() {
    val sample = Sample1("foo", Sample2("bar"))
    val json = Json()
    val serialized: String = json.stringify(Sample1.serializer(), sample)
    println(serialized)

互換モード

元がだどの型だったかを覚えておくための FQCN が 0.10.0 以前は JsonArray の最初の要素に入っていたのに対し、0.11.0 以降は JsonObject のプロパティに出力されます。これを、0.11.0 以降でも 0.10.0 以前のように JsonArray の形に出力して互換性を保つようにするオプションが用意されています。

val json = Json(JsonConfiguration.Stable.copy(useArrayPolymorphism = true), abstractTypeModule)

"type" という名前のメンバーを持つ型をシリアライズする

次のように、sealed class で表される型のひとつに type というメンバーを持つものがあるとします。

import kotlinx.serialization.Polymorphic
import kotlinx.serialization.Serializable

@Serializable
sealed class Sealed

@Serializable
data class Sample2(
    val type: String
) : Sealed()

@Serializable
data class Sample3(
    val number: Int
) : Sealed()

@Serializable
data class Sample1(
    val text: String,
    val sealed: Sealed
)

これをそのまま次のようにフォーマットすると、{"text":"foo","sealed":{"type":"dev.keithyokoma.Sample2","type":"bar"}}と出力されます。見事に "type" が 2 つできてしまいました。これをデシリアライズすると、2つ目の "type" を見に行ってしまい知らない型 (bar) になってしまいます。

import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration

fun main() {
    val json = Json(JsonConfiguration.Stable)
    val serialized = json.stringify(Sample1.serializer(), Sample1("foo", Sample2("bar")))
    println(serialized)
}

これを回避する方法は3つあります。

1つめはメンバーに Json にフォーマットするときの名前を与える方法です。次に示すように @SerialName で Json のプロパティ名を設定すると、結果は{"text":"foo","sealed":{"type":"dev.keithyokoma.Sample2","my_type":"bar"}}となります。

@Serializable
data class Sample2(
    @SerialName("my_type")
    val type: String
) : Sealed()

2つめはJsonの設定で FQCN を入れるプロパティ名を変える方法です。JsonConfiguration#classDiscriminator に任意の文字列を指定して、FQCN のプロパティ名が変えられます。次の例では {"text":"foo","sealed":{"clazz":"dev.keithyokoma.Sample2","type":"bar"}} を出力します。

import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonConfiguration

fun main() {
    val json = Json(JsonConfiguration.Stable.copy(classDiscriminator = "clazz"))
    val serialized = json.stringify(Sample1.serializer(), Sample1("foo", Sample2("bar")))
    println(serialized)
}

3つめは先ほどの互換モードを利用する方法です。JsonArray の要素の位置で決め打ちになるので名前が衝突することがなくなります。

OAuth 2.0 の Redirect URI で戻ってくる画面に Navigation Architecture Component の DeepLink を設定すると直面する問題と回避策

みなさん、やっていますか?私はやっています。

Slack API を使いたくて、Slack の OAuth の仕様通り Redirect URI を設定してアプリに auth codestate を戻すために Navigation Architecture Component を使った DeepLink の実装をしたところ、盛大にハマったので記録を残しておきます。

続きを読む

つよいレンズをもっていろいろ写真を撮ってきた

つい先日、Canon EF 24-70mm f/2.8L II USM というつよいレンズを購入しました。

目的はカンファレンス会場内でバッチリ撮影できるレンズをもつことなわけですが、素振りも必要ということで、RedBull Air Race に誘ってもらったのを機に写真を撮ったのと、DroidKaigi roadshow 福岡へ行ったのを機にイベントの撮影をしつつ翌日門司港でフラフラしたときに写真を撮ってきました。

RedBull Air Race

今回でシリーズそのものが終了となる RedBull Air Race。日本人パイロットが活躍している事は知っていたものの、競技のルールなどは予備知識ゼロで行きました。会場での解説を聴きながらでもルールが理解できるようになっていていいですね。

f:id:KeithYokoma:20190907135119j:plain

24-70mm なのでもちろん寄れないんですが、飛行機にフォーカスを合わせつつ観戦している人々をフレームに入れるにはちょうどいい感じでした。

f:id:KeithYokoma:20190907141904j:plain

f:id:KeithYokoma:20190907141906j:plain

門司港

戦時中、祖父が輸送船の乗組員だったころに来たことがあるという門司港に行ってみようと思って来てみました。ほぼ何も調べずに来てしまいましたが、地ビール醸造所があったり、唐揚げフェスをやっていたり、観光客向けに展望デッキがあったりと、短時間でもスッと楽しめる街でした。 町並みもレトロな感じでいいですね。

f:id:KeithYokoma:20190915140953j:plain

門司港に来るのが初めてなので、関門海峡を眺めるのも初めてです。

f:id:KeithYokoma:20190915141306j:plain

クラシックカーのあつまりもありました。かっこいいですね。

f:id:KeithYokoma:20190915141620j:plain

f:id:KeithYokoma:20190915143650j:plain

f:id:KeithYokoma:20190915144344j:plain

f:id:KeithYokoma:20190915144917j:plain

ゴンチャではなくコンチャが出店してました。

f:id:KeithYokoma:20190915150346j:plain

f:id:KeithYokoma:20190915152657j:plain

f:id:KeithYokoma:20190915155734j:plain

帰りは飛行機をいくつか。

f:id:KeithYokoma:20190915181141j:plain

f:id:KeithYokoma:20190915181148j:plain

f:id:KeithYokoma:20190915181150j:plain

f:id:KeithYokoma:20190915212019j:plain

What's next?

望遠のつよいやつほしい

Kotlin の enum class とシリアライズで気をつけること

Kotlin の enum クラスは、ほぼ Javaenum と同じように扱うことができます。

次の Kotlin コードは FOOBAR という Hoge 型の定数を定義しています。

enum class Hoge {
  FOO,
  BAR
}

enum クラスで定義した定数は String への変換 (nameメソッド) と、 String からの変換 (valueOfメソッド) をサポートしているので、簡単にシリアライズ・デシリアライズができます。またenum クラスに振る舞いを記述できるので、フィールドをもたせたり、メソッドを呼び出したりするコードも動きます。

enum class Hoge(val flag: Boolean) {
  FOO(true),
  BAR(false);

  fun getSomething(): String {
    return "Hello, world!"
  }
}

メソッドは abstract で定義したり、インタフェースを実装したりもできます。この場合、個々の定数の定義で具体的な処理を記述します。

enum class Hoge {
  FOO {
    override fun getSomething(): String {
      return "Hello, world!"
    }
  },
  BAR {
    override fun getSomething(): String {
      return "Hi, world!"
    }
  };

  abstract fun getSomething(): String
}

ここまでがおさらいです。

ここで次のように String? から任意の型のオブジェクトへ変換する Generic なメソッドを考えます。ストレージに書き込んだ文字列から期待する型への変換を行うようなメソッドです。

@Suppress("UNCHECKED_CAST")
fun <T : Any> String?.convert(fallback: T): T {
  return when (fallback) {
    is String -> {
      this ?: fallback
    }
    is Int -> {
      this?.let {
        Integer.valueOf(this)
      } ?: 0
    }
    is Enum<*> -> {
      this?.let {
        val method = fallback.javaClass.getDeclaredMethod("valueOf", String::class.java)
        method.invoke(null, this)
      } ?: fallback
    }
    else -> {
      throw IllegalArgumentException("")
    }
  } as T
}

このメソッドを、次のように標準入力で与えられた文字列からデシリアライズする目的で使ってみましょう。標準入力で FOO と入力すると、正しく定数に変換されます。

fun main(args: Array<String>) {
  println(readLine().convert<Hoge>(Hoge.FOO))
}

enum class Hoge {
  FOO,
  BAR
}

では次の場合はどうなるでしょうか。Hoge には abstract メソッドが定義されています。

fun main(args: Array<String>) {
  println(readLine().convert<Hoge>(Hoge.FOO))
}

enum class Hoge {
  FOO {
    override fun getSomething(): String {
      return "Hello, world!"
    }
  },
  BAR {
    override fun getSomething(): String {
      return "Hi, world!"
    }
  };

  abstract fun getSomething(): String
}

上記のコードの場合、FOO を標準入力に入れると次のようなエラーで異常終了します。

Exception in thread "main" java.lang.NoSuchMethodException: dev.keithyokoma.Hoge$FOO.valueOf(java.lang.String)
    at java.lang.Class.getDeclaredMethod(Class.java:2130)
    at dev.keithyokoma.MainKt.convert(Main.kt:22)
    at dev.keithyokoma.MainKt.main(Main.kt:6)

この挙動の差は生成されるバイトコードの違いを見ると分かります。

メソッドの定義がなかったり、実装を enum クラスの宣言に書く場合は次のようなバイトコードが生成されます。

public final enum dev/keithyokoma/Hoge extends java/lang/Enum  {
  // access flags 0x4019
  public final static enum Ldev/keithyokoma/Hoge; FOO
  // access flags 0x4019
  public final static enum Ldev/keithyokoma/Hoge; BAR
}

一方で、abstract メソッドやインタフェースのメソッドの実装を個別の定数で行う場合は次ようなバイトコードが生成されます。

public abstract enum dev/keithyokoma/Hoge extends java/lang/Enum  {

  // access flags 0x18
  final static INNERCLASS dev/keithyokoma/Hoge$FOO dev/keithyokoma/Hoge FOO
  // access flags 0x18
  final static INNERCLASS dev/keithyokoma/Hoge$BAR dev/keithyokoma/Hoge BAR

  // access flags 0x4019
  public final static enum Ldev/keithyokoma/Hoge; FOO
  // access flags 0x4019
  public final static enum Ldev/keithyokoma/Hoge; BAR
}

なにやら増えていますね。内部クラスとして Hoge$FOOHoge$BAR があるように見えます。そして実行時のエラーでは、Hoge$FOO に対して valueOf メソッドを探したが見つからなかった、というメッセージが出ています。

enum クラス内の個別の定数で実装を定義する場合 Hoge.FOOHoge$FOO 型で、Hoge.BARHoge$BAR 型として解釈されます。これらはすべて単なるクラスなので、Generic な型変換コードの val method = fallback.javaClass.getDeclaredMethod("valueOf", String::class.java)valueOf メソッドを見つけられません (Hoge$FOO 型の Hoge.FOO が格納された fallback 変数に対し valueOf を探そうとするが、valueOf メソッドは Hoge のクラスメソッドとして定義されるので見つからない)。

Hoge$FOO 型と Hoge$BAR 型はどちらも Hoge 型のサブクラスになっているので、次のように修正すると期待通りの動作をします。 fallback の型が enum かどうかを判別し、enum でなければその親クラスから valueOf メソッドを探します。

@Suppress("UNCHECKED_CAST")
fun <T : Any> String?.convert(fallback: T): T {
  return when (fallback) {
    // ...
    is Enum<*> -> {
      this?.let {
        val clazz = fallback.javaClass
        val method = if (clazz.isEnum) {
          clazz.getDeclaredMethod("valueOf", String::class.java)
        } else {
          clazz.superclass.getDeclaredMethod("valueOf", String::class.java)
        }
        method.invoke(null, this)
      } ?: fallback
    }
    // ...
  } as T
}

コンパイラの吐き出すバイトコードまでを考慮しないといけないコードになってしまいました (enum クラスは文法上継承できないはずなので、isEnumfalse のときに superclass を参照しにいくのは一見とても奇妙)。

ちなみに、Java で同様のコードを書いて検証してみたところ、Java でも同じ挙動をしました。

もし、型パラメータの T から直接 Class クラスを参照できれば、このような回りくどい記述は必要ありません。 次のコードでは正しく valueOf メソッドが探せます。

inline fun <reified T : Any> String?.convert(fallback: T): T {
  return when (fallback) {
    // ...
    is Enum<*> -> {
      this?.let {
        val method = T::class.java.getDeclaredMethod("valueOf", String::class.java)
        method.invoke(null, this)
    } ?: fallback
  }
  // ...
  } as T
}

2019/09/03 16:00 追記

型パラメータ TEnum<T> に限定できる場合は、リフレクションは不要になり、代わりに enumValueOf を使います (thank you @red_fat_daruma !)。

inline fun <reified T : Enum<T>> String?.convert(fallback: T): T {
  return this?.let { value ->
    enumValueOf<T>(value)
  } ?: fallback
}