Що таке "приймач" у Котліні?


79

Як це пов'язано з функціями розширення? Чому це with функція , а не ключове слово?

Здається, для цієї теми немає явної документації, лише припущення про знання стосовно посилань на розширення .


1
Існуючі відповіді вже сказали, що таке приймач, але може бути корисним зрозуміти, звідки походить це слово: в об’єктно-орієнтованих мовах, що передають повідомлення, таких як Smalltalk, виклики методів сприймаються як повідомлення, що передаються об’єкту. Об'єктом, за яким викликається метод, є "одержувач" повідомлення.
LarsH

Відповіді:


124

Це правда, що існує мало існуючої документації щодо концепції приймачів (лише невелика допоміжна примітка, пов’язана з функціями розширення ), що дивно, враховуючи:

Всі ці теми мають документацію, але на приймачах нічого не йде поглиблено.


Спочатку:

Що таке приймач?

Будь-який блок коду в 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

У таких типів функцій список параметрів має префікс до типу приймача.


Вирішення коду за допомогою приймачів

Насправді неймовірно просто зрозуміти, як обробляються блоки коду з приймачами:

Уявіть, що, подібно до функцій розширення, блок коду обчислюється всередині класу типу приймача. це фактично змінюється типом приймача.

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

Питання: Я читаю (Foo).() -> Unitяк функцію, яка приймає a Fooяк приймач і не має параметра. Якщо це правда, як так, що ви посилаєтесь на це аргументом Foo()?
Абхіджіт Саркар

1
@AbhijitSarkar Типи функцій з приймачем мають список параметрів із префіксом до приймача. Це має бути в основній частині повідомлення, редагування ..
Ф. Джордж

Ви також можете мати кілька приймачів, коли ви визначаєте розширення всередині класу
Ігор Войда,

@Panel Ви піднімаєте дійсний бал, але якщо хтось прийде за цим постом, щоб зрозуміти, що таке приймач, різниця статичної / віртуальної відправки може бути занадто високою планкою ... чорт візьміть, навіть іноді мене підводить.
Ф. Джордж

1
Ось детальне пояснення різних типів приймачів. blog.kotlin-academy.com/…
Ігор

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

це визначає змінну типу String.() -> Unit , яка вам повідомляє

  • Stringє приймачем
  • () -> Unit - тип функції

Як і Ф. Джордж, згаданий вище, усі методи цього приймача можна викликати в тілі методу.

Отже, у нашому прикладі thisвикористовується для друку String. Функцію можна викликати, написавши ...

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

наведений вище фрагмент коду взято з Kotlin Function Literals with Receiver - Quick Introduction by Simon Wirtz.


2
У цьому випадку ми можемо робити різні типи дзвінків: greet ("мій текст"), що має такий самий ефект, як "мій текст" .greet ()
ультраон

Я не розумію. greetвизначається як метод, який має Stringприймач, але не має параметрів. Тож я розумію, як ми можемо телефонувати "Fitzgerald".greet(), але як ми можемо телефонувати greet("Fitzgerald")?
LarsH

(FYI посилання на статтю Саймона Вірца порушено.)
LarsH

13

Функція літералів / лямбда з приймачем

Котлін підтримує концепцію "функціональних літералів з приймачами". Це дозволяє отримати доступ до видимих ​​методів та властивостей приймача лямбди у своєму тілі без будь-яких додаткових кваліфікаторів . Це дуже схоже на функції розширення, в яких також можна отримати доступ до видимих ​​членів об'єкта приймача всередині розширення.

Простим прикладом, який також є однією з найкращих функцій у стандартній бібліотеці Котліна, є 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та викликаємо applyйого. Екземпляр " Barстає" одержувачем. Параметр block, переданий як аргумент у {}(лямбда-вираз), не потребує використання додаткових кваліфікаторів для доступу та модифікації показаних видимих ​​властивостей colorта text.

Поняття лямбди з приймачем є також найважливішою особливістю для написання DSL-кодів за допомогою Котліна.


Оскільки ви можете вперше побачити цей синтаксис, {...}in apply{...}- це просто лямбда-функція як аргумент apply. Лямбда - це кінцева лямбда , де вона не обов’язково повинна бути в дужках, що застосовуються. Насправді це може бути застосувати ({...}), що було б менш заплутаним для мене, коли я вперше вивчив це. kotlinlang.org/docs/reference/…
Бен Баттерворт,

10

Простіше кажучи (без зайвих слів або ускладнень), "Приймач" - це тип, який розширюється у функції розширення або імені класу. На прикладах, наведених у відповідях вище

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

Тип "Foo" - це "Приймач"

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

Тип "Рядок" - це "Приймач"

Додаткова порада: зверніть увагу на Клас перед повною зупинкою (.) У декларації "веселощі" (функція)

fun receiver_class.function_name() {
   //...
}

1
єдина адекватна відповідь
Вальдманн

1

Зазвичай в Java або Kotlin у вас є методи або функції з вхідними параметрами типу T. У Kotlin ви також можете мати функції розширення, які отримують значення типу T.

Якщо у вас є функція, яка приймає параметр String, наприклад:

fun hasWhitespace(line: String): Boolean {
    for (ch in line) if (ch.isWhitespace()) return true
    return false
}

перетворення параметра в приймач (що можна зробити автоматично за допомогою IntelliJ):

fun String.hasWhitespace(): Boolean {
    for (ch in this) if (ch.isWhitespace()) return true
    return false
}

тепер у нас є функція розширення, яка отримує рядок, і ми можемо отримати доступ до значення за допомогою this


1

Екземпляр об'єкта перед. є приймачем. По суті, це «Сфера застосування», яку ви визначите в цій лямбді. Це все, що вам потрібно знати, насправді, тому що функції та властивості (змінні, супутники тощо), які ви будете використовувати в лямбда-програмі, будуть такими, що надані в рамках цієї області.

        class Music(){
    
        var track:String=""
    
        fun printTrack():Unit{
            println(track)
        }
    }
    
    //Music class is the receiver of this function, in other words, the lambda can be piled after a Music class just like its extension function Since Music is an instance, refer to it by 'this', refer to lambda parameters by 'it', like always
    val track_name:Music.(String)->Unit={track=it;printTrack()}
/*Create an Instance of Music and immediately call its function received by the name 'track_name', and exclusively available to instances of this class*/
Music().track_name("Still Breathing")

//Output
Still Breathing

Ви визначаєте цю змінну за допомогою всіх параметрів та типів повернення, які вона матиме, але серед усіх визначених конструкцій лише екземпляр об'єкта може викликати var, подібно до того, як це буде функція розширення та надаватиме їй свої конструкції, отже, "отримує" її . Отже, приймач буде вільно визначений як об'єкт, для якого функція розширення визначена за допомогою ідіоматичного стилю лямбда.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.