web-dev-qa-db-fra.com

Quand faut-il préférer les fonctions d'extension Kotlin?

Dans Kotlin, une fonction avec au moins un argument peut être définie soit comme une fonction non membre normale, soit comme une fonction extension avec un argument comme destinataire.

En ce qui concerne la portée, il ne semble y avoir aucune différence: les deux peuvent être déclarés à l'intérieur ou à l'extérieur de classes et d'autres fonctions, et les deux peuvent ou ne peuvent pas avoir des modificateurs de visibilité de manière égale.

Le langage de référence ne semble pas recommander l'utilisation de fonctions standard ou d'extension dans différentes situations.

Ma question est donc la suivante: à quel moment les fonctions d’extension donnent-elles un avantage par rapport à celles qui ne sont pas membres régulières? Et quand celles qui sont régulières par rapport aux extensions?

foo.bar(baz, baq) vs bar(foo, baz, baq).

S'agit-il juste d'un soupçon de sémantique de fonction (le récepteur est définitivement au centre) ou existe-t-il des cas dans lesquels l'utilisation de fonctions d'extension rend le code beaucoup plus propre/ouvre des opportunités?

42
hotkey

Les fonctions d'extension sont utiles dans quelques cas et obligatoires dans d'autres:

Cas idiomatiques:

  1. Lorsque vous souhaitez améliorer, étendre ou modifier une API existante. Une fonction d'extension est le moyen idiomatique de changer une classe en ajoutant de nouvelles fonctionnalités. Vous pouvez ajouter fonctions d'extension et propriétés d'extension . Voir un exemple dans le module Jackson-Kotlin pour l'ajout de méthodes à la classe ObjectMapper en simplifiant le traitement de TypeReference et des génériques.

  2. Ajout de la sécurité null à des méthodes nouvelles ou existantes qui ne peuvent pas être appelées sur une null. Par exemple, la fonction d'extension pour String of String?.isNullOrBlank() vous permet d'utiliser cette fonction même sur une chaîne null sans avoir à effectuer votre propre vérification null au préalable. La fonction elle-même effectue le contrôle avant d'appeler les fonctions internes. Voir documentation pour les extensions avec Nullable Receiver

Cas obligatoires:

  1. Lorsque vous souhaitez une fonction par défaut en ligne pour une interface, vous devez utiliser une fonction d'extension pour l'ajouter à l'interface car vous ne pouvez pas le faire dans la déclaration d'interface (les fonctions en ligne doivent être final, ce qui n'est actuellement pas autorisé dans une interface). Ceci est utile lorsque vous avez besoin de fonctions réifiées en ligne, par exemple ce code de Injekt

  2. Lorsque vous souhaitez ajouter le support for (item in collection) { ... } à une classe qui ne prend actuellement pas en charge cet usage. Vous pouvez ajouter une méthode d'extension iterator() qui suit les règles décrites dans la pour la documentation sur les boucles . Même l'objet renvoyé de type itérateur peut utiliser des extensions pour satisfaire les règles de fourniture de next() et hasNext().

  3. L'ajout d'opérateurs à des classes existantes telles que + et * (spécialisation de # 1 mais vous ne pouvez pas le faire autrement, est donc obligatoire). Voir documentation sur la surcharge de l'opérateur

Cas facultatifs:

  1. Vous souhaitez contrôler la portée de l'appelant lorsque quelque chose est visible, vous étendez la classe uniquement dans le contexte dans lequel vous autoriserez la visibilité de l'appel. Ceci est facultatif car vous pouvez simplement permettre que les extensions soient toujours vues. voir la réponse dans une autre question SO pour des fonctions d'extension de la portée }

  2. Vous avez une interface que vous souhaitez simplifier l'implémentation requise, tout en permettant des fonctions d'assistance plus simples pour l'utilisateur. Vous pouvez éventuellement ajouter des méthodes par défaut pour l'interface afin de vous aider ou utiliser des fonctions d'extension pour ajouter les parties non attendues de l'interface. L'un permet de remplacer les valeurs par défaut, l'autre non (sauf pour la priorité des extensions par rapport aux membres).

  3. Lorsque vous souhaitez associer des fonctions à une catégorie de fonctionnalités; les fonctions d’extension utilisent leur classe de récepteurs comme un lieu de recherche. Leur espace de noms devient la classe (ou les classes) à partir de laquelle ils peuvent être déclenchés. Les fonctions de niveau supérieur seront plus difficiles à trouver et rempliront l’espace de noms global dans les boîtes de dialogue de complétion de code IDE. Vous pouvez également résoudre les problèmes d'espace de nom de bibliothèque existants. Par exemple, dans Java 7, vous avez la classe Path et il est difficile de trouver la méthode Files.exist(path) car son nom est espacé de manière étrange. La fonction pourrait être placée directement sur Path.exists() à la place. (@kirill)

Règles de priorité:

Lors de l'extension de classes existantes, gardez à l'esprit les règles de priorité. Ils sont décrits dans KT-10806 comme:

Pour chaque récepteur implicite du contexte actuel, nous essayons les membres, puis les fonctions d'extension locales (ainsi que les paramètres ayant le type de fonction d'extension), puis les extensions non locales.

46
Jayson Minard

Les fonctions d’extension fonctionnent très bien avec l’opérateur d’appel sécurisé ?.. Si vous vous attendez à ce que l'argument de la fonction devienne parfois null, au lieu d'un retour anticipé, faites-en le récepteur d'une fonction d'extension. 

Fonction ordinaire:

fun nullableSubstring(s: String?, from: Int, to: Int): String? {
    if (s == null) {
        return null
    }

    return s.substring(from, to)
}

Fonction d'extension:

fun String.extensionSubstring(from: Int, to: Int) = substring(from, to)

Site d'appel:

fun main(args: Array<String>) {
    val s: String? = null

    val maybeSubstring = nullableSubstring(s, 0, 1)
    val alsoMaybeSubstring = s?.extensionSubstring(0, 1)

Comme vous pouvez le constater, les deux font la même chose, mais la fonction d’extension est plus courte et sur le site d’appel, il est clair que le résultat sera nul.

8
Kirill Rakhman

Il existe au moins un cas où les fonctions d'extension constituent un chaînage indispensable, également appelé "style fluide":

foo.doX().doY().doZ()

Supposons que vous souhaitiez étendre l'interface Stream à partir de Java 8 avec vos propres opérations. Bien sûr, vous pouvez utiliser des fonctions ordinaires pour cela, mais ça aura l'air horrible:

doZ(doY(doX(someStream())))

Clairement, vous voulez utiliser des fonctions d'extension pour cela . De plus, vous ne pouvez pas créer de fonctions infixes ordinaires, mais vous pouvez le faire avec des fonctions d'extension:

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))
}
4
bytefu

Il y a des cas où vous devez utiliser des méthodes d'extension. Par exemple. si vous avez une implémentation de liste MyList<T>, vous pouvez écrire une méthode d'extension comme

fun Int MyList<Int>.sum() { ... }

Il est impossible d'écrire ceci en tant que méthode "normale".

0
Landei