エクステンション(拡張) : kotlin

※以下のコードは全て"Target platform: JVMRunning on kotlin v. 1.4.20"のもの。元ページと同じように表記させるのが面倒なのでここで表記するw

※いつも通りの原文無視がちの超訳が多くなった。なんか原文の表現がわかりにくいので適当に超訳したが、勘違いしてる部分も多々あると思う(ノ∀`)

※修飾子(qualifier)の言葉の使い方がちょっと理解出来ない。「オブジェクト.メソッド()」という時にオブジェクトをメソッド()の修飾子と呼ぶのだろうか? 修飾語句でもなんか変だし。加えて言うとmodifierとの違いは何だろうか?


Extensions

Kotlinはクラス継承やDecoratorなどのデザインパターンを使用することなく、新しい機能によってクラスを拡張する能力を提供します。これはエクステンション(拡張/拡張機能)と呼ばれる特別な宣言を介して行われます。

たとえば、内容を変更することができないサードパーティのライブラリのクラスに対して新しい関数を記述できます。このような関数は元のクラスのメソッドであるかのように通常の方法で呼び出すことができます。

このメカニズムは拡張関数と呼ばれます。既存のクラスに対して新しいプロパティを定義できる拡張プロパティもあります。

拡張関数

拡張関数を宣言するには、レシーバ(追加を受ける、つまり拡張したい対象)の型名の後ろに追加する関数名と処理を記述します。以下はMutableListにswap関数を追加する例です。: swap 交換する

fun MutableList<Int>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this'は追加先のMutableListを指します。
    this[index1] = this[index2]
    this[index2] = tmp
}

拡張関数内のthisキーワードは、レシーバオブジェクト(ドットの前に記述された型のオブジェクト)を指します。以下のように記述することで任意のMutableListで定義した関数を呼び出すことができます。:

val list = mutableListOf(1, 2, 3)
list.swap(0, 2) // swap()内の'this'は 'list'の値を保持します.

もちろん、この関数はどのMutableListにも意味があり、ジェネリクスにすることができます。:

fun <T> MutableList<T>.swap(index1: Int, index2: Int) {
    val tmp = this[index1] // 'this'はそのlistを指します。
    this[index1] = this[index2]
    this[index2] = tmp
}

レシーバ型の式で使用できるように、関数名の前にジェネリクスの型パラメーターを宣言します。詳細はジェネリクス関数を参照してください。
※この場合、レシーバ型名の前じゃないか? 関数名はあくまでもswapではなかろうか?


拡張機能は静的に解決されます

※なんか難しかったんで、俺氏超訳発動(`・ω・´)シェルダン

拡張機能は拡張するクラスを実際には変更しません。拡張機能を定義することによりクラスに新しいメンバ関数を挿入するのではなく、単に定義した関数をこの型の変数においてドット記法で呼び出しできるようにするだけです。
拡張機能は静的に割当てられます、つまり、レシーバ型の仮想の存在ではないということを強く認識してください。これは呼び出される拡張関数が、実行時にその式を評価した結果の型ではなく、関数を呼び出す式の型によって決定されることを意味します。例えば:

open class Shape
​
class Rectangle: Shape()
​
fun Shape.getName() = "Shape" //※Shapeへ拡張関数 getName()を追加
​
fun Rectangle.getName() = "Rectangle" //※Rectangleへ拡張関数 getName()を追加
​
fun printClassName(s: Shape) {
    println(s.getName())
}    
​
printClassName(Rectangle()) 

このコード例では"Shape"と出力されます。なぜなら呼び出される拡張関数は引数sの宣言型(Shapeクラス)にのみ依存するからです。

クラスにメンバ関数があり、同じレシーバ型と関数名を持ち、指定された引数に適用できる拡張関数が定義されている場合、メンバが常に勝ちます。例えば:

class Example {
    fun printFunctionType() { println("Class method") }
}
​
fun Example.printFunctionType() { println("Extension function") }
​
Example().printFunctionType()

このコードは"Class method"を出力します。
※"拡張関数と同じシグニチャ(関数名と引数)を持つメンバ関数がレシーバ型にある場合、常にメンバ関数が呼ばれます。"とか"レシーバ型内で拡張関数と既存のメンバ関数のシグニチャが競合する場合、常にメンバ関数が呼ばれます"という表現じゃ駄目なんだろうか?
Javaのバイトコードにした時に第一引数としてレシーバのオブジェクトを取るらしく、故にkotlin上では同じシグニチャに見えるけれども、実行時のJavaコードでは違うシグニチャになるから同一シグニチャという表現を避けたのかな?
#Kotlin の拡張関数の優先度についてメモ

ただし、拡張関数が同じ名前でシグニチャが異なるメンバ関数をオーバーロードしてもまったく問題ありません。:

class Example {
    fun printFunctionType() { println("Class method") }
}

fun Example.printFunctionType(i: Int) { println("Extension function") }

Example().printFunctionType(1)
​

※そもそもシグニチャは関数名も含むはずだから、この文章が何を言いたいのかよくわからない(´・ω・`) "同じ関数名でも引数の数や型が異なる、即ちシグニチャが異なるメンバ関数"という表現の方が妥当なような気がするけど、それでもここでわざわざ言っている意味がわからない。 メンバ関数と同じシグニチャの拡張関数を作成する、即ちオーバーライド的なことをするとメンバ関数が勝つということの対比としてオーバーロードに言及したかったのか?


Null許容レシーバ

拡張機能はnull許容のレシーバ型でも定義できることに注意してください。この場合の拡張関数は、値がnullであるオブジェクト変数であっても呼び出すことができ、本体内で"this == null"を判定できます。この仕組みにより、Kotlinではnull判定をせずにtoString()を呼び出すことができます。:チェックは拡張関数内で行われます。

fun Any?.toString(): String {
    if (this == null) return "null"
    // null判定後、'this'はスマートキャストにより非null型に変換されるので、
    // 以下のtoString()はAnyクラスのメンバ関数のtoString()になります。
    return toString()
}

拡張プロパティ

拡張関数と同様に、Kotlinは拡張プロパティも提供しています。

val <T> List<T>.lastIndex: Int
    get() = size - 1

拡張機能は実際にはメンバ(変数)をクラスに挿入しないため、拡張プロパティにバッキングフィールドを持たせる有効な方法がないことに注意してください。これが拡張プロパティに初期化子(イニシャライザ)が許可されていない理由です。拡張プロパティの動作はゲッター/セッターを明示的に提供することによってのみ定義できます。

例:

val House.number = 1 // エラー:初期化子は拡張プロパティには許可されていません。

※なんでここにエラー例だけしか載せてないんだろうか?


コンパニオンオブジェクト拡張

クラスにコンパニオンオブジェクトが定義されている場合、そのコンパニオンオブジェクトの拡張関数と拡張プロパティを定義することもできます。

コンパニオンオブジェクトの通常のメンバと同じ様に、修飾子としてクラス名のみを使用して呼び出すことができます。: ※クラス名とかって修飾子に分類されるの?

class MyClass {
    companion object { }  // 「コンパニオン」と呼ばれます。
}
​
fun MyClass.Companion.printCompanion() { println("companion") }
​
fun main() {
    MyClass.printCompanion()
}

拡張機能の範囲

ほとんどの場合、拡張機能はトップレベル(パッケージの直下)で定義します。:

package org.example.declarations
 
fun List<String>.getLongestString() { /*...*/}

定義を行ったパッケージの外部でその拡張機能を使用するには、呼び出し側でインポートする必要があります。:
call siteってあるけど、ピンポイントではなく、そのパッケージでインポートをするんだから呼び出し側(side)とかの方が適切ではなかろうか?

package org.example.usage
​
import org.example.declarations.getLongestString
​
fun main() {
    val list = listOf("red", "green", "blue")
    list.getLongestString()
}

詳細については、インポートを参照してください。


拡張関数をメンバとして宣言する

クラス内部で、別のクラスの拡張を宣言できます。このような拡張の内部には複数の暗黙的なレシーバがあります。(これらのオブジェクトのメンバには、修飾子なしでアクセスできます。) 拡張が宣言されているクラスのインスタンスはディスパッチレシーバと呼ばれ、拡張関数のレシーバ型のインスタンスは拡張レシーバと呼ばれます。
※ここのmultipleの意味がよくわからない。暗黙的なレシーバオブジェクトが存在するのは良いとしても。

class Host(val hostname: String) {
    fun printHostname() { print(hostname) }
}
​
class Connection(val host: Host, val port: Int) {
     fun printPort() { print(port) }
​
     fun Host.printConnectionString() {
         printHostname()   // Host.printHostname()を呼ぶ。
         print(":")
         printPort()   // Connection.printPort()を呼ぶ。
     }
​
     fun connect() {
         /*...*/
         host.printConnectionString()   // 拡張関数 printConnectionString()を呼ぶ。
     }
}
​
fun main() {
    Connection(Host("kotl.in"), 443).connect()   //"kotl.in:443"と出力される。
    //Host("kotl.in").printConnectionString(443)  //Connectionの外側で拡張関数 printConnectionString()は利用出来ないので、この呼出はエラーになる。
 
}

ディスパッチレシーバと拡張レシーバのメンバ間で名前が競合する場合は、拡張レシーバが優先されます。ディスパッチレシーバのメンバーを参照するには、this修飾構文を使用できます。

class Connection {
    fun Host.getConnectionString() {
        toString()         // Host.toString()を呼ぶ
        this@Connection.toString()  // Connection.toString()を呼ぶ
    }
}

メンバとして宣言される拡張関数は、openとして宣言でき、サブクラスでオーバーライドすることができます。つまり、このような関数の割当は、ディスパッチレシーバ型に対しては仮想ですが、拡張レシーバ型に対しては静的です。

※ここもopenとして宣言できると表現するよりも素直に"メンバ関数と同様にopenを用いることによってサブクラスでオーバーライドすることができます。"とかの方が良いような。

open class Base { }
​
class Derived : Base() { }
​
open class BaseCaller {
    open fun Base.printFunctionInfo() {
        println("Base extension function in BaseCaller")
    }
​
    open fun Derived.printFunctionInfo() {
        println("Derived extension function in BaseCaller")
    }
​
    fun call(b: Base) {
        b.printFunctionInfo()   // call the extension function
    }
}
​
class DerivedCaller: BaseCaller() {
    override fun Base.printFunctionInfo() {
        println("Base extension function in DerivedCaller")
    }
​
    override fun Derived.printFunctionInfo() {
        println("Derived extension function in DerivedCaller")
    }
}
​
fun main() {
    BaseCaller().call(Base())   // "Base extension function in BaseCaller"
    DerivedCaller().call(Base())  // "Base extension function in DerivedCaller" - dispatch receiver is resolved virtually
    DerivedCaller().call(Derived())  // "Base extension function in DerivedCaller" - extension receiver is resolved statically
}

可視性に関する注意

拡張機能は、同じスコープで宣言された通常の関数と同じように、他のエンティティの可視性を利用します。例えば:

-ファイルのトップレベルで宣言された拡張機能は、同じファイル内の他のprivateトップレベル宣言にアクセスできます。 :

-拡張機能がそのレシーバ型の外部で宣言されている場合、そのような拡張機能はレシーバのprivateメンバにアクセスできません。