Статьи

Kotlin From Scratch: расширенные функции

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

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

  • функции расширения
  • функции высшего порядка
  • укупорочные
  • встроенные функции

Разве не было бы неплохо, если бы у типа String в Java был метод для заглавной буквы первой буквы в String как ucfirst() в PHP? Мы могли бы вызвать этот метод upperCaseFirstLetter() .

Чтобы реализовать это, вы можете создать подкласс String который расширяет тип String в Java. Но помните, что класс String в Java является окончательным, что означает, что вы не можете его расширить. Возможным решением для Kotlin было бы создание вспомогательных функций или функций верхнего уровня, но это не могло бы быть идеальным, потому что тогда мы не могли использовать функцию автозаполнения IDE, чтобы увидеть список методов, доступных для типа String . Что было бы действительно хорошо, так это добавить функцию к классу без наследования от этого класса.

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

Функция расширения объявляется вне класса, который она хочет расширить. Другими словами, это также функция верхнего уровня (если вы хотите освежить в памяти функции верхнего уровня в Kotlin, посетите руководство « Еще больше удовольствия с функциями» в этой серии).

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

Как вы можете видеть в приведенном ниже коде, мы определили функцию верхнего уровня как нормальную, чтобы мы могли объявить функцию расширения. Эта функция расширения находится внутри пакета с именем com.chike.kotlin.strings .

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

1
2
3
4
5
package com.chike.kotlin.strings
 
fun String.upperCaseFirstLetter(): String {
    return this.substring(0, 1).toUpperCase().plus(this.substring(1))
}

Обратите внимание, что ключевое слово this внутри тела функции ссылается на объект или экземпляр получателя.

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

1
2
3
4
5
package com.chike.kotlin.packagex
 
import com.chike.kotlin.strings.upperCaseFirstLetter
 
print(«chike».upperCaseFirstLetter()) // «Chike»

В приведенном выше примере тип получателя — класс String , а объект получателя"chike" . Если вы используете IDE, такую ​​как IntelliJ IDEA, которая имеет функцию IntelliSense, вы увидите новую функцию расширения, предложенную в списке других функций типа String .

IntelliJ IDEA IntelliSense особенность

Обратите внимание, что за кулисами Котлин создаст статический метод. Первый аргумент этого статического метода — объект-получатель. Поэтому вызывающим Java-программам легко вызвать этот статический метод и затем передать объект-получатель в качестве аргумента.

Например, если наша функция расширения была объявлена ​​в файле StringUtils.kt , компилятор Kotlin создаст класс Java StringUtilsKt со статическим методом upperCaseFirstLetter() .

1
2
3
4
5
6
7
8
9
/* Java */
package com.chike.kotlin.strings
 
public class StringUtilsKt {
     
    public static String upperCaseFirstLetter(String str) {
        return str.substring(0, 1).toUpperCase() + str.substring(1);
    }
}

Это означает, что вызывающие Java могут просто вызывать метод, ссылаясь на его сгенерированный класс, как и для любого другого статического метода.

1
2
/* Java */
print(StringUtilsKt.upperCaseFirstLetter(«chike»));

Помните, что этот механизм взаимодействия Java похож на работу функций верхнего уровня в Kotlin, как мы обсуждали в посте « Больше удовольствия с функциями» !

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

Итак, подведем итог: функции-члены всегда побеждают.

Давайте посмотрим на практический пример.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
class Student {
 
    fun printResult() {
        println(«Printing student result»)
    }
 
    fun expel() {
        println(«Expelling student from school»)
    }
}
 
fun Student.printResult() {
    println(«Extension function printResult()»)
}
 
fun Student.expel(reason: String) {
    println(«Expelling student from School. Reason: \»$reason\»»)
}

В приведенном выше коде мы определили тип с именем Student с двумя функциями-членами: printResult() и printResult() expel() . Затем мы определили две функции расширения, которые имеют те же имена, что и функции-члены.

Давайте вызовем printResult() и посмотрим результат.

1
2
val student = Student()
student.printResult() // Printing student result

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

Однако вызов функции-члена expel() и функции расширения expel(reason: String) приведет к разным результатам, поскольку сигнатуры функций различны.

1
2
student.expel() // Expelling student from school
student.expel(«stole money») // Expelling student from School.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
class ClassB {
 
}
 
class ClassA {
 
    fun ClassB.exFunction() {
        print(toString()) // calls ClassB toString()
    }
 
    fun callExFunction(classB: ClassB) {
        classB.exFunction() // call the extension function
    }
}

В приведенном выше коде мы объявили функцию расширения exFunction() типа ClassB внутри другого класса ClassA . Получатель диспетчеризации является экземпляром класса, в котором объявлено расширение, а экземпляр типа приемника метода расширения называется получателем расширения . В случае конфликта имен или дублирования между получателем отправки и получателем расширения, обратите внимание, что компилятор выбирает получателя расширения.

Таким образом, в приведенном выше примере кода получатель расширения является экземпляром ClassB — это означает, что метод toString() имеет тип ClassB при вызове внутри функции расширения exFunction() . Чтобы мы вместо этого ClassA метод toString() получателя диспетчеризации ClassA , нам нужно использовать квалифицированное this :

1
2
3
4
5
// …
fun ClassB.extFunction() {
    print(this@ClassA.toString()) // now calls ClassA toString() method
}
// …

Функция высшего порядка — это просто функция, которая принимает в качестве параметра другую функцию (или лямбда-выражение), возвращает функцию или выполняет обе функции. Функция коллекции last() является примером функции высшего порядка из стандартной библиотеки.

1
2
val stringList: List<String> = listOf(«in», «the», «club»)
print(stringList.last{ it.length == 3}) // «the»

Здесь мы передали лямбду last функции, которая будет служить предикатом для поиска в подмножестве элементов. Теперь мы погрузимся в создание наших собственных функций высшего порядка в Kotlin.

Глядя на функцию circleOperation() ниже, она имеет два параметра. Первый, radius , принимает double, а второй, op , — это функция, которая принимает double в качестве входных данных и также возвращает double в качестве выходных данных — мы можем более кратко сказать, что вторым параметром является «функция от double до double» ,

Обратите внимание, что типы параметров функции op для функции заключены в скобки () , а тип вывода разделен стрелкой. Функция circleOperation() является типичным примером функции высшего порядка, которая принимает функцию в качестве параметра.

1
2
3
4
5
6
7
8
fun calCircumference(radius: Double) = (2 * Math.PI) * radius
 
fun calArea(radius: Double): Double = (Math.PI) * Math.pow(radius, 2.0)
 
fun circleOperation(radius: Double, op: (Double) -> Double): Double {
    val result = op(radius)
    return result
}

При вызове этой функции circleOperation() мы передаем ей другую функцию calArea() . (Обратите внимание, что если сигнатура метода переданной функции не совпадает с тем, что объявляет функция высшего порядка, то вызов функции не будет компилироваться.)

Чтобы передать calArea() в качестве параметра функции circleOperation() , нам нужно circleOperation() к ней префикс :: и опустить скобки () .

1
2
3
4
print(circleOperation(3.0, ::calArea)) // 28.274333882308138
print(circleOperation(3.0, calArea)) // won’t compile
print(circleOperation(3.0, calArea())) // won’t compile
print(circleOperation(6.7, ::calCircumference)) // 42.09734155810323

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

Мы также можем передать лямбду (или литерал функции) в функцию более высокого порядка непосредственно при вызове функции:

1
circleOperation(5.3, { (2 * Math.PI) * it })

Помните, что для того, чтобы избежать явного именования аргумента, мы можем использовать автоматически генерируемое для it имя аргумента it только если лямбда имеет один аргумент. (Если вы хотите освежить лямбду в Котлине, посетите учебник « Больше удовольствия с функциями» в этой серии).

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

1
fun multiplier(factor: Double): (Double) -> Double = { number -> number*factor }

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

1
2
val doubler = multiplier(2)
print(doubler(5.6)) // 11.2

Чтобы проверить это, мы передали коэффициент два и присвоили возвращаемую функцию переменной doubler. Мы можем вызвать это как обычную функцию, и любое значение, которое мы передадим, будет удвоено.

Закрытие — это функция, которая имеет доступ к переменным и параметрам, которые определены во внешней области видимости.

1
2
3
4
5
6
7
8
9
fun printFilteredNamesByLength(length: Int) {
    val names = arrayListOf(«Adam», «Andrew», «Chike», «Kechi»)
    val filterResult = names.filter {
        it.length == length
    }
    println(filterResult)
}
 
printFilteredNamesByLength(5) // [Chike, Kechi]

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

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

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

Чтобы предотвратить эти издержки, команда Kotlin предоставила нам inline модификатор для функций. Функция более высокого порядка с inline модификатором будет встроена во время компиляции кода. Другими словами, компилятор скопирует лямбда (или литерал функции), а также тело функции высшего порядка и вставит их на сайт вызова.

Давайте посмотрим на практический пример.

1
2
3
4
5
6
7
8
9
fun circleOperation(radius: Double, op: (Double) -> Double) {
    println(«Radius is $radius»)
    val result = op(radius)
    println(«The result is $result»)
}
 
fun main(args: Array<String>) {
    circleOperation(5.3, { (2 * Math.PI) * it })
}

В приведенном выше коде у нас есть функция circleOperation() высшего порядка, у которой нет inline модификатора. Теперь давайте посмотрим байт-код Kotlin, сгенерированный, когда мы компилируем и декомпилируем код, а затем сравним его с тем, у которого есть inline модификатор.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public final class InlineFunctionKt {
   public static final void circleOperation(double radius, @NotNull Function1 op) {
      Intrinsics.checkParameterIsNotNull(op, «op»);
      String var3 = «Radius is » + radius;
      System.out.println(var3);
      double result = ((Number)op.invoke(radius)).doubleValue();
      String var5 = «The result is » + result;
      System.out.println(var5);
   }
 
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, «args»);
      circleOperation(5.3D, (Function1)null.INSTANCE);
   }
}

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

Давайте теперь вместо этого укажем функцию высшего порядка как inline , а также увидим сгенерированный байт-код.

1
2
3
4
5
6
7
8
9
inline fun circleOperation(radius: Double, op: (Double) -> Double) {
    println(«Radius is $radius»)
    val result = op(radius)
    println(«The result is $result»)
}
 
fun main(args: Array<String>) {
    circleOperation(5.3, { (2 * Math.PI) * it })
}

Чтобы встроить функцию высшего порядка, мы должны вставить модификатор inline перед ключевым словом fun , как мы это делали в приведенном выше коде. Давайте также проверим байт-код, сгенерированный для этой встроенной функции.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
public static final void circleOperation(double radius, @NotNull Function1 op) {
     Intrinsics.checkParameterIsNotNull(op, «op»);
     String var4 = «Radius is » + radius;
     System.out.println(var4);
     double result = ((Number)op.invoke(radius)).doubleValue();
     String var6 = «The result is » + result;
     System.out.println(var6);
  }
 
  public static final void main(@NotNull String[] args) {
     Intrinsics.checkParameterIsNotNull(args, «args»);
     double radius$iv = 5.3D;
     String var3 = «Radius is » + radius$iv;
     System.out.println(var3);
     double result$iv = 6.283185307179586D * radius$iv;
     String var9 = «The result is » + result$iv;
     System.out.println(var9);
  }

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

Благодаря этому механизму наш код был значительно оптимизирован — больше не нужно создавать анонимные классы или выделять дополнительную память. Но помните, что за кулисами у нас будет больший байт-код, чем раньше. По этой причине настоятельно рекомендуется включать только меньшие функции высшего порядка, которые принимают лямбду в качестве параметров.

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

1
2
3
4
5
6
7
8
public inline fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T> {
    return filterTo(ArrayList<T>(), predicate)
}
 
public inline fun <T> Iterable<T>.first(predicate: (T) -> Boolean): T {
    for (element in this) if (predicate(element)) return element
    throw NoSuchElementException(«Collection contains no element matching the predicate.»)
}

Не забудьте встроить обычные функции, которые не принимают лямбду в качестве параметра! Они будут компилироваться, но не будет значительного улучшения производительности (IntelliJ IDEA даже даст подсказку об этом).

Если в функции имеется более двух лямбда-параметров, у вас есть возможность решить, какую лямбду не noinline модификатор noinline для параметра. Эта функциональность особенно полезна для лямбда-параметра, который принимает много кода. Другими словами, компилятор Kotlin не будет копировать и вставлять лямбду, где он вызывается, но вместо этого создаст анонимный класс за сценой.

1
2
3
inline fun myFunc(op: (Double) -> Double, noinline op2: (Int) -> Int) {
   // perform operations
}

Здесь мы вставили модификатор noinline во второй лямбда-параметр. Обратите внимание, что этот модификатор действителен, только если функция имеет inline модификатор.

Обратите внимание, что, когда исключение выдается внутри встроенной функции, стек вызова метода в трассировке стека отличается от обычной функции без inline модификатора. Это из-за механизма копирования и вставки, используемого компилятором для встроенных функций. Круто то, что IntelliJ IDEA помогает нам легко перемещаться по стеку вызовов методов в трассировке стека для встроенной функции. Давайте посмотрим на пример.

1
2
3
4
5
6
7
inline fun myFunc(op: (Double) -> Double) {
    throw Exception(«message 123»)
}
 
fun main(args: Array<String>) {
    myFunc({ 4.5 })
}

В приведенном выше коде исключение специально myFunc() внутри встроенной функции myFunc() . Теперь давайте посмотрим трассировку стека внутри IntelliJ IDEA при запуске кода. Посмотрев на скриншот ниже, вы можете увидеть, что нам даны два варианта навигации: тело встроенной функции или сайт вызова встроенной функции. Выбор первого приведет нас к тому, что исключение было сгенерировано в теле функции, в то время как второе приведет нас к тому, что метод был вызван.

IntelliJ IDEA трассировка стека для встроенной функции

Если бы функция не была встроенной, наша трассировка стека была бы похожа на ту, с которой вы, возможно, уже знакомы:

Трассировка стека IntelliJ IDEA для нормальной функции

В этом уроке вы узнали еще больше вещей, которые вы можете делать с функциями в Kotlin. Мы покрыли:

  • функции расширения
  • функции высшего порядка
  • укупорочные
  • встроенные функции

В следующем уроке из серии Kotlin From Scratch мы рассмотрим объектно-ориентированное программирование и начнем изучать, как работают классы в Kotlin. До скорого!

Чтобы узнать больше о языке Kotlin, я рекомендую посетить документацию Kotlin . Или посмотрите некоторые другие наши посты о разработке приложений для Android здесь на Envato Tuts +!

  • Android SDK
    Java против Kotlin: стоит ли использовать Kotlin для разработки под Android?
    Джессика Торнсби
  • Android SDK
    Введение в компоненты архитектуры Android
    Жестяная мегали
  • Android SDK
    Как использовать API Google Cloud Vision в приложениях для Android
    Ашраф Хатхибелагал
  • Android SDK
    Что такое мгновенные приложения для Android?
    Джессика Торнсби