Когда следует предпочесть функции расширения Котлина?

В Kotlin функция с хотя бы одним аргументом может быть определена либо как регулярная нечлена-функция, либо как функция расширения с одним аргумент является приемником.

Что касается области охвата, то, похоже, нет никакой разницы: оба могут быть объявлены внутри или вне классов и других функций, и оба могут или не могут иметь модификаторы видимости одинаково.

Ссылка на язык, похоже, не рекомендует использовать обычные функции или функции расширения для разных ситуаций.

Итак, мой вопрос: , когда функции расширения дают преимущество над регулярными нечленными? И когда регулярные над расширениями?

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

Это просто намек на семантику функции (приемник определенно находится в фокусе), или есть случаи, когда использование функций расширений делает код намного более чистым/открывает возможности?

Ответ 1

Функции расширения полезны в нескольких случаях и обязательны для других:

Идиоматические случаи:

  • Если вы хотите улучшить, расширить или изменить существующий API. Функция расширения - это идиоматический способ изменить класс, добавив новые функции. Вы можете добавить функции расширения и свойства расширения. См. Пример в Jackson-Kotlin Module для добавления методов в класс ObjectMapper, упрощающий обработку TypeReference и дженериков.

  • Добавление нулевой безопасности к новым или существующим методам, которые нельзя вызвать в null. Например, функция расширения для String из String?.isNullOrBlank() позволяет вам использовать эту функцию даже в строке null, не выполняя сначала свою проверку null. Сама функция выполняет проверку перед вызовом внутренних функций. См. документация для расширений с помощью Nullable Receiver

Обязательные случаи:

  1. Если вы хотите встроенную функцию по умолчанию для интерфейса, вы должны использовать функцию расширения, чтобы добавить ее в интерфейс, потому что вы не можете сделать это в объявлении интерфейса (встроенные функции должны быть final, которые в настоящее время отсутствуют разрешено внутри интерфейса). Это полезно, когда вам нужны встроенные функции reified, например, этот код из Injekt

  2. Если вы хотите добавить поддержку for (item in collection) { ... } к классу, который в настоящее время не поддерживает это использование. Вы можете добавить метод расширения iterator(), который следует правилам, описанным в для документации циклов - даже возвращенный объект, похожий на итератор, может использовать расширения для удовлетворения правил предоставления next() и hasNext().

  3. Добавление операторов в существующие классы, такие как + и * (специализация №1, но вы не можете сделать это каким-либо другим способом, поэтому обязательно). См. документация для перегрузки оператора

Дополнительные случаи:

  1. Вы хотите управлять областью определения, когда что-то видимо вызывающему, поэтому вы расширяете класс только в том контексте, в котором вы разрешаете вызов быть видимым. Это необязательно, потому что вы можете просто позволить расширениям видеть всегда. см. ответ в другом вопросе SO для определения функций расширения

  2. У вас есть интерфейс, который вы хотите упростить для реализации, но при этом предоставляете пользователю более удобные вспомогательные функции. Вы можете дополнительно добавить методы по умолчанию для интерфейса, чтобы помочь, или использовать функции расширения для добавления частей интерфейса, которые не подлежат ожиданию. Один позволяет переопределять значения по умолчанию, другой - нет (кроме приоритета расширений против членов).

  3. Если вы хотите связать функции с категорией функциональности; функции расширения используют свой класс приемников в качестве места для поиска. Их пространство имен становится классом (или классами), из которого они могут быть запущены. В то время как функции верхнего уровня будут сложнее найти и заполнит глобальное пространство имен в диалоговом окне завершения кода IDE. Вы также можете исправить существующие проблемы пространства имен библиотеки. Например, в Java 7 у вас есть класс Path, и трудно найти метод Files.exist(path), потому что это имя задано странно. Вместо этого функция может быть помещена непосредственно на Path.exists(). (@kirill)

Правила приоритета:

При расширении существующих классов соблюдайте правила приоритета. Они описаны в KT-10806 как:

Для каждого неявного приемника в текущем контексте мы пытаемся использовать члены, затем локальные функции расширения (также параметры, которые имеют тип функции расширения), а затем нелокальные расширения.

Ответ 2

Существует, по крайней мере, один случай, когда функции расширения являются цепочкой обязательных вызовов, также известной как "свободный стиль":

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

Предположим, вы хотите расширить интерфейс Stream от Java 8 с помощью собственных операций. Конечно, для этого вы можете использовать обычные функции, но это будет выглядеть уродливо, как черт:

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

Очевидно, вы хотите использовать для этого функции расширения. Кроме того, вы не можете делать обычные функции infix, но вы можете сделать это с помощью функций расширения:

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

Ответ 3

Функции расширения отлично работают с оператором безопасного вызова ?.. Если вы ожидаете, что аргумент функции иногда будет 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)

Как вы можете видеть, оба делают то же самое, однако функция расширения короче и на сайте вызова сразу же очищает, что результат будет нулевым.   }