Что такое «приемник» в Котлине?

Как это связано с функциями расширения? Почему with функцией , а не с ключевым словом?

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

Это правда, что, по-видимому, существует мало существующей документации для концепции приемников (только небольшая заметка, связанная с функциями расширения ), что удивительно:

  • их существование вытекает из функций расширения ;
  • их роль в построении DSL с использованием упомянутых функций расширения;
  • существование стандартной библиотечной функции with , которая не знает знаний получателей, может выглядеть как ключевое слово ;
  • полностью отдельный синтаксис для типов функций .

Все эти темы имеют документацию, но на приемниках ничего не происходит.


Первый:

Что такое приемник?

Любой блок кода в Kotlin может иметь (или даже несколько) типы в качестве приемника , делая функции и свойства получателя доступными в этом блоке кода без его квалификации.

Представьте себе такой блок кода:

 { toLong() } 

Не имеет большого смысла, не так ли? Фактически, присвоение этого типа функции (Int) -> Long – где Int является (единственным) параметром, а тип возврата – Long – по праву может привести к ошибке компиляции. Вы можете исправить это, просто присвоив вызов функции с помощью неявного одиночного параметра. Однако для создания DSL это вызовет ряд проблем:

  • Вложенные блоки DSL будут обладать верхними уровнями:
    html { it.body { // how to access extensions of html here? } ... }
    Это может не вызвать проблем для HTML DSL, но может быть для других случаев использования.
  • Он может помешать коду вызовами, особенно для lambdas, которые используют свой параметр (скоро, чтобы быть приемником) много.

Это то, где приемники вступают в игру.

Назначив этот блок кода типу функции, который имеет 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 

Такие типы функций имеют список параметров, предваряемый типом приемника.


Разрешение кода с приемниками

На самом деле невероятно просто понять, как обрабатываются блоки кода с приемниками:

Представьте, что, подобно функциям расширения, блок кода оценивается внутри класса типа приемника. это эффективно изменяется по типу приемника.

В нашем предыдущем примере 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 – то же самое происходит в блоке с приемником.

Так получилось, что в документации по этому вопросу также объясняется, как использовать самый внешний приемник, если текущий блок кода имеет два приемника, с помощью этого .


Подождите, несколько приемников?

Да. Блок кода может иметь несколько приемников, но в настоящее время он не имеет выражения в системе типов. Единственный способ архивировать это – это несколько функций более высокого порядка, которые используют один тип функции приемника. Пример:

 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 для своего любимого языка разметки, возможно, помогите разобрать ту или иную ( кому нужны регулярные выражения?! ).
  • Вы понимаете, почему существует стандартная библиотечная функция, а не ключевое слово – действие, связанное с изменением объема блока кода для сохранения на типизации redudant, настолько распространено, что разработчики языка правильно в стандартной библиотеке.
  • (возможно) вы немного узнали о типах функций на ответвлении.

Объяснение Ф. Джорджа хорошее.

То, как я участвую, – это прочитать несколько разных учебников и статей, потому что каждый объясняет что-то по-другому, чем другой. Для тех из вас, кто ищет другой ресурс …

 var greet: String.() -> Unit = { println("Hello $this") } 

это определяет переменную типа String.() -> Unit , которая сообщает вам

  • String – это приемник
  • () -> Unit – это тип функции

Как упоминалось выше, Ф. Джордж , все методы этого приемника можно вызвать в теле метода .

Итак, в нашем примере this используется для печати String . Функция может быть вызвана путем записи …

 greet("Fitzgerald") // result is "Hello Fitzgerald" 

приведенный фрагмент кода был взят из KOTLIN FUNCTION LITERALS WITH RECEIVER – БЫСТРОЕ ВВЕДЕНИЕ , Саймон Виртц.

Функциональные литералы / Лямбда с приемником

Котлин поддерживает концепцию «функциональных литералов с приемниками». Он позволяет получать доступ к видимым методам и свойствам приемника лямбда в своем теле без каких-либо конкретных квалификаторов . Это очень похоже на функции расширения, в которых также возможен доступ к видимым членам объекта-получателя внутри расширения.

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

 public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this } 

Как вы можете видеть, такой литерал функции с приемником принимается как block аргументов здесь. Этот блок просто выполняется и возвращается получатель (который является экземпляром T ). В действии это выглядит следующим образом:

 val foo: Bar = Bar().apply { color = RED text = "Foo" } 

Мы создаем экземпляр объекта Bar и вызываем его. Экземпляр Bar становится «приемником». block , переданный в качестве аргумента в {} (выражение лямбда), не должен использовать дополнительные квалификаторы для доступа и изменения отображаемых видимых свойств color и text .

Концепция lambdas с приемником также является самой важной функцией для написания DSL с Kotlin.