中級: スコープ関数
この章では、拡張関数の理解を深め、スコープ関数を使用してよりイディオマティックなコードを書く方法を学びます。
スコープ関数
プログラミングにおいて、スコープとは変数またはオブジェクトが認識される範囲を指します。最も一般的に参照されるスコープは、グローバルスコープとローカルスコープです。
- グローバルスコープ – プログラム内のどこからでもアクセス可能な変数またはオブジェクト。
- ローカルスコープ – 定義されているブロックまたは関数内でのみアクセス可能な変数またはオブジェクト。
Kotlinには、オブジェクトの周囲に一時的なスコープを作成し、一部のコードを実行できるスコープ関数もあります。
スコープ関数を使用すると、一時的なスコープ内でオブジェクトの名前を参照する必要がないため、コードをより簡潔にできます。スコープ関数に応じて、this
キーワードを介してオブジェクトを参照するか、it
キーワードを介して引数として使用することでオブジェクトにアクセスできます。
Kotlinには、let
、apply
、run
、also
、with
の合計5つのスコープ関数があります。
各スコープ関数はラムダ式を受け取り、オブジェクトまたはラムダ式の結果のいずれかを返します。このツアーでは、各スコープ関数と、その使用方法を説明します。
Sebastian Aigner 氏(Kotlin開発者アドボケイト)によるスコープ関数に関するトーク、Back to the Stdlib: Making the Most of Kotlin's Standard Library もご覧いただけます。
let
let
スコープ関数は、コードで null チェックを実行し、後で返されたオブジェクトに対してさらにアクションを実行したい場合に使用します。
例を見てみましょう。
fun sendNotification(recipientAddress: String): String {
println("Yo $recipientAddress!")
return "Notification sent!"
}
fun getNextAddress(): String {
return "[email protected]"
}
fun main() {
val address: String? = getNextAddress()
sendNotification(address)
}
この例には2つの関数があります。
sendNotification()
: 関数パラメータrecipientAddress
を持ち、文字列を返します。getNextAddress()
: 関数パラメータを持たず、文字列を返します。
この例では、nullable な String
型の変数 address
を作成します。しかし、sendNotification()
関数を呼び出す際に問題が発生します。この関数は address
が null
値である可能性を想定していないためです。結果として、コンパイラはエラーを報告します。
Argument type mismatch: actual type is 'String?', but 'String' was expected.
入門ツアーから、if 条件を使って null チェックを実行したり、Elvis演算子 ?:
を使用できることをすでに知っています。しかし、返されたオブジェクトを後でコードで使用したい場合はどうでしょうか?これは、if 条件 と else ブランチを使用することで実現できます。
fun sendNotification(recipientAddress: String): String {
println("Yo $recipientAddress!")
return "Notification sent!"
}
fun getNextAddress(): String {
return "[email protected]"
}
fun main() {
val address: String? = getNextAddress()
val confirm = if(address != null) {
sendNotification(address)
} else { null }
}
しかし、より簡潔なアプローチは、let
スコープ関数を使用することです。
fun sendNotification(recipientAddress: String): String {
println("Yo $recipientAddress!")
return "Notification sent!"
}
fun getNextAddress(): String {
return "[email protected]"
}
fun main() {
val address: String? = getNextAddress()
val confirm = address?.let {
sendNotification(it)
}
}
この例では、次のことを行っています。
confirm
という変数を作成します。address
変数に対してlet
スコープ関数を安全呼び出しで使用します。let
スコープ関数内に一時的なスコープを作成します。sendNotification()
関数をラムダ式としてlet
スコープ関数に渡します。- 一時的なスコープを使用して、
it
を介してaddress
変数を参照します。 - 結果を
confirm
変数に代入します。
このアプローチにより、コードは address
変数が null
値である可能性を処理でき、後でコードで confirm
変数を使用できます。
apply
apply
スコープ関数は、クラスインスタンスのようなオブジェクトを、コードの後の方ではなく、作成時に初期化するために使用します。このアプローチにより、コードが読みやすく、管理しやすくなります。
例を見てみましょう。
class Client() {
var token: String? = null
fun connect() = println("connected!")
fun authenticate() = println("authenticated!")
fun getData(): String = "Mock data"
}
val client = Client()
fun main() {
client.token = "asdf"
client.connect()
// connected!
client.authenticate()
// authenticated!
client.getData()
}
この例には、token
という1つのプロパティと、connect()
、authenticate()
、getData()
の3つのメンバー関数を持つ Client
クラスがあります。
この例では、main()
関数で client
を Client
クラスのインスタンスとして作成し、その token
プロパティを初期化し、メンバー関数を呼び出しています。
この例は簡潔ですが、実際には、クラスインスタンスを作成してから設定して使用する(およびそのメンバー関数を呼び出す)までに時間がかかる場合があります。しかし、apply
スコープ関数を使用すると、クラスインスタンスの作成、設定、およびメンバー関数の使用をすべてコードの同じ場所で行うことができます。
class Client() {
var token: String? = null
fun connect() = println("connected!")
fun authenticate() = println("authenticated!")
fun getData(): String = "Mock data"
}
val client = Client().apply {
token = "asdf"
connect()
authenticate()
}
fun main() {
client.getData()
// connected!
// authenticated!
}
この例では、次のことを行っています。
client
をClient
クラスのインスタンスとして作成します。client
インスタンスに対してapply
スコープ関数を使用します。apply
スコープ関数内に一時的なスコープを作成し、そのプロパティや関数にアクセスする際にclient
インスタンスを明示的に参照する必要がないようにします。token
プロパティを更新し、connect()
およびauthenticate()
関数を呼び出すラムダ式をapply
スコープ関数に渡します。main()
関数でclient
インスタンスのgetData()
メンバー関数を呼び出します。
ご覧のとおり、この戦略は大規模なコードを扱う場合に便利です。
run
apply
と同様に、run
スコープ関数を使用してオブジェクトを初期化できますが、コードの特定の瞬間にオブジェクトを初期化 し、すぐに結果を計算したい場合は run
を使用する方が良いです。
apply
関数の前の例を続けますが、今回は connect()
関数と authenticate()
関数をグループ化し、すべてのリクエストで呼び出されるようにしたいとします。
例:
class Client() {
var token: String? = null
fun connect() = println("connected!")
fun authenticate() = println("authenticated!")
fun getData(): String = "Mock data"
}
val client: Client = Client().apply {
token = "asdf"
}
fun main() {
val result: String = client.run {
connect()
// connected!
authenticate()
// authenticated!
getData()
}
}
この例では、次のことを行っています。
client
をClient
クラスのインスタンスとして作成します。client
インスタンスに対してapply
スコープ関数を使用します。apply
スコープ関数内に一時的なスコープを作成し、そのプロパティや関数にアクセスする際にclient
インスタンスを明示的に参照する必要がないようにします。token
プロパティを更新するラムダ式をapply
スコープ関数に渡します。
main()
関数は、次のことを行います。
String
型のresult
変数を作成します。client
インスタンスに対してrun
スコープ関数を使用します。run
スコープ関数内に一時的なスコープを作成し、そのプロパティや関数にアクセスする際にclient
インスタンスを明示的に参照する必要がないようにします。connect()
、authenticate()
、およびgetData()
関数を呼び出すラムダ式をrun
スコープ関数に渡します。- 結果を
result
変数に代入します。
これで、返された結果をコード内でさらに使用できます。
also
also
スコープ関数は、オブジェクトに対して追加のアクションを完了し、そのオブジェクトを返してコード内で引き続き使用する(例えばログを書き込むなど)場合に使用します。
例を見てみましょう。
fun main() {
val medals: List<String> = listOf("Gold", "Silver", "Bronze")
val reversedLongUppercaseMedals: List<String> =
medals
.map { it.uppercase() }
.filter { it.length > 4 }
.reversed()
println(reversedLongUppercaseMedals)
// [BRONZE, SILVER]
}
この例では、次のことを行っています。
- 文字列のリストを含む
medals
変数を作成します。 List<String>
型のreversedLongUpperCaseMedals
変数を作成します。medals
変数に対して.map()
拡張関数を使用します。it
キーワードを介してmedals
を参照し、その上で.uppercase()
拡張関数を呼び出すラムダ式を.map()
関数に渡します。medals
変数に対して.filter()
拡張関数を使用します。it
キーワードを介してmedals
を参照し、medals
変数に含まれるリストの長さが4項目よりも長いかどうかをチェックするラムダ式を述語として.filter()
関数に渡します。medals
変数に対して.reversed()
拡張関数を使用します。- 結果を
reversedLongUpperCaseMedals
変数に代入します。 reversedLongUpperCaseMedals
変数に含まれるリストを出力します。
関数呼び出しの間にログを追加して、medals
変数に何が起きているかを確認できると便利です。also
関数はこれに役立ちます。
fun main() {
val medals: List<String> = listOf("Gold", "Silver", "Bronze")
val reversedLongUppercaseMedals: List<String> =
medals
.map { it.uppercase() }
.also { println(it) }
// [GOLD, SILVER, BRONZE]
.filter { it.length > 4 }
.also { println(it) }
// [SILVER, BRONZE]
.reversed()
println(reversedLongUppercaseMedals)
// [BRONZE, SILVER]
}
この例では、次のことを行っています。
medals
変数に対してalso
スコープ関数を使用します。also
スコープ関数内に一時的なスコープを作成し、medals
変数を関数パラメータとして使用する際に明示的に参照する必要がないようにします。it
キーワードを介してmedals
変数を関数パラメータとして使用してprintln()
関数を呼び出すラムダ式をalso
スコープ関数に渡します。
also
関数はオブジェクトを返すため、ログ記録だけでなく、デバッグ、複数の操作のチェイン、およびコードのメインフローに影響を与えないその他の副作用操作の実行にも役立ちます。
with
他のスコープ関数とは異なり、with
は拡張関数ではないため、構文が異なります。レシーバーオブジェクトを引数として with
に渡します。
オブジェクトに対して複数の関数を呼び出したい場合は、with
スコープ関数を使用します。
この例を見てみましょう。
class Canvas {
fun rect(x: Int, y: Int, w: Int, h: Int): Unit = println("$x, $y, $w, $h")
fun circ(x: Int, y: Int, rad: Int): Unit = println("$x, $y, $rad")
fun text(x: Int, y: Int, str: String): Unit = println("$x, $y, $str")
}
fun main() {
val mainMonitorPrimaryBufferBackedCanvas = Canvas()
mainMonitorPrimaryBufferBackedCanvas.text(10, 10, "Foo")
mainMonitorPrimaryBufferBackedCanvas.rect(20, 30, 100, 50)
mainMonitorPrimaryBufferBackedCanvas.circ(40, 60, 25)
mainMonitorPrimaryBufferBackedCanvas.text(15, 45, "Hello")
mainMonitorPrimaryBufferBackedCanvas.rect(70, 80, 150, 100)
mainMonitorPrimaryBufferBackedCanvas.circ(90, 110, 40)
mainMonitorPrimaryBufferBackedCanvas.text(35, 55, "World")
mainMonitorPrimaryBufferBackedCanvas.rect(120, 140, 200, 75)
mainMonitorPrimaryBufferBackedCanvas.circ(160, 180, 55)
mainMonitorPrimaryBufferBackedCanvas.text(50, 70, "Kotlin")
}
この例では、rect()
、circ()
、text()
の3つのメンバー関数を持つ Canvas
クラスを作成します。これらの各メンバー関数は、提供された関数パラメータから構築されたステートメントを出力します。
この例では、mainMonitorPrimaryBufferBackedCanvas
を Canvas
クラスのインスタンスとして作成し、そのインスタンスで異なる関数パラメータを持つ一連のメンバー関数を呼び出しています。
このコードは読みにくいことがわかります。with
関数を使用すると、コードは効率化されます。
class Canvas {
fun rect(x: Int, y: Int, w: Int, h: Int): Unit = println("$x, $y, $w, $h")
fun circ(x: Int, y: Int, rad: Int): Unit = println("$x, $y, $rad")
fun text(x: Int, y: Int, str: String): Unit = println("$x, $y, $str")
}
fun main() {
val mainMonitorSecondaryBufferBackedCanvas = Canvas()
with(mainMonitorSecondaryBufferBackedCanvas) {
text(10, 10, "Foo")
rect(20, 30, 100, 50)
circ(40, 60, 25)
text(15, 45, "Hello")
rect(70, 80, 150, 100)
circ(90, 110, 40)
text(35, 55, "World")
rect(120, 140, 200, 75)
circ(160, 180, 55)
text(50, 70, "Kotlin")
}
}
この例では、次のことを行っています。
mainMonitorSecondaryBufferBackedCanvas
インスタンスをレシーバーオブジェクトとしてwith
スコープ関数を使用します。with
スコープ関数内に一時的なスコープを作成し、そのメンバー関数を呼び出す際にmainMonitorSecondaryBufferBackedCanvas
インスタンスを明示的に参照する必要がないようにします。- 異なる関数パラメータを持つ一連のメンバー関数を呼び出すラムダ式を
with
スコープ関数に渡します。
このコードがはるかに読みやすくなったため、間違いを犯す可能性が低くなります。
ユースケースの概要
このセクションでは、Kotlinで利用できるさまざまなスコープ関数と、コードをよりイディオマティックにするための主要なユースケースについて説明しました。この表をクイックリファレンスとして使用できます。これらの関数がどのように機能するかを完全に理解していなくても、コードで使用できることに注意することが重要です。
関数 | x へのアクセス方法 | 戻り値 | ユースケース |
---|---|---|---|
let | it | ラムダの結果 | コードで null チェックを実行し、後で返されたオブジェクトに対してさらにアクションを実行する。 |
apply | this | x | オブジェクトを作成時に初期化する。 |
run | this | ラムダの結果 | オブジェクトを作成時に初期化 し、結果を計算する。 |
also | it | x | オブジェクトを返す前に追加のアクションを完了する。 |
with | this | ラムダの結果 | オブジェクトに対して複数の関数を呼び出す。 |
スコープ関数の詳細については、スコープ関数 を参照してください。
練習問題
演習 1
.getPriceInEuros()
関数を、安全呼び出し演算子 ?.
と let
スコープ関数を使用する単一式関数として書き直してください。
ヒント
?.
を使用して、getProductInfo()
関数から priceInDollars
プロパティに安全にアクセスします。その後、let
スコープ関数を使用して、priceInDollars
の値をユーロに変換します。 data class ProductInfo(val priceInDollars: Double?)
class Product {
fun getProductInfo(): ProductInfo? {
return ProductInfo(100.0)
}
}
// Rewrite this function
fun Product.getPriceInEuros(): Double? {
val info = getProductInfo()
if (info == null) return null
val price = info.priceInDollars
if (price == null) return null
return convertToEuros(price)
}
fun convertToEuros(dollars: Double): Double {
return dollars * 0.85
}
fun main() {
val product = Product()
val priceInEuros = product.getPriceInEuros()
if (priceInEuros != null) {
println("Price in Euros: €$priceInEuros")
// Price in Euros: €85.0
} else {
println("Price information is not available.")
}
}
解答例
data class ProductInfo(val priceInDollars: Double?)
class Product {
fun getProductInfo(): ProductInfo? {
return ProductInfo(100.0)
}
}
fun Product.getPriceInEuros() = getProductInfo()?.priceInDollars?.let { convertToEuros(it) }
fun convertToEuros(dollars: Double): Double {
return dollars * 0.85
}
fun main() {
val product = Product()
val priceInEuros = product.getPriceInEuros()
if (priceInEuros != null) {
println("Price in Euros: €$priceInEuros")
// Price in Euros: €85.0
} else {
println("Price information is not available.")
}
}
演習 2
ユーザーのメールアドレスを更新する updateEmail()
関数があります。apply
スコープ関数を使用してメールアドレスを更新し、次に also
スコープ関数を使用してログメッセージ Updating email for user with ID: ${it.id}
を出力してください。
data class User(val id: Int, var email: String)
fun updateEmail(user: User, newEmail: String): User = // Write your code here
fun main() {
val user = User(1, "[email protected]")
val updatedUser = updateEmail(user, "[email protected]")
// Updating email for user with ID: 1
println("Updated User: $updatedUser")
// Updated User: User(id=1, [email protected])
}
解答例
data class User(val id: Int, var email: String)
fun updateEmail(user: User, newEmail: String): User = user.apply {
this.email = newEmail
}.also { println("Updating email for user with ID: ${it.id}") }
fun main() {
val user = User(1, "[email protected]")
val updatedUser = updateEmail(user, "[email protected]")
// Updating email for user with ID: 1
println("Updated User: $updatedUser")
// Updated User: User(id=1, [email protected])
}