1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Kotlinのレシーバと関数呼び出しの優先順位

Posted at

はじめに

Kotlinには関数、メンバ関数1、拡張関数2、そしてクラス定義内で定義された拡張関数3のように多くの種類の関数があります。そのため、あるスコープで

A().foo()
foo()

というような記述があったとき、「何を呼び出す可能性があるのか」をはっきり理解したいと思い、少し整理をしてみました。

扱っていないこと

  • ローカル関数、コンパニオンオブジェクトのスコープ、オブジェクト宣言との関連
  • この優先順位を踏まえて、どんな使い方が考えられるか

確認した環境

環境 バージョン
Kotlin 1.7.20
JRE 17.0.8

1. 関数と拡張関数のみの場合

まずは基本的な場合から始めてみたいと思います。

ソース1: 関数と拡張関数のみ
class A {
    fun foo() {
        println("A#foo()")
    }
}

fun A.foo() {
    println("A.foo()")
}

fun main() {
    A().foo()       // A#foo()が優先、その次はA.foo()
}

何が表示されるでしょうか?

これをコンパイルしてみると

warning: extension is shadowed by a member: public final fun foo(): Unit

と拡張関数A.foo()がメンバ関数のA#foo()に隠されるという警告が出て、実行すると

A#foo()

と出力され、メンバ関数が優先的に呼び出されていることが分かります。

確認のためにメンバ関数の定義を削除してみると

ソース2: 拡張関数のみ
class A {
-    fun foo() {
-       println("A#foo()")
-    }
}

fun A.foo() {
    println("A.foo()")
}

fun main() {
    A().foo()       // A#foo()が優先、その次はA.foo()
}

実行結果は

A.foo()

となります。
つまり両方の定義がある場合、レシーバでの呼び出しは拡張関数よりもメンバ関数が優先されることが分かります。

A().foo()のA()や、x.foo()のxが表すオブジェクトをレシーバといいます

2. with内でレシーバなしの呼び出し

では、ソース1に関数foo()を追加して、with内でfoo()と呼び出すとどうなるのでしょうか?

ソース3: with内でレシーバなしの呼び出し
class A {
    fun foo() {
        println("A#foo()")
    }
}

fun A.foo() {
    println("A.foo()")
}

+ fun foo() {
+     println("foo()")
+ }

fun main() {
-    A().foo()       // A#foo()が優先、その次はA.foo()

    with (A()) {
        // foo()とするとA#foo()が優先、次にA.foo()、その次がfoo()
        foo()
    }
}

再び優先順位の高いA#foo()から順に削除しつつ実行してみると、
追加されたfoo()の優先順位が最も低くなっていることが確認できます。

(以下同様に、呼び出された関数の定義から順に削除して、呼び出しの優先順位を調べています)

3. メンバに拡張関数が定義されている場合

ここで他のクラスを登場させて、fooという名前のメンバを追加したら、どうなるのでしょうか?

ソース4: メンバに拡張関数1
class A {
    fun foo() {
        println("A#foo()")
    }
}

fun A.foo() {
    println("A.foo()")
}

fun foo() {
    println("foo()")
}

+ class B {
+    fun foo() {
+        println("B#foo()")
+    }

+    fun A.foo() {
+        println("B#A.foo()")
+    }
+ }

fun main() {
-    with (A()) {
-        // foo()とするとA#foo()が優先、次にA.foo()、その次がfoo()
-        foo()
-    }

+    B().foo()
}

とすると

B#foo()

となり、B()のメンバ関数が呼び出せるのは基本ですが、、、

ここで疑問が生じます。Bのメンバの拡張関数B#A.foo()はどのようにして呼び出したらいいのでしょうか?

ソース5: メンバに拡張関数2
class A {
-    fun foo() {
-        println("A#foo()")
-    }
}

fun A.foo() {
    println("A.foo()")
}

fun foo() {
    println("foo()")
}

class B {
    fun foo() {
        println("B#foo()")
    }

    fun A.foo() {
        println("B#A.foo()")
    }
}

fun main() {
-    B().foo()
    
+    // B#A.foo()の呼び出し方
+    with (B()) {
+        A().foo()       // A#foo()が最優先、次にB#A.foo()、その次がA.foo()
+        // foo()           // B#foo()が最優先、その次はあればB.foo()、最後がfoo()
+    }
}

このようにB#A.foo()が呼び出す記述は、少し特殊なものとなります。
Aのメンバ関数であるA#foo()が定義されているならそれが呼び出され、定義されていないなら上記のような記述で呼び出しが可能になります。

呼び出しの優先順位をまとめてみると、、、

  1. ここでもやはり最優先はメンバ関数を探す(A#foo()があれば呼ばれる)ということです。
  2. それがなければクラスBのスコープ内で拡張関数を探し(結果B#A.foo()に解決する)、
  3. それもなければ、その外側のスコープで拡張関数を探すということになります。

4. メンバの拡張関数を同じクラス内から呼び出す

ソース5ではmain()関数からクラスBのメンバの拡張関数を呼び出していましたが、同じクラスBから呼び出すにはどうしたらよいのでしょうか?

ソース6: 同じクラス内から呼び出す
class A {
}

fun A.foo() {
    println("A.foo()")
}

fun foo() {
    println("foo()")
}

class B {
    fun foo() {
        println("B#foo()")
    }

    fun A.foo() {
        println("B#A.foo()")
    }

+    fun call_foo() {  // この関数内でB#A.foo()を呼び出すには?
+        print("call_foo() >>> ")
+        // A().foo()とするとA#foo()が最優先、次にB#A.foo()、最後がA.foo()
+        A().foo()
+
+        // ちなみに
+        // this.foo()とするとB#foo()が呼ばれる
+        // this@Bfoo()とするとB#foo()が呼ばれる
+        // foo()とするとB#foo()が最優先、次にfoo()
+    }
}

fun main() {
-    // B#A.foo()の呼び出し方
-    with (B()) {
-        A().foo()       // A#foo()が最優先、次にB#A.foo()、その次がA.foo()
-        // foo()           // B#foo()が最優先、その次はあればB.foo()、最後がfoo()
-    }

    B().call_foo()
}

ここでもソース5で確認したことと同じ優先順位で呼び出されていることが分かります。

5. クラス内の拡張関数からの呼び出し

最後はちょっと複雑ですが、優先順位の整理までもう一歩です。

ソース7: 同じクラス内で、メンバの拡張関数から明示的なレシーバなしで呼び出す
class A {
}

fun A.foo() {
    println("A.foo()")
}

fun foo() {
    println("foo()")
}

class B {
    fun foo() {
        println("B#foo()")
    }

    fun A.foo() {
        println("B#A.foo()")
    }

    fun call_foo() {  // この関数内でB#A.foo()を呼び出すには?
        print("call_foo() >>> ")
        // A().foo()とするとA#foo()が最優先、次にB#A.foo()、最後がA.foo()
        A().foo()

        // ちなみに
        // this.foo()とするとB#foo()が呼ばれる
        // this@Bfoo()とするとB#foo()が呼ばれる
        // foo()とするとB#foo()が最優先、次にfoo()
    }

+    fun A.call_foo() {    // この関数内での記述foo()は何を呼び出す?
+        print("A.call_foo() >>> ")
+        // foo()とするとA#foo()が最優先、
+        // 次にB#A.foo()、その次がA.foo()、
+        // さらにその次がB#foo()、あれば次はB.foo()で、
+        // 最後がトップレベルのfoo()
+        foo()
+
+        // ちなみに
+        // this.foo()とするとA#foo()が最優先、次にB#A.foo()、その次がA.foo()
+        // A().foo()とするとA#foo()が最優先、次にB#A.foo()、その次がA.foo()
+        // this@B.foo()とするとB#foo()が呼ばれる
+    }
}

fun main() {
-    B().call_foo()

+    with (B()) {
+        A().call_foo()
+    }
}

結論

クラスBで定義された、拡張関数A.call_foo()内でのfoo()という呼び出しは以下の優先順位で行われます。

Aのオブジェクトをレシーバとして、
 1 Aのメンバ関数を探す(A#foo())
 2 Aの拡張関数を探す(内側のスコープからB#A.foo(), A.foo()の順)

Bのオブジェクトをレシーバとして
 3 Bのメンバ関数を探す(B#foo())
 4 Bの拡張関数を探す(B.foo())

最後に
 5 トップレベルのfoo()を探す

Kotlinの公式リファレンスには、この動作が次のように説明されています4

In the event of a name conflict between the members of a dispatch receiver and an extension receiver, the extension receiver takes precedence.

「ディスパッチレシーバ(dispatch receiver)と拡張レシーバ(extension receiver)で、名前の競合が生じた場合は、拡張レシーバが優先される」

上記の例に当てはめると、ディスパッチレシーバのBよりも、拡張レシーバのAで優先的に関数探索がなされることになります。

おわりに

Kotlinでは関数呼び出しの優先順位を

  1. メンバ関数を探す
  2. 拡張関数を探す

としつつ、クラスのメンバのスコープと拡張関数のスコープも踏まえて決定されることが分かりました。

確かに複雑と言えば複雑ですが、整理してみると素直なルールで決定されているのですね。

  1. この記事ではクラス定義内で定義された関数をメンバ関数と表記しています。

  2. この記事ではfun A.funcName(仮引数)のように、レシーバの型と.記号を伴って定義された関数を拡張関数と表記しています。

  3. Kotlin公式リファレンスでのDeclaring extensions as members項目に相当

  4. https://kotlinlang.org/docs/extensions.html#cb9a4064

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?