拡張
Kotlinの_拡張_は、継承や_Decorator_のようなデザインパターンを使用することなく、クラスやインターフェースに新しい機能を追加できます。これらは、直接変更できないサードパーティ製ライブラリを扱う際に役立ちます。一度作成すれば、これらの拡張は、あたかも元のクラスやインターフェースのメンバーであるかのように呼び出すことができます。
重要なこととして、拡張は拡張するクラスやインターフェースを変更しません。拡張を定義しても、新しいメンバーを追加するわけではありません。同じ構文を使って、新しい関数を呼び出したり、新しいプロパティにアクセスできるようにするだけです。
レシーバー
拡張は常にレシーバーで呼び出されます。レシーバーは、拡張されるクラスまたはインターフェースと同じ型を持つ必要があります。拡張を使用するには、レシーバーの後に .と関数またはプロパティ名を付けてプレフィックスとして付けます。
例えば、標準ライブラリのappendLine()拡張関数はStringBuilderクラスを拡張します。したがって、この場合、レシーバーはStringBuilderインスタンスであり、_レシーバー型_はStringBuilderです。
fun main() {
// builder is an instance of StringBuilder
val builder = StringBuilder()
// Calls .appendLine() extension function on builder
.appendLine("Hello")
.appendLine()
.appendLine("World")
println(builder.toString())
// Hello
//
// World
}拡張関数
独自の拡張関数を作成する前に、Kotlinの標準ライブラリに既に探しているものがあるかどうかを確認してください。 標準ライブラリは、以下のような多くの便利な拡張関数を提供します。
- コレクションの操作:
map()、filter()、reduce()、fold()、groupBy()。 - 文字列への変換:
joinToString()。 - null値の操作:
filterNotNull()。
独自の拡張関数を作成するには、その名前にレシーバー型と .をプレフィックスとして付けます。この例では、.truncate()関数はStringクラスを拡張するため、レシーバー型はStringです。
fun String.truncate(maxLength: Int): String {
return if (this.length <= maxLength) this else take(maxLength - 3) + "..."
}
fun main() {
val shortUsername = "KotlinFan42"
val longUsername = "JetBrainsLoverForever"
println("Short username: ${shortUsername.truncate(15)}")
// KotlinFan42
println("Long username: ${longUsername.truncate(15)}")
// JetBrainsLov...
}.truncate()関数は、呼び出された文字列を指定されたmaxLengthで切り詰め、省略記号...を追加します。文字列がmaxLengthより短い場合、関数は元の文字列を返します。
この例では、.displayInfo()関数はUserインターフェースを拡張します。
interface User {
val name: String
val email: String
}
fun User.displayInfo(): String = "User(name=$name, email=$email)"
// Inherits from and implements the properties of the User interface
class RegularUser(override val name: String, override val email: String) : User
fun main() {
val user = RegularUser("Alice", "[email protected]")
println(user.displayInfo())
// User(name=Alice, [email protected])
}.displayInfo()関数は、RegularUserインスタンスのnameとemailを含む文字列を返します。このようにインターフェースに拡張を定義することは、インターフェースを実装するすべての型に一度だけ機能を追加したい場合に便利です。
この例では、.mostVoted()関数はMap<String, Int>クラスを拡張します。
fun Map<String, Int>.mostVoted(): String? {
return maxByOrNull { (key, value) -> value }?.key
}
fun main() {
val poll = mapOf(
"Cats" to 37,
"Dogs" to 58,
"Birds" to 22
)
println("Top choice: ${poll.mostVoted()}")
// Dogs
}.mostVoted()関数は、呼び出されたマップのキーと値のペアを反復処理し、maxByOrNull()関数を使用して最大値を含むペアのキーを返します。マップが空の場合、maxByOrNull()関数はnullを返します。mostVoted()関数は、maxByOrNull()関数が非null値を返す場合にのみkeyプロパティにアクセスするために、セーフコール?.を使用します。
ジェネリックな拡張関数
ジェネリックな拡張関数を作成するには、関数名の前にジェネリック型パラメータを宣言して、レシーバー型式で利用できるようにします。この例では、.endpoints()関数はList<T>を拡張しており、Tは任意の型です。
fun <T> List<T>.endpoints(): Pair<T, T> {
return first() to last()
}
fun main() {
val cities = listOf("Paris", "London", "Berlin", "Prague")
val temperatures = listOf(21.0, 19.5, 22.3)
val cityEndpoints = cities.endpoints()
val tempEndpoints = temperatures.endpoints()
println("First and last cities: $cityEndpoints")
// (Paris, Prague)
println("First and last temperatures: $tempEndpoints")
// (21.0, 22.3)
}.endpoints()関数は、呼び出されたリストの最初と最後の要素を含むペアを返します。関数本体内では、first()関数とlast()関数を呼び出し、to中置関数を使用して返された値をPairに結合します。
ジェネリクスに関する詳細については、ジェネリック関数を参照してください。
Null許容レシーバー
null許容レシーバー型で拡張関数を定義できます。これにより、その値がnullであっても変数に対して呼び出すことができます。レシーバーがnullの場合、thisもnullになります。関数内でnull許容性を正しく処理するようにしてください。例えば、関数本体内でthis == nullチェック、セーフコール?.、またはエルビス演算子?:を使用します。
この例では、toString()関数をnullチェックなしで呼び出すことができます。なぜなら、そのチェックは既に拡張関数内で実行されているからです。
fun main() {
// Extension function on nullable Any
fun Any?.toString(): String {
if (this == null) return "null"
// After null check, `this` is smart-cast to non-nullable Any
// So this call resolves to the regular toString() function
return toString()
}
val number: Int? = 42
val nothing: Any? = null
println(number.toString())
// 42
println(nothing.toString())
// null
}拡張関数とメンバー関数
拡張関数とメンバー関数の呼び出しは同じ表記であるため、コンパイラはどちらを使用すべきかをどのように判断するのでしょうか? 拡張関数は_静的に_ディスパッチされます。つまり、コンパイラはコンパイル時にレシーバー型に基づいてどの関数を呼び出すかを決定します。例えば:
fun main() {
open class Shape
class Rectangle: Shape()
fun Shape.getName() = "Shape"
fun Rectangle.getName() = "Rectangle"
fun printClassName(shape: Shape) {
println(shape.getName())
}
printClassName(Rectangle())
// Shape
}この例では、パラメータshapeがShape型として宣言されているため、コンパイラはShape.getName()拡張関数を呼び出します。拡張関数は静的に解決されるため、コンパイラは実際のインスタンスではなく、宣言された型に基づいて関数を選択します。
したがって、例ではRectangleインスタンスを渡していますが、変数がShape型として宣言されているため、.getName()関数はShape.getName()に解決されます。
あるクラスがメンバー関数を持ち、かつ、同じレシーバー型、同じ名前で、互換性のある引数を持つ拡張関数が定義されている場合、メンバー関数が優先されます。例えば:
fun main() {
class Example {
fun printFunctionType() { println("Member function") }
}
fun Example.printFunctionType() { println("Extension function") }
Example().printFunctionType()
// Member function
}ただし、拡張関数は、同じ名前でも_異なる_シグネチャを持つメンバー関数をオーバーロードできます。
fun main() {
class Example {
fun printFunctionType() { println("Member function") }
}
// Same name but different signature
fun Example.printFunctionType(index: Int) { println("Extension function #$index") }
Example().printFunctionType(1)
// Extension function #1
}この例では、.printFunctionType()関数にIntが渡されるため、コンパイラはシグネチャに一致する拡張関数を選択します。コンパイラは引数を取らないメンバー関数を無視します。
匿名拡張関数
拡張関数に名前を付けずに定義できます。これは、グローバルな名前空間を汚染したくない場合や、拡張動作をパラメータとして渡す必要がある場合に役立ちます。
例えば、名前を付けずに、データクラスを1回限りの関数で拡張して送料を計算したいとします。
fun main() {
data class Order(val weight: Double)
val calculateShipping = fun Order.(rate: Double): Double = this.weight * rate
val order = Order(2.5)
val cost = order.calculateShipping(3.0)
println("Shipping cost: $cost")
// Shipping cost: 7.5
}拡張動作をパラメータとして渡すには、型アノテーション付きのラムダ式を使用します。例えば、名前付き関数を定義せずに、数値が範囲内にあるかどうかをチェックしたいとします。
fun main() {
val isInRange: Int.(min: Int, max: Int) -> Boolean = { min, max -> this in min..max }
println(5.isInRange(1, 10))
// true
println(20.isInRange(1, 10))
// false
}この例では、isInRange変数はInt.(min: Int, max: Int) -> Boolean型の関数を保持しています。この型は、minとmaxパラメータを取りBooleanを返すIntクラスの拡張関数です。
ラムダ本体{ min, max -> this in min..max }は、関数が呼び出されるInt値がminとmaxパラメータ間の範囲内にあるかどうかをチェックします。チェックが成功した場合、ラムダはtrueを返します。
詳細については、ラムダ式と匿名関数を参照してください。
拡張プロパティ
Kotlinは拡張プロパティをサポートしており、これは、作業しているクラスを汚染することなく、データ変換を実行したり、UI表示ヘルパーを作成したりするのに役立ちます。
拡張プロパティを作成するには、拡張したいクラスの名前の後に .とプロパティ名を記述します。
例えば、名と姓を持つユーザーを表すデータクラスがあり、アクセス時にメール形式のユーザー名を返すプロパティを作成したいとします。コードは次のようになります。
data class User(val firstName: String, val lastName: String)
// An extension property to get a username-style email handle
val User.emailUsername: String
get() = "${firstName.lowercase()}.${lastName.lowercase()}"
fun main() {
val user = User("Mickey", "Mouse")
// Calls extension property
println("Generated email username: ${user.emailUsername}")
// Generated email username: mickey.mouse
}拡張は実際にクラスにメンバーを追加しないため、拡張プロパティがバッキングフィールドを持つ効率的な方法はありません。そのため、拡張プロパティには初期化子は許可されていません。その動作は、明示的にゲッターとセッターを提供することによってのみ定義できます。例えば:
data class House(val streetName: String)
// Doesn't compile because there is no getter and setter
// var House.number = 1
// Error: Initializers are not allowed for extension properties
// Compiles successfully
val houseNumbers = mutableMapOf<House, Int>()
var House.number: Int
get() = houseNumbers[this] ?: 1
set(value) {
println("Setting house number for ${this.streetName} to $value")
houseNumbers[this] = value
}
fun main() {
val house = House("Maple Street")
// Shows the default
println("Default number: ${house.number} ${house.streetName}")
// Default number: 1 Maple Street
house.number = 99
// Setting house number for Maple Street to 99
// Shows the updated number
println("Updated number: ${house.number} ${house.streetName}")
// Updated number: 99 Maple Street
}この例では、ゲッターはエルビス演算子を使用して、houseNumbersマップに家の番号が存在する場合はその番号を返し、そうでない場合は1を返します。ゲッターとセッターの書き方について詳しく知るには、カスタムゲッターとセッターを参照してください。
コンパニオンオブジェクト拡張
クラスがコンパニオンオブジェクトを定義している場合、そのコンパニオンオブジェクトに対して拡張関数やプロパティを定義することもできます。コンパニオンオブジェクトの通常のメンバーと同様に、クラス名を修飾子として使用するだけで呼び出すことができます。コンパイラはデフォルトでコンパニオンオブジェクトをCompanionと名付けます。
class Logger {
companion object { }
}
fun Logger.Companion.logStartupMessage() {
println("Application started.")
}
fun main() {
Logger.logStartupMessage()
// Application started.
}メンバーとしての拡張の宣言
あるクラスの内部で、別のクラスの拡張を宣言することができます。このような拡張には複数の_暗黙のレシーバー_があります。暗黙のレシーバーとは、thisで修飾することなくメンバーにアクセスできるオブジェクトのことです。
- 拡張を宣言するクラスが_ディスパッチレシーバー_です。
- 拡張関数のレシーバー型が_拡張レシーバー_です。
ConnectionクラスにHostクラスの拡張関数printConnectionString()がある次の例を考えてみましょう。
class Host(val hostname: String) {
fun printHostname() { print(hostname) }
}
class Connection(val host: Host, val port: Int) {
fun printPort() { print(port) }
// Host is the extension receiver
fun Host.printConnectionString() {
// Calls Host.printHostname()
printHostname()
print(":")
// Calls Connection.printPort()
// Connection is the dispatch receiver
printPort()
}
fun connect() {
/*...*/
// Calls the extension function
host.printConnectionString()
}
}
fun main() {
Connection(Host("kotl.in"), 443).connect()
// kotl.in:443
// Triggers an error because the extension function isn't available outside Connection
// Host("kotl.in").printConnectionString()
// Unresolved reference 'printConnectionString'.
}この例では、printConnectionString()関数をConnectionクラスの内部で宣言しているため、Connectionクラスがディスパッチレシーバーです。拡張関数のレシーバー型はHostクラスであるため、Hostクラスが拡張レシーバーです。
ディスパッチレシーバーと拡張レシーバーに同じ名前のメンバーがある場合、拡張レシーバーのメンバーが優先されます。ディスパッチレシーバーに明示的にアクセスするには、修飾this構文を使用します。
class Connection {
fun Host.getConnectionString() {
// Calls Host.toString()
toString()
// Calls Connection.toString()
this@Connection.toString()
}
}メンバー拡張のオーバーライド
メンバー拡張をopenとして宣言し、サブクラスでオーバーライドできます。これは、各サブクラスで拡張の動作をカスタマイズしたい場合に役立ちます。コンパイラは各レシーバー型を異なる方法で処理します。
| レシーバー型 | 解決時間 | ディスパッチ型 |
|---|---|---|
| ディスパッチレシーバー | 実行時 | 仮想 |
| 拡張レシーバー | コンパイル時 | 静的 |
Userクラスがopenで、Adminクラスがそれを継承している次の例を考えてみましょう。NotificationSenderクラスはUserとAdminクラスの両方にsendNotification()拡張関数を定義し、SpecialNotificationSenderクラスはそれらをオーバーライドします。
open class User
class Admin : User()
open class NotificationSender {
open fun User.sendNotification() {
println("Sending user notification from normal sender")
}
open fun Admin.sendNotification() {
println("Sending admin notification from normal sender")
}
fun notify(user: User) {
user.sendNotification()
}
}
class SpecialNotificationSender : NotificationSender() {
override fun User.sendNotification() {
println("Sending user notification from special sender")
}
override fun Admin.sendNotification() {
println("Sending admin notification from special sender")
}
}
fun main() {
// Dispatch receiver is NotificationSender
// Extension receiver is User
// Resolves to User.sendNotification() in NotificationSender
NotificationSender().notify(User())
// Sending user notification from normal sender
// Dispatch receiver is SpecialNotificationSender
// Extension receiver is User
// Resolves to User.sendNotification() in SpecialNotificationSender
SpecialNotificationSender().notify(User())
// Sending user notification from special sender
// Dispatch receiver is SpecialNotificationSender
// Extension receiver is User NOT Admin
// The notify() function declares user as type User
// Statically resolves to User.sendNotification() in SpecialNotificationSender
SpecialNotificationSender().notify(Admin())
// Sending user notification from special sender
}ディスパッチレシーバーは仮想ディスパッチを使用して実行時に解決されるため、main()関数の動作を理解しやすくなります。驚くかもしれませんが、Adminインスタンスでnotify()関数を呼び出す際、コンパイラは宣言された型であるuser: Userに基づいて拡張を選択します。これは、拡張レシーバーを静的に解決するためです。
拡張と可視性修飾子
拡張は、他のクラスのメンバーとして宣言された拡張を含め、同じスコープで宣言された通常の関数と同じ可視性修飾子を使用します。
例えば、ファイルのトップレベルで宣言された拡張は、同じファイル内の他のprivateトップレベル宣言にアクセスできます。
// File: StringUtils.kt
private fun removeWhitespace(input: String): String {
return input.replace("\\s".toRegex(), "")
}
fun String.cleaned(): String {
return removeWhitespace(this)
}
fun main() {
val rawEmail = " user @example. com "
val cleaned = rawEmail.cleaned()
println("Raw: '$rawEmail'")
// Raw: ' user @example. com '
println("Cleaned: '$cleaned'")
// Cleaned: '[email protected]'
println("Looks like an email: ${cleaned.contains("@") && cleaned.contains(".")}")
// Looks like an email: true
}また、拡張がそのレシーバー型の外部で宣言されている場合、レシーバーのprivateまたはprotectedメンバーにアクセスできません。
class User(private val password: String) {
fun isLoggedIn(): Boolean = true
fun passwordLength(): Int = password.length
}
// Extension declared outside the class
fun User.isSecure(): Boolean {
// Can't access password because it's private:
// return password.length >= 8
// Instead, we rely on public members:
return passwordLength() >= 8 && isLoggedIn()
}
fun main() {
val user = User("supersecret")
println("Is user secure: ${user.isSecure()}")
// Is user secure: true
}拡張がinternalとマークされている場合、そのモジュール内でのみアクセス可能です。
// Networking module
// JsonParser.kt
internal fun String.parseJson(): Map<String, Any> {
return mapOf("fakeKey" to "fakeValue")
}拡張のスコープ
ほとんどの場合、拡張はパッケージ直下のトップレベルで定義します。
package org.example.declarations
fun List<String>.getLongestString() { /*...*/}宣言パッケージの外で拡張を使用するには、呼び出し側でインポートします。
package org.example.usage
import org.example.declarations.getLongestString
fun main() {
val list = listOf("red", "green", "blue")
list.getLongestString()
}詳細については、インポートを参照してください。
