中級:Null安全
初級編のツアーでは、コード内で null 値を扱う方法を学びました。この章では、Null安全機能の一般的なユースケースと、それらを最大限に活用する方法について説明します。
スマートキャストと安全なキャスト
Kotlinでは、明示的な宣言がなくても型を推論できる場合があります。変数やオブジェクトを特定の型に属しているかのように扱うようKotlinに指示するプロセスは、キャスト(casting)と呼ばれます。型が自動的にキャストされる場合(推論される場合など)、それはスマートキャスト(smart casting)と呼ばれます。
is および !is 演算子
キャストの仕組みを詳しく見る前に、オブジェクトが特定の型であるかどうかを確認する方法を見てみましょう。これには、when や if の条件式で is および !is 演算子を使用します。
isはオブジェクトがその型であるかを確認し、ブール値を返します。!isはオブジェクトがその型でないことを確認し、ブール値を返します。
例:
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)
// It's an Integer with value 42
// 型は List なので、Double ではない
printObjectType(myList)
// It's NOT a Double
// 型は Double なので、else ブランチが実行される
printObjectType(myDouble)
// Unknown type
}
when条件式でisおよび!is演算子を使用する例は、openクラスとその他の特殊なクラス の章ですでに確認しました。
as および as? 演算子
オブジェクトを他の型に明示的にキャストするには、as 演算子を使用します。これには、Null許容型からそれに対応する非Null型へのキャストも含まれます。キャストが不可能な場合、プログラムは実行時にクラッシュします。そのため、これは安全ではない(unsafe)キャスト演算子と呼ばれます。
fun main() {
val a: String? = null
val b = a as String
// 実行時にエラーが発生する
print(b)
}オブジェクトを非Null型に明示的にキャストしつつ、失敗した場合にエラーを投げるのではなく null を返したい場合は、as? 演算子を使用します。as? 演算子は失敗してもエラーを発生させないため、安全な(safe)演算子と呼ばれます。
fun main() {
val a: String? = null
val b = a as? String
// null 値を返す
print(b)
// null
}as? 演算子とエルビス演算子 ?: を組み合わせることで、数行のコードを1行にまとめることができます。たとえば、以下の calculateTotalStringLength() 関数は、混在したリストに含まれるすべての文字列の合計の長さを計算します。
fun calculateTotalStringLength(items: List<Any>): Int {
var totalLength = 0
for (item in items) {
totalLength += if (item is String) {
item.length
} else {
0 // 文字列以外のアイテムには 0 を加算
}
}
return totalLength
}この例では:
totalLength変数をカウンターとして使用しています。forループを使用してリスト内のすべてのアイテムをループしています。ifとis演算子を使用して、現在のアイテムが文字列かどうかを確認しています。- 文字列であれば、その長さがカウンターに加算されます。
- 文字列でなければ、カウンターはインクリメントされません。
totalLength変数の最終的な値を返します。
このコードは以下のように短縮できます。
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() 関数を使用します。
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() 関数を使用します。
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を返します。
例:
fun main() {
// 1週間に記録された気温
val temperatures = listOf(15, 18, 21, 21, 19, 17, 16)
// 週の最高気温を見つける
val maxTemperature = temperatures.maxOrNull()
println("Highest temperature recorded: ${maxTemperature ?: "No data"}")
// Highest temperature recorded: 21
// 週の最低気温を見つける
val minTemperature = temperatures.minOrNull()
println("Lowest temperature recorded: ${minTemperature ?: "No data"}")
// Lowest temperature recorded: 15
}この例では、エルビス演算子 ?: を使用して、関数が null 値を返した場合に出力する文字列を指定しています。
maxOrNull()およびminOrNull()関数は、null値を含まないコレクションで使用するように設計されています。そうでない場合、関数が目的の値を見つけられなかったのか、それともnull値を見つけたのかを区別できません。
singleOrNull() 関数をラムダ式と組み合わせて使用すると、条件に一致する単一のアイテムを検索できます。一致するものが存在しない、あるいは複数存在する場合、関数は null 値を返します。
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"}")
// Single hot day with 30 degrees: None
}
singleOrNull()関数は、null値を含まないコレクションで使用するように設計されています。
一部の関数は、ラムダ式を使用してコレクションを変換し、目的を果たせない場合には null 値を返します。
ラムダ式でコレクションを変換し、null ではない最初の値を返すには、firstNotNullOfOrNull() 関数を使用します。そのような値が存在しない場合、関数は null 値を返します。
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() 関数を使用します。
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"}")
// Total price of items in the cart: 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"}")
// Total price of items in the empty cart: No items
}この例でも、関数が null 値を返した場合に出力する文字列を指定するためにエルビス演算子 ?: を使用しています。
reduceOrNull()関数は、null値を含まないコレクションで使用するように設計されています。
コードをより安全にするために使用できるその他の関数については、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
}この例では:
id、name、および友人のリストのプロパティを持つUserデータクラスがあります。getNumberOfFriends()関数は:Userインスタンスのマップと、整数としてのユーザーIDを受け取ります。- 提供されたユーザーIDを使用して
Userインスタンスのマップの値にアクセスします。 - エルビス演算子を使用して、マップの値が
nullの場合に-1という値で関数を早期に終了させます。 - マップから見つかった値を
user変数に代入します。 sizeプロパティを使用して、ユーザーの友人リストの友人数を返します。
main()関数は:- 3つの
Userインスタンスを作成します。 - これらの
Userインスタンスのマップを作成し、users変数に代入します。 users変数に対して1と2の値でgetNumberOfFriends()関数を呼び出し、"Alice"には2人の友人が、"Bob"には1人の友人がいることを返します。users変数に対して4の値でgetNumberOfFriends()関数を呼び出し、値-1で早期リターンを発生させます。
- 3つの
早期リターンを使わなくても、コードをもっと簡潔にできることに気づくかもしれません。しかし、そのアプローチでは users[userId] が null 値を返す可能性があるため、複数の安全な呼び出しが必要になり、コードが少し読みにくくなります。
fun getNumberOfFriends(users: Map<Int, User>, userId: Int): Int {
// ユーザーを取得し、見つからない場合は -1 を返す
return users[userId]?.friends?.size ?: -1
}この例ではエルビス演算子で1つの条件のみをチェックしていますが、重要なエラーパスをカバーするために複数のチェックを追加することもできます。エルビス演算子を使用した早期リターンは、プログラムが不要な処理を行うのを防ぎ、null 値や無効なケースが検出されたらすぐに停止させることで、コードをより安全にします。
コードで return を使用する方法の詳細については、リターンとジャンプ を参照してください。
練習問題
練習問題 1
ユーザーがさまざまな種類の通知を有効または無効にできるアプリの通知システムを開発しています。以下の条件を満たすように getNotificationPreferences() 関数を完成させてください。
validUser変数でas?演算子を使用して、userがUserクラスのインスタンスであるかを確認する。そうでなければ空のリストを返す。userName変数でエルビス演算子?:を使用して、ユーザー名がnullの場合にデフォルトで"Guest"になるようにする。- 最終的な return 文で
.takeIf()関数を使用し、メールとSMSの通知設定が有効な場合にのみ含めるようにする。 main()関数が正常に実行され、期待通りの出力が表示されるようにする。
takeIf()関数 は、与えられた条件が真であれば元の値を返し、そうでなければnullを返します。例:kotlinfun main() { // ユーザーがログインしている val userIsLoggedIn = true // ユーザーにアクティブなセッションがある val hasSession = true // ユーザーがログインしており、かつアクティブなセッションがある場合に // ダッシュボードへのアクセスを許可する val canAccessDashboard = userIsLoggedIn.takeIf { hasSession } println(canAccessDashboard ?: "Access denied") // true }
|--|--|
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))
// []
}|--|--|
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つだけです。singleOrNull() 関数を述語(predicate)とともに使用して、アクティブなサブスクリプションが複数ある場合に null 値を返すように、getActiveSubscription() 関数を完成させてください。
|--|--|
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
}|--|--|
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
}|--|--|
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
あなたはソーシャルメディアプラットフォームに取り組んでおり、ユーザーにはユーザー名とアカウントステータスがあります。現在アクティブなユーザー名のリストを確認したいと考えています。mapNotNull() 関数 に、ユーザーがアクティブであればそのユーザー名を返し、そうでなければ null 値を返す述語(predicate)を指定して、getActiveUsernames() 関数を完成させてください。
|--|--|
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()関数 を使用することもできます。
|--|--|
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]
}|--|--|
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 で早期リターンする必要があります。
|--|--|
fun validateStock(requested: Int?, available: Int?): Int {
// ここにコードを書いてください
}
fun main() {
println(validateStock(5,10))
// 5
println(validateStock(null,10))
// -1
println(validateStock(-2,10))
// -1
}|--|--|
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
}