型間違いでシステムクラッシュ?Inline Value ClassとSealed Classを使いこなそう【Kotlin・実例付き】

2025/02/10に公開

「ある日のデプロイで、サービス全体がクラッシュした――。」
新たなロジックをデプロイした瞬間、エラーが次々と発生。原因は、メートル (m) とフィート (ft) を取り違えたせいで、位置データが異常な数値を出し続けたことだった。

・・・というのは架空の話ですが、実際にNASAのミッションでも同様のミスが発生したそうです。1999年、NASAの火星探査機が単位変換ミスによって予定コースを逸脱し、火星の大気圏で消滅。数百万ドルもの損害が出たそうです[1]

A rocket

このように、 単位を厳密に区別しないことは、エンジニアにとって「ロケット級の危険」 なのです🚀 私たちの日常的な開発でも、異なるデータ型を正しく扱わないと「ロケット級」のバグを引き起こす可能性はあります。例えば、日本円とアメリカドルの区別、国コードと言語コードの区別、APIのステータスなどは、みなさんの開発環境でも「あるある」なシーンなのではないでしょうか。

嬉しいことに、Kotlinの型安全機能により、こうした単位の取り違えを未然に防ぐことができます。そこで、本記事では、Kotlinで型安全性を高める 「Inline Value Class」と「Sealed Class」 について、「単位間違いの例」を用いながら私の学んだことを整理します。

本記事を通じて、Kotlinの型安全機能である「Inline Value Class」と「Sealed Class」へのみなさんの理解が深まれば幸いです。

では、「Inline Value Class」と「Sealed Class」の使い分けを見ていきましょう!

Inline Value Class

Inline Value Classは、シンプルな型を定義することで、異なる型との混同を防ぎます。例えば、「メートル」と「フィート」という異なるInline Value Classにより、両者を取り違えることは防がれます。

実際に「メートル」と「フィート」をInline Value Classで表現すると、このようになります:

// メートルのInline Value Class
@JvmInline
value class Meters(val value: Double)

// フィートのInline Value Class
@JvmInline
value class Feet(val value: Double)

// メートルの距離を表示する関数
fun printMetersDistance(meters: Meters) {
    println("Distance: ${meters.value} meters")
}

// フィートの高さを表示する関数
fun printFeetAltitude(feet: Feet) {
    println("Altitude: ${feet.value} feet")
}

fun main() {
    // 土地の横幅
    val groundDistance = Meters(1000.0)
    // 山の標高
    val mountainHeight = Feet(3000.0)

    printMetersDistance(groundDistance)
    printFeetAltitude(mountainHeight)

    // コンパイルエラーの例(異なる型を渡すとエラー)
    // printMetersDistance(mountainHeight)  // エラー!
    // printFeetAltitude(groundDistance)    // エラー!
}

「メートル」と「フィート」を、Doubleではなくそれぞれのクラスとして表現したことで、型安全性が向上しましたね。

コード例では、アノテーション @JvmInline と 修飾子valueが特徴的です。なぜ、これらが必要なのでしょうか?

何が起きているか

アノテーション @JvmInline はなんのためにあるのでしょうか? これは、Kotlinコンパイラに対して「このクラスはインライン化されるべきだ」という指示を与えています。インライン化されたクラスは、ランタイム時に一つのプリミティブ型として扱われます。 これにより、Inline Value Classを利用した関数(コード例でいうprintMetersDistance(), printFeetAltitude() )が呼ばれた際には、クラスの新たなインスタンスが生成されるのではなく、プリミティブ型として渡されるのです。

例えば、以下のようなクラス:

@JvmInline
value class Meters(val value: Double)

コンパイル時には、基本的に次のように最適化されます:

val metersValue: Double = 1000.0

つまり、オブジェクトの生成は行われず、直接 Double の値がメモリ上に保存されるため、通常のオブジェクト生成に比べてパフォーマンスの向上が期待できるということです。 1秒間に数回しか呼ばれない関数であれば、パフォーマンスの問題は顕著でないかもしれませんが、例えば1秒間に100回呼ばれる関数であれば、差が出てくるはずです。個人的な興味ですが、実際に普通のクラスを使用した場合とインライン化されたクラスでどのような差が生じるのか自分の環境で実験してみたいと思いました。関数が呼ばれる回数の指数関数的に実行時間が長くなることは想像できますが、近いうちに試してみます。

余談:なぜわざわざアノテーション?

『Kotlin in Action, 2nd Edition』の第4章「Classes, objects, and interfaces」にて、@JvmInlineというアノテーションが必要な理由について興味深い点が言及されていました。現在このアノテーションが必要なのは、JVMがProject Valhalla[6](Javaのプリミティブ型拡張プロジェクト)を実装するまでの暫定的な仕様であるそうです。将来的に、Valhallaによるプリミティブ型の拡張が標準化されると、@JvmInline のアノテーションなしで同等の動作が期待されていると記載されていました。

今後、この文法は変わるかもしれませんね。

Inline Value Classの制約

とても便利なInline Value Classですが、いくつかの制約があります:

1. 単一のプロパティしか保持できない
@JvmInline を付けたクラスは、単一のプロパティしか持つことができません。以下のように複数のプロパティを持つことは許されません。

// これはコンパイルエラー!
// プロパティが複数あるため
@JvmInline
value class Meters(val value: Double, val anotherProperty: Double)

2. 継承不可
Inline Value Classは他のクラスを継承することができません。これは、インライン化の目的がパフォーマンスにあるため、複雑な継承を排除したいという意図によるものです。

本当にInline Value Classでいいの?

型の使われ方によっては、Inline Value Classが適していない場合があります。例えば、以下のような場合はどうでしょうか?

1. 複数のプロパティが必要!
Inline Value Classは単一のプロパティしか保持できません。複数のプロパティを同時に保持するケースでは、使えません。

2. 新しい型(例:センチメートル)をもっと追加したい!
新しい単位を追加するとき、Inline Value Classでは網羅性を担保することが難しいです。例えば、異なるファイルにまたがってInline Value Classが追加されていれば、システム上に存在する長さの単位を全て把握することは、困難です。

上記のような場合は、「Sealed Class」で型を表現することができます!

Sealed Classを使おう

Inline Value Classでは十分ではない場合、Sealed Classを使うことができます。Sealed Classにより、複数の型や状態を1つのシステム内で管理することが可能です。

Sealed Class自体は、Abstract Classです。しかし、Sealed Classを使用することで、すべてのサブクラスを同じコンテキスト内(ファイル内)で定義することが強制されます。言い換えれば、異なるコンテキストでSealed Classを継承することは不可能です。これにより、型の網羅性をコンパイル時に保証することができます。

とっても単純な例ですが、こういうことです:

// 単位を管理するSealed Class
sealed class Unit {
    object Meters : Unit() // サブクラス
    object Feet : Unit() // サブクラス
    // Unitクラスを継承できるのは、このファイル内だけ
}

fun describeUnit(unit: Unit): String {
    // UnitのサブクラスはMetersとFeetしかないことが分かっている
    // どちらかが欠けていれば、コンパイルエラーが発生
    return when (unit) {
        is Unit.Meters -> "This is meters!"
        is Unit.Feet -> "This is feet!" // この行を消すとコンパイルエラー
    }
}

では、関連する複数の型を管理するとき、どうやってSealed Classを活用できるでしょうか?

実装例:異なる単位をSealed Classで統一

あくまで一例ですが、以下のように複数の単位をSealed Classで定義し、それぞれに共通して「メートル」を保持することが可能です。

// 長さを管理するSealed Class
sealed class Length {
    abstract val meters: Double  // 共通プロパティとして、メートルに変換した値を保持

    class Meters(val value: Double) : Length() {
        override val meters: Double = value
    }

    class Feet(val value: Double) : Length() {
        override val meters: Double = value * 0.3048  // フィートからメートルへの変換
    }

    class Centimeters(val value: Double) : Length() {
        override val meters: Double = value / 100.0  // センチメートルからメートルへの変換
    }

    // プロパティを保持しないので、object(つまりSingleton)で良い
    object class Unknown() : Length() {
        override val meters: Double = 0
    }
}

使用例:

fun main() {
    // 異なる単位の長さを保存
    val lengths = listOf(
        Length.Meters(10.0),
        Length.Feet(0.8),
        Length.Centimeters(50.0),
        Length.Unknown()
    )

    // 全長さに共通する「メートル」プロパティにより、簡単にメートルで表現できる
    lengths.forEach { length ->
        println("Length in meters: ${length.meters} meters")
    }
}

このように、Sealed Classの共通プロパティを活用することで、異なる単位のデータを統一した形で管理できるのが大きな利点です。

使い分け方

では、Inline Value ClassとSealed Classはどのような場合に活用できるでしょうか?

Inline Value Classを使うべきなとき

  • 入力における型の取り違えを防ぎたいとき
    • 例えば、メートルとフィートの型を間違えて渡すと、コンパイルエラーになります。
  • パフォーマンスを最大限に重視したいとき
    • プリミティブ型に変換されるため、オブジェクトの生成を省略できます。

Sealed Classを使うべきとき

  • 複数のプロパティ管理が必要なとき
    • コード例でいう、metersとvalue。
    • 他には、サーバーレスポンスの型管理、状態遷移の管理(ロード中、成功、失敗)。
  • 異なる単位間の変換など、型ごとに特有のロジックを持たせたい場合
    • 言い換えれば、単なる入力の検証以上の操作をしたい場合
  • 拡張性が重要で、将来的に新しい単位や型を追加する可能性がある場合
    • when構文などで漏れがある場合にコンパイルエラーを出すため、将来的に型を拡張する際の安全性が高いと言えます。

終わりに

Kotlinの型安全機能が適切に活用されていれば、NASAの火星探査機のような事故は防げたかもしれません😃

・・・という宇宙のお話はさておき、一般的なソフトウェア開発の多くの場面においても、Inline Value ClassとSealed Classは、Kotlinの嬉しい機能だと思います。適切な型の選択によって、より良いソフトウェアデザインが可能になるはずです。

個人的な感想

執筆をして学びをアウトプットするのって難しい・・・。たくさん学んだ後でも、他者に分かりやすい形で書こうとすると、極めて浅いアウトプットになってしまいますね。4月からKotlinサーバーサイドエンジニアとして外資ITに新卒入社するのですが、入社後もKotlin関係の学びを執筆し、いつかはもっと有益な記事が書けるようになりたいです。そして、日本のKotlinコミュニティにとってなにか貢献できますように。。。!

Kotlin利用者と繋がりたいと考えているので、Zennのフォローやブログ[7]のRSS登録、Twitter(X)[8]のフォローをしていただければ幸いです。最後まで読んでくださり、ありがとうございました!

脚注
  1. https://ja.wikipedia.org/wiki/マーズ・クライメイト・オービター ↩︎

  2. Roman Elizarov, Svetlana Isakova, and Sebastian Aigner. Kotlin in Action, Second Edition. Manning Publications, 2024. https://learning.oreilly.com/library/view/kotlin-in-action/9781617299605/ ↩︎

  3. How to build a good API with Kotlin - Wolt Careers Blog. (2024, December 9). https://careers.wolt.com/en/blog/engineering/how-to-build-a-good-api-with-kotlin ↩︎

  4. Inline value classes. https://kotlinlang.org/docs/inline-classes.html ↩︎

  5. Sealed classes and interfaces. https://kotlinlang.org/docs/sealed-classes.html ↩︎

  6. Project Valhalla. https://openjdk.org/projects/valhalla/ ↩︎

  7. Kurumi writes. https://mtkrm.com/ja/ ↩︎

  8. X. https://x.com/mtkrm_com ↩︎

Discussion