1. ホーム
  2. kotlin

[解決済み] Kotlinの "receiver "って何?

2023-02-12 06:53:04

質問

拡張機能とどのような関係があるのですか?なぜ with 関数なのか キーワードではないのですか?

このトピックに関する明示的な文書はないようです。 拡張機能 .

どのように解決するのですか?

確かに、レシーバの概念に関する既存のドキュメントはほとんどないようです (唯一の例外は 拡張関数に関連する小さなサイドノート を参照)、これは驚くべきことです。

これらのトピックにはすべてドキュメントがありますが、レシーバーについて深く掘り下げたものはありません。


最初に

レシーバーって何?

Kotlin のコードのブロックは、型(あるいは複数の型)を レシーバ として持つことができ、修飾することなくそのブロックのコードでレシーバの関数とプロパティを利用できるようになります。

このようなコードのブロックを想像してみてください。

{ toLong() }

あまり意味はないですよね?実際、これを 関数型 (Int) -> Long - ここで Int は(唯一の)パラメータで、戻り値の型は Long - である場合、当然ながらコンパイルエラーになります。これを解決するには、関数呼び出しを暗黙の単一パラメータである it . しかし、DSL構築の場合、これでは問題が山積みです。

  • DSL のネストされたブロックは、その上位レイヤーがシャドウされます。

    html { it.body { // how to access extensions of html here? } ... }

    これは、HTML DSL としては問題を起こさないかもしれませんが、他のユースケースとしては問題を起こすかもしれません。
  • コードに it の呼び出しで、特にパラメータ(もうすぐレシーバになる)を多用するラムダでは、コードを散らかしたりします。

これは レシーバー が効いてきます。

このコードブロックは、関数型に割り当てられることで Int として レシーバ (パラメータとしてではなく!) として指定すると、コードは突然コンパイルされます。

val intToLong: Int.() -> Long = { toLong() }

どうしたんだ?


ちょっとした余談

このトピックでは 関数の種類 を理解していることを前提としていますが、受信者のために少し補足が必要です。

関数型はまた 一つ レシーバを持つこともできます。その場合は、型の前にドットを付けます。例

Int.() -> Long  // taking an integer as receiver producing a long
String.(Long) -> String // taking a string as receiver and long as parameter producing a string
GUI.() -> Unit // taking an GUI and producing nothing

このような関数型は、パラメータリストの先頭に受信機型が付きます。


レシーバーによるコードの解決

Receiver を持つコードのブロックがどのように処理されるかを理解するのは、実はとても簡単です。

拡張関数と同様に、コードのブロックが受信機型のクラス内部で評価されると想像してください。 これ は事実上、受信機タイプによって修正されるようになります。

先ほどの例では val intToLong: Int.() -> Long = { toLong() } の中の関数に置かれたように、コードのブロックが異なる文脈で評価されることになります。 Int . このことをよりよく示すために、手作りの型を使用した別の例を示します。

class Bar

class Foo {
    fun transformToBar(): Bar = TODO()
}

val myBlockOfCodeWithReceiverFoo: (Foo).() -> Bar = { transformToBar() }

は事実上、(コード上ではなく、心の中で - JVM上で実際にクラスを拡張することはできません)。

class Bar 

class Foo {
    fun transformToBar(): Bar = TODO()

    fun myBlockOfCode(): Bar { return transformToBar() }
}

val myBlockOfCodeWithReceiverFoo: (Foo) -> Bar = { it.myBlockOfCode() }

クラスの内部で、どのように this にアクセスするために transformToBar - は、レシーバを持つブロックでも同じことが起こります。

たまたま、ドキュメントにある この のドキュメントでは、現在のコードブロックに 2 つのレシーバがある場合に、一番外側のレシーバを使用する方法も説明されており、それは 修飾された this .


待てよ、複数の受信機があるのか?

そうです。1つのコードブロックに 複数 レシーバーがありますが、これは現在のところ型システムで表現されていません。これを実現する唯一の方法は、複数の 高次関数 を使うことです。例

class Foo
class Bar

fun Foo.functionInFoo(): Unit = TODO()
fun Bar.functionInBar(): Unit = TODO()

inline fun higherOrderFunctionTakingFoo(body: (Foo).() -> Unit) = body(Foo())
inline fun higherOrderFunctionTakingBar(body: (Bar).() -> Unit) = body(Bar())

fun example() {
    higherOrderFunctionTakingFoo {
        higherOrderFunctionTakingBar {
            functionInFoo()
            functionInBar()
        }
    }
}

もしKotlin言語のこの機能があなたのDSLに不適切だと思われる場合は、注意してください。 DslMarker はあなたの味方です!


結論

なぜ、このようなことが重要なのでしょうか?この知識で

  • と書くことができる理由を理解しました。 toLong() を書くことができるのはなぜか、ご理解いただけたと思います。 拡張関数は拡張であるべきではないのでは?
  • あなたの好きなマークアップ言語のための DSL を構築することができます。たぶん、1つまたは他の( 正規表現が必要なのは誰ですか? !).
  • あなたは、なぜ with は、標準ライブラリ 関数 は存在します。冗長なタイピングを節約するためにコードブロックのスコープを修正する行為は非常に一般的なので、言語設計者はそれを標準ライブラリに正しく配置しました。
  • (たぶん) あなたはオフショットで関数型について少し学びました。