Kotlinでは、少なくとも1つの引数を持つ関数は、通常の非メンバー関数として、または 拡張関数 として定義できます。1つの引数はレシーバーです。
スコーピングに関しては、違いはないようです。どちらもクラスや他の関数の内部または外部で宣言でき、可視性修飾子を同じにすることもできないこともできます。
言語リファレンスは、さまざまな状況で通常の関数または拡張関数を使用することを推奨していないようです。
だから、私の質問は:拡張関数は通常の非メンバー関数よりも優れているのですか?そして、通常の関数は拡張よりも優れているのですか?
foo.bar(baz, baq)
vs bar(foo, baz, baq)
。
関数のセマンティクスのほんのヒントですか(レシーバーは確実にフォーカスされています)か、拡張機能を使用してコードをよりクリーンにしたり、機会を開いたりするケースはありますか?
拡張関数はいくつかの場合に役立ち、他の場合には必須です:
慣用的なケース:
既存のAPIを拡張、拡張、または変更する場合。拡張関数は、新しい機能を追加してクラスを変更する慣用的な方法です。 拡張関数 と 拡張プロパティ を追加できます。 ObjectMapper
クラスへのメソッドの追加については、 Jackson-Kotlin Module の例を参照して、TypeReference
およびジェネリックの処理を簡素化してください。
null
で呼び出すことができない新規または既存のメソッドにnull安全性を追加します。たとえば、String?.isNullOrBlank()
の文字列の拡張関数を使用すると、最初に独自のnull
チェックを行わなくても、null
文字列でもその関数を使用できます。関数自体は、内部関数を呼び出す前にチェックを行います。 Nullable Receiver(---)を使用した拡張機能については、 ドキュメントを参照してください
必須のケース:
インターフェイスのインラインデフォルト関数が必要な場合は、拡張関数を使用してインターフェイスに追加する必要があります。これは、インターフェイス宣言内では追加できないためです(インライン関数はfinal
である必要があります。インターフェース)。これは、インライン化された関数が必要な場合に便利です 、たとえばInjekt からのこのコード
現在その使用法をサポートしていないクラスにfor (item in collection) { ... }
サポートを追加する場合。 forループのドキュメント で説明されているルールに従うiterator()
拡張メソッドを追加できます-返されたイテレータのようなオブジェクトでも拡張を使用して、 next()
およびhasNext()
を提供する規則。
_+
_や_*
_などの既存のクラスに演算子を追加する(#1の特殊化ですが、これを他の方法で行うことはできないため、必須です)。演算子のオーバーロードについては、 ドキュメントを参照してください
オプションのケース:
呼び出し元に何かが表示されるときのスコープを制御する必要があるため、呼び出しを表示できるコンテキストでのみクラスを拡張します。拡張機能が常に表示されるようにすることができるため、これはオプションです。 スコーピング拡張関数については、他のSO質問の回答を参照してください
必要な実装を簡素化する一方で、ユーザーにとってより簡単なヘルパー関数を許可したいインターフェイスがあります。オプションで、インターフェースのデフォルトのメソッドを追加して支援したり、拡張機能を使用して、インターフェースの実装が予期されていない部分を追加したりできます。 1つはデフォルトのオーバーライドを許可し、もう1つは許可しません(拡張機能とメンバーの優先順位を除く)。
関数を機能のカテゴリに関連付けたい場合。拡張関数は、レシーバークラスを検索場所として使用します。それらの名前空間は、トリガーできる1つまたは複数のクラスになります。一方、トップレベルの関数は見つけにくく、IDEコード補完ダイアログのグローバルネームスペースがいっぱいになります。既存のライブラリの名前空間の問題を修正することもできます。たとえば、Java 7ではPath
クラスがあり、名前の配列が奇数であるため、Files.exist(path)
メソッドを見つけるのは困難です。代わりに、関数をPath.exists()
に直接配置できます。 (@kirill)
優先ルール:
既存のクラスを拡張するときは、優先ルールを覚えておいてください。それらは KT-10806 で次のように記述されます:
現在のコンテキストの各暗黙のレシーバーについて、メンバー、ローカル拡張関数(拡張関数タイプを持つパラメーター)、非ローカル拡張の順に試行します。
拡張関数は、安全な呼び出し演算子?.
。関数の引数がnull
になることが予想される場合は、早期に戻るのではなく、それを拡張関数のレシーバーにします。
通常の機能:
fun nullableSubstring(s: String?, from: Int, to: Int): String? {
if (s == null) {
return null
}
return s.substring(from, to)
}
拡張機能:
fun String.extensionSubstring(from: Int, to: Int) = substring(from, to)
呼び出しサイト:
fun main(args: Array<String>) {
val s: String? = null
val maybeSubstring = nullableSubstring(s, 0, 1)
val alsoMaybeSubstring = s?.extensionSubstring(0, 1)
ご覧のとおり、どちらも同じことを行いますが、拡張関数は短く、呼び出しサイトでは結果がnull可能であることはすぐにわかります。
拡張関数が必須であるケースが少なくとも1つあります-「流暢なスタイル」とも呼ばれる呼び出しチェーン。
foo.doX().doY().doZ()
独自の操作でJava 8からStreamインターフェースを拡張したいとします。もちろん、そのために通常の関数を使用できますが、それはひどく醜く見えます:
doZ(doY(doX(someStream())))
明らかに、そのために拡張関数を使用したいのです。また、通常の関数を中置型にすることはできませんが、拡張関数を使用して行うことができます。
infix fun <A, B, C> ((A) -> B).`|`(f: (B) -> C): (A) -> C = { a -> f(this(a)) }
@Test
fun pipe() {
val mul2 = { x: Int -> x * 2 }
val add1 = { x: Int -> x + 1 }
assertEquals("7", (mul2 `|` add1 `|` Any::toString)(3))
}
haveを使用して拡張メソッドを使用する場合があります。例えば。リストの実装がある場合MyList<T>
、次のような拡張メソッドを書くことができます
fun Int MyList<Int>.sum() { ... }
これを「通常の」メソッドとして書くことは不可能です。