Як це пов'язано з функціями розширення? Чому це with
функція , а не ключове слово?
Здається, для цієї теми немає явної документації, лише припущення про знання стосовно посилань на розширення .
Як це пов'язано з функціями розширення? Чому це with
функція , а не ключове слово?
Здається, для цієї теми немає явної документації, лише припущення про знання стосовно посилань на розширення .
Відповіді:
Це правда, що існує мало існуючої документації щодо концепції приймачів (лише невелика допоміжна примітка, пов’язана з функціями розширення ), що дивно, враховуючи:
with
, яка, не маючи знань про приймачі, може виглядати як ключове слово ;Всі ці теми мають документацію, але на приймачах нічого не йде поглиблено.
Спочатку:
Будь-який блок коду в Kotlin може мати (або навіть кілька) типів як приймач , роблячи функції та властивості приймача доступними в цьому блоці коду без його кваліфікації.
Уявіть собі такий код:
{ toLong() }
Не має сенсу, правда? Насправді, призначаючи це до типу функції з (Int) -> Long
- де Int
є (тільки) параметр, і тип значення Long
- буде справедливо призведе до помилки компіляції. Ви можете це виправити, просто кваліфікуючи виклик функції із неявним єдиним параметром it
. Однак для побудови DSL це спричинить купу проблем:
html { it.body { // how to access extensions of html here? } ... }
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()
у функції розширення на число, замість того, щоб якось посилатися на номер. Можливо, ваша функція розширення не повинна бути розширенням?with
, стандартна функція бібліотеки а не ключове слово - акт внесення змін до обсягу блоку коду, щоб заощадити на надмірному введенні, є настільки поширеним, що дизайнери мов поміщають це прямо в стандартну бібліотеку.(Foo).() -> Unit
як функцію, яка приймає a Foo
як приймач і не має параметра. Якщо це правда, як так, що ви посилаєтесь на це аргументом Foo()
?
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.
greet
визначається як метод, який має String
приймач, але не має параметрів. Тож я розумію, як ми можемо телефонувати "Fitzgerald".greet()
, але як ми можемо телефонувати greet("Fitzgerald")
?
Котлін підтримує концепцію "функціональних літералів з приймачами". Це дозволяє отримати доступ до видимих методів та властивостей приймача лямбди у своєму тілі без будь-яких додаткових кваліфікаторів . Це дуже схоже на функції розширення, в яких також можна отримати доступ до видимих членів об'єкта приймача всередині розширення.
Простим прикладом, який також є однією з найкращих функцій у стандартній бібліотеці Котліна, є 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/…
Простіше кажучи (без зайвих слів або ускладнень), "Приймач" - це тип, який розширюється у функції розширення або імені класу. На прикладах, наведених у відповідях вище
fun Foo.functionInFoo(): Unit = TODO()
Тип "Foo" - це "Приймач"
var greet: String.() -> Unit = { println("Hello $this") }
Тип "Рядок" - це "Приймач"
Додаткова порада: зверніть увагу на Клас перед повною зупинкою (.) У декларації "веселощі" (функція)
fun receiver_class.function_name() {
//...
}
Зазвичай в 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
Екземпляр об'єкта перед. є приймачем. По суті, це «Сфера застосування», яку ви визначите в цій лямбді. Це все, що вам потрібно знати, насправді, тому що функції та властивості (змінні, супутники тощо), які ви будете використовувати в лямбда-програмі, будуть такими, що надані в рамках цієї області.
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, подібно до того, як це буде функція розширення та надаватиме їй свої конструкції, отже, "отримує" її . Отже, приймач буде вільно визначений як об'єкт, для якого функція розширення визначена за допомогою ідіоматичного стилю лямбда.