Skip to content

中級: Null Safety

初心者向けツアーでは、コードで null 値を処理する方法を学びました。この章では、Null Safety機能の一般的なユースケースと、それらを最大限に活用する方法について説明します。

スマートキャストとセーフキャスト

Kotlinは、明示的な宣言なしに型を推論できる場合があります。変数やオブジェクトを特定の型として扱うようKotlinに指示するこのプロセスは、キャストと呼ばれます。型が自動的にキャストされる場合、たとえば推論される場合などは、スマートキャストと呼ばれます。

is および !is 演算子

キャストがどのように機能するかを掘り下げる前に、オブジェクトが特定の型を持っているかどうかをチェックする方法を見てみましょう。これには、when または if 条件式で is および !is 演算子を使用できます。

  • is は、オブジェクトがその型を持つかどうかをチェックし、真偽値を返します。
  • !is は、オブジェクトがその型を持たないかどうかをチェックし、真偽値を返します。

例:

kotlin
fun printObjectType(obj: Any) {
    when (obj) {
        is Int -> println("It's an Integer with value $obj")
        !is Double -> println("It's NOT a Double")
        else -> println("Unknown type")
    }
}

fun main() {
    val myInt = 42
    val myDouble = 3.14
    val myList = listOf(1, 2, 3)
  
    // 型は Int
    printObjectType(myInt)
    // 値 42 の Integer です

    // 型は List なので、Double ではありません。
    printObjectType(myList)
    // Double ではありません

    // 型は Double なので、else ブランチがトリガーされます。
    printObjectType(myDouble)
    // 不明な型
}

when 条件式を is および !is 演算子とともに使用する例は、オープンクラスとその他の特殊なクラスの章ですでに確認しました。

as および as? 演算子

オブジェクトを任意の他の型に明示的に_キャスト_するには、as 演算子を使用します。これには、null許容型から非null許容型へのキャストが含まれます。キャストが不可能な場合、プログラムは実行時にクラッシュします。これが、この演算子が安全でないキャスト演算子と呼ばれる理由です。

kotlin
fun main() {
    val a: String? = null
    val b = a as String

    // 実行時にエラーが発生します
    print(b)
}

オブジェクトを非null許容型に明示的にキャストするが、失敗時にエラーをスローする代わりに null を返すには、as? 演算子を使用します。as? 演算子は失敗時にエラーをトリガーしないため、安全な演算子と呼ばれます。

kotlin
fun main() {
    val a: String? = null
    val b = a as? String

    // null 値を返します
    print(b)
    // null
}

as? 演算子をエルビス演算子 ?: と組み合わせることで、数行のコードを1行に減らすことができます。たとえば、次の calculateTotalStringLength() 関数は、混合リストで提供されるすべての文字列の合計長を計算します。

kotlin
fun calculateTotalStringLength(items: List<Any>): Int {
    var totalLength = 0

    for (item in items) {
        totalLength += if (item is String) {
            item.length
        } else {
            0  // String 以外のアイテムには 0 を加算
        }
    }

    return totalLength
}

この例は次のことを行います。

  • totalLength 変数をカウンターとして使用します。
  • for ループを使用してリスト内の各アイテムをループします。
  • ifis 演算子を使用して、現在のアイテムが文字列であるかどうかをチェックします。
    • 文字列である場合、文字列の長さがカウンターに追加されます。
    • 文字列でない場合、カウンターはインクリメントされません。
  • totalLength 変数の最終的な値を返します。

このコードは、次のように短縮できます。

kotlin
fun calculateTotalStringLength(items: List<Any>): Int {
    return items.sumOf { (it as? String)?.length ?: 0 }
}

この例では、sumOf() 拡張関数を使用し、次のラムダ式を提供します。

  • リスト内の各アイテムに対して、as? を使用して String へのセーフキャストを実行します。
  • 呼び出しが null 値を返さない場合、セーフコール ?. を使用して length プロパティにアクセスします。
  • セーフコールが null 値を返す場合、エルビス演算子 ?: を使用して 0 を返します。

Null値とコレクション

Kotlinでは、コレクションを扱う際に null 値の処理や不要な要素のフィルタリングが頻繁に発生します。Kotlinには、リスト、セット、マップ、およびその他の種類のコレクションを扱う際に、クリーンで効率的、かつNull安全なコードを書くために使用できる便利な関数が用意されています。

リストから null 値をフィルタリングするには、filterNotNull() 関数を使用します。

kotlin
fun main() {
    val emails: List<String?> = listOf("[email protected]", null, "[email protected]", null, "[email protected]")

    val validEmails = emails.filterNotNull()

    println(validEmails)
    // [[email protected], [email protected], [email protected]]
}

リストを作成する際に null 値のフィルタリングを直接実行したい場合は、listOfNotNull() 関数を使用します。

kotlin
fun main() {
    val serverConfig = mapOf(
        "appConfig.json" to "App Configuration",
        "dbConfig.json" to "Database Configuration"
    )

    val requestedFile = "appConfig.json"
    val configFiles = listOfNotNull(serverConfig[requestedFile])

    println(configFiles)
    // [App Configuration]
}

これらの両方の例では、すべてのアイテムが null 値の場合、空のリストが返されます。

Kotlinには、コレクション内の値を見つけるために使用できる関数も用意されています。値が見つからない場合、エラーをトリガーする代わりに null 値を返します。

  • maxOrNull() は最大値を見つけます。存在しない場合は null 値を返します。
  • minOrNull() は最小値を見つけます。存在しない場合は null 値を返します。

例:

kotlin
fun main() {
    // 1週間に記録された気温
    val temperatures = listOf(15, 18, 21, 21, 19, 17, 16)
  
    // 週の最高気温を見つける
    val maxTemperature = temperatures.maxOrNull()
    println("Highest temperature recorded: ${maxTemperature ?: "No data"}")
    // 記録された最高気温: 21

    // 週の最低気温を見つける
    val minTemperature = temperatures.minOrNull()
    println("Lowest temperature recorded: ${minTemperature ?: "No data"}")
    // 記録された最低気温: 15
}

この例では、関数が null 値を返す場合に、エルビス演算子 ?: を使用して出力文を返します。

maxOrNull() および minOrNull() 関数は、null 値を含まないコレクションで使用するように設計されています。そうしないと、関数が目的の値を見つけられなかったのか、それとも null 値が見つかったのかを判断できません。

singleOrNull() 関数をラムダ式と共に使用して、条件に一致する単一のアイテムを見つけることができます。存在しない場合、または一致するアイテムが複数ある場合、関数は null 値を返します。

kotlin
fun main() {
    // 1週間に記録された気温
    val temperatures = listOf(15, 18, 21, 21, 19, 17, 16)

    // 30度の日は正確に1日だけだったかを確認
    val singleHotDay = temperatures.singleOrNull{ it == 30 }
    println("Single hot day with 30 degrees: ${singleHotDay ?: "None"}")
    // 30度だった暑い日は1日だけ: None
}

singleOrNull() 関数は、null 値を含まないコレクションで使用するように設計されています。

一部の関数は、ラムダ式を使用してコレクションを変換し、目的を達成できない場合に null 値を返します。

ラムダ式を使用してコレクションを変換し、null でない最初の値を返すには、firstNotNullOfOrNull() 関数を使用します。そのような値が存在しない場合、関数は null 値を返します。

kotlin
fun main() {
    data class User(val name: String?, val age: Int?)

    val users = listOf(
        User(null, 25),
        User("Alice", null),
        User("Bob", 30)
    )

    val firstNonNullName = users.firstNotNullOfOrNull { it.name }
    println(firstNonNullName)
    // Alice
}

各コレクションアイテムを順番に処理して累積値を作成する(またはコレクションが空の場合に null 値を返す)ラムダ式を使用するには、reduceOrNull() 関数を使用します。

kotlin
fun main() {
    // ショッピングカート内の商品の価格
    val itemPrices = listOf(20, 35, 15, 40, 10)

    // reduceOrNull() 関数を使用して合計価格を計算
    val totalPrice = itemPrices.reduceOrNull { runningTotal, price -> runningTotal + price }
    println("Total price of items in the cart: ${totalPrice ?: "No items"}")
    // カート内の商品の合計価格: 120

    val emptyCart = listOf<Int>()
    val emptyTotalPrice = emptyCart.reduceOrNull { runningTotal, price -> runningTotal + price }
    println("Total price of items in the empty cart: ${emptyTotalPrice ?: "No items"}")
    // 空のカート内の商品の合計価格: No items
}

この例でも、関数が null 値を返す場合に、エルビス演算子 ?: を使用して出力文を返します。

reduceOrNull() 関数は、null 値を含まないコレクションで使用するように設計されています。

コードをより安全にするために使用できるその他の関数については、Kotlinの標準ライブラリを参照してください。

早期リターンとエルビス演算子

初心者向けツアーでは、関数が特定のポイントを超えて処理されるのを停止するための早期リターンの使用方法を学びました。エルビス演算子 ?: を早期リターンと共に使用して、関数内の事前条件をチェックできます。このアプローチは、ネストされたチェックを使用する必要がないため、コードを簡潔に保つための優れた方法です。コードの複雑さが軽減されるため、保守も容易になります。例:

kotlin
data class User(
    val id: Int,
    val name: String,
    // 友達のユーザーIDのリスト
    val friends: List<Int>
)

// ユーザーの友達の数を取得する関数
fun getNumberOfFriends(users: Map<Int, User>, userId: Int): Int {
    // ユーザーを取得するか、見つからない場合は -1 を返します
    val user = users[userId] ?: return -1
    // 友達の数を返します
    return user.friends.size
}

fun main() {
    // いくつかのサンプルユーザーを作成
    val user1 = User(1, "Alice", listOf(2, 3))
    val user2 = User(2, "Bob", listOf(1))
    val user3 = User(3, "Charlie", listOf(1))

    // ユーザーのマップを作成
    val users = mapOf(1 to user1, 2 to user2, 3 to user3)

    println(getNumberOfFriends(users, 1))
    // 2
    println(getNumberOfFriends(users, 2))
    // 1
    println(getNumberOfFriends(users, 4))
    // -1
}

この例では、次のことを行います。

  • ユーザーの idname、および友達のリストのプロパティを持つ User データクラスがあります。
  • getNumberOfFriends() 関数:
    • User インスタンスのマップと整数としてのユーザーIDを受け取ります。
    • 提供されたユーザーIDを使用して User インスタンスのマップの値にアクセスします。
    • マップの値が null の場合に、関数を早期に -1 の値で返すためにエルビス演算子を使用します。
    • マップから見つかった値を user 変数に割り当てます。
    • size プロパティを使用して、ユーザーの友達リストの友達数を返します。
  • main() 関数:
    • 3つの User インスタンスを作成します。
    • これらの User インスタンスのマップを作成し、users 変数に割り当てます。
    • users 変数に対して値 12getNumberOfFriends() 関数を呼び出し、"Alice" には2人の友達を、"Bob" には1人の友達を返します。
    • users 変数に対して値 4getNumberOfFriends() 関数を呼び出し、値 -1 で早期リターンをトリガーします。

このコードは、早期リターンなしでより簡潔にできることに気づくかもしれません。しかし、このアプローチでは users[userId]null 値を返す可能性があるため、複数のセーフコールが必要となり、コードが少し読みにくくなります。

kotlin
fun getNumberOfFriends(users: Map<Int, User>, userId: Int): Int {
    // ユーザーを取得するか、見つからない場合は -1 を返します
    return users[userId]?.friends?.size ?: -1
}

この例ではエルビス演算子で1つの条件のみをチェックしていますが、複数のチェックを追加して、重要なエラーパスをカバーできます。早期リターンとエルビス演算子を使用すると、プログラムが不要な作業を行うのを防ぎ、null 値または無効なケースが検出され次第停止することで、コードをより安全にします。

コードで return を使用する方法の詳細については、戻りとジャンプを参照してください。

練習

演習 1

ユーザーがさまざまな種類の通知を有効または無効にできるアプリの通知システムを開発しています。getNotificationPreferences() 関数を完成させてください。

  1. validUser 変数は as? 演算子を使用して、userUser クラスのインスタンスであるかどうかをチェックします。そうでない場合は、空のリストを返します。
  2. userName 変数はエルビス ?: 演算子を使用して、ユーザー名が null の場合にデフォルトで "Guest" になるようにします。
  3. 最後の return 文は .takeIf() 関数を使用して、メールとSMSの通知設定が有効になっている場合にのみ含めます。
  4. main() 関数が正常に実行され、期待される出力が表示されます。

takeIf() 関数は、指定された条件が true の場合は元の値を返し、そうでない場合は null を返します。例:

kotlin
fun main() {
    // ユーザーがログインしています
    val userIsLoggedIn = true
    // ユーザーにはアクティブなセッションがあります
    val hasSession = true

    // ユーザーがログインしており
    // アクティブなセッションを持っている場合にダッシュボードへのアクセスを許可します
    val canAccessDashboard = userIsLoggedIn.takeIf { hasSession }

    println(canAccessDashboard ?: "Access denied")
    // true
}

|--|--|

kotlin
data class User(val name: String?)

fun getNotificationPreferences(user: Any, emailEnabled: Boolean, smsEnabled: Boolean): List<String> {
    val validUser = // ここにコードを記述
    val userName = // ここにコードを記述

    return listOfNotNull( /* ここにコードを記述 */)
}

fun main() {
    val user1 = User("Alice")
    val user2 = User(null)
    val invalidUser = "NotAUser"

    println(getNotificationPreferences(user1, emailEnabled = true, smsEnabled = false))
    // [Email Notifications enabled for Alice]
    println(getNotificationPreferences(user2, emailEnabled = false, smsEnabled = true))
    // [SMS Notifications enabled for Guest]
    println(getNotificationPreferences(invalidUser, emailEnabled = true, smsEnabled = true))
    // []
}

|--|--|

kotlin
data class User(val name: String?)

fun getNotificationPreferences(user: Any, emailEnabled: Boolean, smsEnabled: Boolean): List<String> {
    val validUser = user as? User ?: return emptyList()
    val userName = validUser.name ?: "Guest"

    return listOfNotNull(
        "Email Notifications enabled for $userName".takeIf { emailEnabled },
        "SMS Notifications enabled for $userName".takeIf { smsEnabled }
    )
}

fun main() {
    val user1 = User("Alice")
    val user2 = User(null)
    val invalidUser = "NotAUser"

    println(getNotificationPreferences(user1, emailEnabled = true, smsEnabled = false))
    // [Email Notifications enabled for Alice]
    println(getNotificationPreferences(user2, emailEnabled = false, smsEnabled = true))
    // [SMS Notifications enabled for Guest]
    println(getNotificationPreferences(invalidUser, emailEnabled = true, smsEnabled = true))
    // []
}

演習 2

ユーザーが複数のサブスクリプションを持つことができるサブスクリプションベースのストリーミングサービスに取り組んでいますが、一度にアクティブにできるのは1つだけです。getActiveSubscription() 関数を完成させて、singleOrNull() 関数を述語と共に使用し、アクティブなサブスクリプションが複数ある場合に null 値を返すようにしてください。

|--|--|

kotlin
data class Subscription(val name: String, val isActive: Boolean)

fun getActiveSubscription(subscriptions: List<Subscription>): Subscription? // ここにコードを記述

fun main() {
    val userWithPremiumPlan = listOf(
        Subscription("Basic Plan", false),
        Subscription("Premium Plan", true)
    )

    val userWithConflictingPlans = listOf(
        Subscription("Basic Plan", true),
        Subscription("Premium Plan", true)
    )

    println(getActiveSubscription(userWithPremiumPlan))
    // Subscription(name=Premium Plan, isActive=true)

    println(getActiveSubscription(userWithConflictingPlans))
    // null
}

|--|--|

kotlin
data class Subscription(val name: String, val isActive: Boolean)

fun getActiveSubscription(subscriptions: List<Subscription>): Subscription? {
    return subscriptions.singleOrNull { subscription -> subscription.isActive }
}

fun main() {
    val userWithPremiumPlan = listOf(
        Subscription("Basic Plan", false),
        Subscription("Premium Plan", true)
    )

    val userWithConflictingPlans = listOf(
        Subscription("Basic Plan", true),
        Subscription("Premium Plan", true)
    )

    println(getActiveSubscription(userWithPremiumPlan))
    // Subscription(name=Premium Plan, isActive=true)

    println(getActiveSubscription(userWithConflictingPlans))
    // null
}

|--|--|

kotlin
data class Subscription(val name: String, val isActive: Boolean)

fun getActiveSubscription(subscriptions: List<Subscription>): Subscription? =
    subscriptions.singleOrNull { it.isActive }

fun main() {
    val userWithPremiumPlan = listOf(
        Subscription("Basic Plan", false),
        Subscription("Premium Plan", true)
    )

    val userWithConflictingPlans = listOf(
        Subscription("Basic Plan", true),
        Subscription("Premium Plan", true)
    )

    println(getActiveSubscription(userWithPremiumPlan))
    // Subscription(name=Premium Plan, isActive=true)

    println(getActiveSubscription(userWithConflictingPlans))
    // null
}

演習 3

ユーザー名とアカウントステータスを持つソーシャルメディアプラットフォームに取り組んでいます。現在アクティブなユーザー名のリストを表示したいと考えています。getActiveUsernames() 関数を完成させて、mapNotNull() 関数が、ユーザーがアクティブな場合はユーザー名を、そうでない場合は null 値を返す述語を持つようにしてください。

|--|--|

kotlin
data class User(val username: String, val isActive: Boolean)

fun getActiveUsernames(users: List<User>): List<String> {
    return users.mapNotNull { /* ここにコードを記述 */ }
}

fun main() {
    val allUsers = listOf(
        User("alice123", true),
        User("bob_the_builder", false),
        User("charlie99", true)
    )

    println(getActiveUsernames(allUsers))
    // [alice123, charlie99]
}

|--|--|

演習1と同様に、ユーザーがアクティブであるかをチェックする際に takeIf() 関数を使用できます。

|--|--|

kotlin
data class User(val username: String, val isActive: Boolean)

fun getActiveUsernames(users: List<User>): List<String> {
    return users.mapNotNull { user ->
        if (user.isActive) user.username else null
    }
}

fun main() {
    val allUsers = listOf(
        User("alice123", true),
        User("bob_the_builder", false),
        User("charlie99", true)
    )

    println(getActiveUsernames(allUsers))
    // [alice123, charlie99]
}

|--|--|

kotlin
data class User(val username: String, val isActive: Boolean)

fun getActiveUsernames(users: List<User>): List<String> =
    users.mapNotNull { user -> user.username.takeIf { user.isActive } }

fun main() {
    val allUsers = listOf(
        User("alice123", true),
        User("bob_the_builder", false),
        User("charlie99", true)
    )

    println(getActiveUsernames(allUsers))
    // [alice123, charlie99]
}

演習 4

eコマースプラットフォームの在庫管理システムに取り組んでいます。販売を処理する前に、要求された製品の数量が、利用可能な在庫に基づいて有効であるかをチェックする必要があります。

validateStock() 関数を完成させて、早期リターンと(該当する場合は)エルビス演算子を使用して、次のことをチェックするようにしてください。

  • requested 変数が null である。
  • available 変数が null である。
  • requested 変数が負の値である。
  • requested 変数の量が available 変数の量よりも大きい。

上記のすべての場合において、関数は値 -1 で早期リターンする必要があります。

|--|--|

kotlin
fun validateStock(requested: Int?, available: Int?): Int {
    // ここにコードを記述
}

fun main() {
    println(validateStock(5,10))
    // 5
    println(validateStock(null,10))
    // -1
    println(validateStock(-2,10))
    // -1
}

|--|--|

kotlin
fun validateStock(requested: Int?, available: Int?): Int {
    val validRequested = requested ?: return -1
    val validAvailable = available ?: return -1

    if (validRequested < 0) return -1
    if (validRequested > validAvailable) return -1

    return validRequested
}

fun main() {
    println(validateStock(5,10))
    // 5
    println(validateStock(null,10))
    // -1
    println(validateStock(-2,10))
    // -1
}

次のステップ

中級: ライブラリとAPI