中級: Null Safety
拡張関数
スコープ関数
レシーバー付きラムダ式
クラスとインターフェース
オブジェクト
オープンクラスと特殊なクラス
プロパティ
Null Safety
ライブラリとAPI
初心者向けツアーでは、コードで null
値を処理する方法を学びました。この章では、Null Safety機能の一般的なユースケースと、それらを最大限に活用する方法について説明します。
スマートキャストとセーフキャスト
Kotlinは、明示的な宣言なしに型を推論できる場合があります。変数やオブジェクトを特定の型として扱うようKotlinに指示するこのプロセスは、キャストと呼ばれます。型が自動的にキャストされる場合、たとえば推論される場合などは、スマートキャストと呼ばれます。
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)
// 値 42 の Integer です
// 型は List なので、Double ではありません。
printObjectType(myList)
// Double ではありません
// 型は Double なので、else ブランチがトリガーされます。
printObjectType(myDouble)
// 不明な型
}
when
条件式をis
および!is
演算子とともに使用する例は、オープンクラスとその他の特殊なクラスの章ですでに確認しました。
as および as? 演算子
オブジェクトを任意の他の型に明示的に_キャスト_するには、as
演算子を使用します。これには、null許容型から非null許容型へのキャストが含まれます。キャストが不可能な場合、プログラムは実行時にクラッシュします。これが、この演算子が安全でないキャスト演算子と呼ばれる理由です。
fun main() {
val a: String? = null
val b = a as String
// 実行時にエラーが発生します
print(b)
}
オブジェクトを非null許容型に明示的にキャストするが、失敗時にエラーをスローする代わりに null
を返すには、as?
演算子を使用します。as?
演算子は失敗時にエラーをトリガーしないため、安全な演算子と呼ばれます。
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 // String 以外のアイテムには 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"}")
// 記録された最高気温: 21
// 週の最低気温を見つける
val minTemperature = temperatures.minOrNull()
println("Lowest temperature recorded: ${minTemperature ?: "No data"}")
// 記録された最低気温: 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"}")
// 30度だった暑い日は1日だけ: 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"}")
// カート内の商品の合計価格: 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の標準ライブラリを参照してください。
早期リターンとエルビス演算子
初心者向けツアーでは、関数が特定のポイントを超えて処理されるのを停止するための早期リターンの使用方法を学びました。エルビス演算子 ?:
を早期リターンと共に使用して、関数内の事前条件をチェックできます。このアプローチは、ネストされたチェックを使用する必要がないため、コードを簡潔に保つための優れた方法です。コードの複雑さが軽減されるため、保守も容易になります。例:
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()
関数は、指定された条件が true の場合は元の値を返し、そうでない場合は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つだけです。getActiveSubscription()
関数を完成させて、singleOrNull()
関数を述語と共に使用し、アクティブなサブスクリプションが複数ある場合に null
値を返すようにしてください。
|--|--|
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
ユーザー名とアカウントステータスを持つソーシャルメディアプラットフォームに取り組んでいます。現在アクティブなユーザー名のリストを表示したいと考えています。getActiveUsernames()
関数を完成させて、mapNotNull()
関数が、ユーザーがアクティブな場合はユーザー名を、そうでない場合は null
値を返す述語を持つようにしてください。
|--|--|
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
}