Статьи

Kotlin с нуля: больше веселья с функциями

Kotlin — это современный язык программирования, который компилируется в байт-код Java. Он бесплатный и с открытым исходным кодом и обещает сделать кодирование для Android еще более увлекательным.

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

  • функции верхнего уровня
  • лямбда-выражения или функциональные литералы
  • анонимные функции
  • локальные или вложенные функции
  • инфиксные функции
  • функции-члены

Вы будете поражены всеми классными вещами, которые вы можете делать с функциями в Kotlin!

Функции верхнего уровня — это функции внутри пакета Kotlin, которые определены вне любого класса, объекта или интерфейса. Это означает, что они являются функциями, которые вы вызываете напрямую, без необходимости создавать какой-либо объект или вызывать какой-либо класс.

Если вы Java-кодер, вы знаете, что мы обычно создаем вспомогательные статические методы внутри вспомогательных классов. Эти вспомогательные классы на самом деле ничего не делают — у них нет методов состояния или экземпляра, и они просто действуют как контейнер для статических методов. Типичным примером является класс Collections в пакете java.util и его статические методы.

Функции верхнего уровня в Kotlin можно использовать вместо статических служебных методов внутри вспомогательных классов, которые мы кодируем в Java. Давайте посмотрим, как определить функцию верхнего уровня в Kotlin.

1
2
3
4
5
package com.chikekotlin.projectx.utils
 
fun checkUserStatus(): String {
    return «online»
}

В приведенном выше коде мы определили пакет com.chikekotlin.projectx.utils внутри файла с именем UserUtils.kt, а также определили служебную функцию верхнего уровня с именем checkUserStatus() внутри этого же пакета и файла. Для краткости эта очень простая функция возвращает строку «онлайн».

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

1
2
3
4
5
6
7
package com.chikekotlin.projectx.users
 
import com.chikekotlin.projectx.utils.checkUserStatus
 
if (checkUserStatus() == «online») {
    // do something
}

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

Учитывая, что Java не поддерживает функции верхнего уровня, компилятор Kotlin за кулисами создаст класс Java, а отдельные функции верхнего уровня будут преобразованы в статические методы. В нашем собственном случае сгенерированный класс Java был UserUtilsKt со статическим методом checkUserStatus() .

1
2
3
4
5
6
7
8
9
/* Java */
package com.chikekotlin.projectx.utils
 
public class UserUtilsKt {
 
    public static String checkUserStatus() {
        return «online»;
    }
}

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

1
2
3
4
5
6
/* Java */
import com.chikekotlin.projectx.utils.UserUtilsKt
 
 
UserUtilsKt.checkUserStatus()

Обратите внимание, что мы можем изменить имя класса Java, которое генерирует компилятор Kotlin, используя аннотацию @JvmName .

1
2
3
4
5
6
@file:JvmName(«UserUtils»)
package com.chikekotlin.projectx.utils
 
fun checkUserStatus(): String {
    return «online»
}

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

На него можно ссылаться из Java следующим образом:

1
2
3
4
5
6
/* Java */
import com.chikekotlin.projectx.utils.UserUtils
 
 
UserUtils.checkUserStatus()

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

Если вы Java-кодер, вы знаете, что Java 8 и выше обеспечивает поддержку лямбда-выражений. Чтобы использовать лямбда-выражения в проекте, который поддерживает более ранние версии Java, такие как Java 7, 6 или 5, мы можем использовать популярную библиотеку Retrolambda .

Одна из замечательных особенностей Kotlin заключается в том, что лямбда-выражения поддерживаются «из коробки». Поскольку лямбда не поддерживается в Java 6 или 7, чтобы Kotlin мог взаимодействовать с ней, Kotlin создает за сценой анонимный класс Java. Но обратите внимание, что создание лямбда-выражения в Kotlin совсем не так, как в Java.

Вот характеристики лямбда-выражения в Kotlin:

  • Он должен быть окружен фигурными скобками {} .
  • У этого нет fun ключевого слова.
  • Модификатора доступа (частного, открытого или защищенного) не существует, поскольку он не принадлежит ни одному классу, объекту или интерфейсу.
  • У него нет имени функции. Другими словами, это анонимно.
  • Тип возврата не указан, так как он будет определен компилятором.
  • Параметры не заключены в круглые скобки () .

Более того, мы можем назначить лямбда-выражение переменной и затем выполнить ее.

Давайте теперь посмотрим несколько примеров лямбда-выражений. В приведенном ниже коде мы создали лямбда-выражение без каких-либо параметров и присвоили ему переменное message . Затем мы выполнили лямбда-выражение, вызвав message() .

1
2
val message = { println(«Hey, Kotlin is really cool!») }
message() // «Hey, Kotlin is really cool!»

Давайте также посмотрим, как включить параметры в лямбда-выражение.

1
2
3
val message = { myString: String -> println(myString) }
message(«I love Kotlin») // «I love Kotlin»
message(«How far?») // «How far?»

В приведенном выше коде мы создали лямбда-выражение с параметром myString вместе с параметром типа String . Как видите, перед типом параметра есть стрелка: это относится к лямбда-телу. Другими словами, эта стрелка отделяет список параметров от лямбда-тела. Чтобы сделать его более кратким, мы можем полностью игнорировать тип параметра (уже выведенный компилятором).

1
val message = { myString -> println(myString) } // will still compile

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

1
2
3
4
5
6
val addNumbers = { number1: Int, number2: Int ->
        println(«Adding $number1 and $number2»)
        val result = number1 + number2
        println(«The result is $result»)
    }
addNumbers(1, 3)

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

1
2
Adding 1 and 3
The result is 4

Мы можем передавать лямбда-выражения в качестве параметров функциям: они называются «функциями высшего порядка», потому что они являются функциями функций. Эти типы функций могут принимать лямбда или анонимную функцию в качестве параметра: например, функцию коллекции last() .

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

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

Давайте посмотрим, как сделать эту последнюю строку кода выше более читабельной.

1
stringList.last { s: String -> s.length == 3 } // will also compile and print «the»

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

Кроме того, мы можем сделать его более кратким, удалив тип параметра.

1
stringList.last { s -> s.length == 3 } // will also compile print «the»

Нам не нужно явно указывать тип параметра, потому что тип параметра всегда совпадает с типом элемента коллекции. В приведенном выше коде мы вызываем last в коллекции списков объектов String , поэтому компилятор Kotlin достаточно умен, чтобы знать, что параметр также будет иметь тип String .

Мы даже можем еще больше упростить лямбда-выражение, заменив аргумент лямбда-выражения на сгенерированный по умолчанию аргумент с именем name.

1
stringList.last { it.length == 3 }

Имя аргумента it было сгенерировано автоматически, потому что last может принимать лямбда-выражение или анонимную функцию (мы вскоре вернемся к этому) только с одним аргументом, и его тип может быть определен компилятором.

Давайте начнем с примера. В приведенном ниже коде мы передаем лямбда-выражение в функцию foreach() вызываемую в коллекции intList . Эта функция будет перебирать коллекцию и выполнять лямбду для каждого элемента в списке. Если какой-либо элемент делится на 2, он остановится и вернется из лямбды.

01
02
03
04
05
06
07
08
09
10
11
fun surroundingFunction() {
    val intList = listOf(1, 2, 3, 4, 5)
    intList.forEach {
        if (it % 2 == 0) {
            return
        }
    }
    println(«End of surroundingFunction()»)
}
 
surroundingFunction() // nothing happened

Выполнение приведенного выше кода может не дать вам ожидаемого результата. Это связано с тем, что оператор return не будет возвращаться из лямбды, а вместо этого из содержащей функции Это означает, что последний оператор кода вroundFunction surroundingFunction() не будет выполнен.

1
2
3
// …
println(«End of surroundingFunction()») // This won’t execute
// …

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

01
02
03
04
05
06
07
08
09
10
11
fun surroundingFunction() {
    val intList = listOf(1, 2, 3, 4, 5)
    intList.forEach {
        if (it % 2 == 0) {
            return@forEach
        }
    }
    println(«End of surroundingFunction()») // Now, it will execute
}
 
surroundingFunction() // print «End of surroundingFunction()»

В обновленном коде выше мы указали тег по умолчанию @forEach сразу после ключевого слова return внутри лямбды. Теперь мы дали указание компилятору возвращаться из лямбды, а не в функцию, содержащую surroundingFunction() функцию surroundingFunction() . Теперь будет выполнено последнее утверждениеroundFunction surroundingFunction() .

Обратите внимание, что мы также можем определить нашу собственную метку или имя тега.

1
2
3
4
5
// …
   intList.forEach myLabel@ {
       if (it % 2 == 0) {
           return@myLabel
   // …

В приведенном выше коде мы определили нашу собственную метку myLabel@ а затем указали ее для ключевого слова return . @forEach сгенерированная компилятором для функции forEach больше не доступна, поскольку мы определили нашу собственную.

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

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

1
2
3
4
5
6
7
class Circle {
 
    fun calculateArea(radius: Double): Double {
        require(radius > 0, { «Radius must be greater than 0» })
        return Math.PI * Math.pow(radius, 2.0)
    }
}

В этом фрагменте кода показан класс Circle (мы обсудим классы Kotlin в последующих публикациях), в котором есть функция-член calculateArea() . Эта функция принимает radius параметра для вычисления площади круга.

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

1
2
val circle = Circle()
print(circle.calculateArea(4.5)) // will print «63.61725123519331»

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

  • не имеет имени
  • создается с fun ключевым словом
  • содержит тело функции
1
2
val stringList: List<String> = listOf(«in», «the», «club»)
print(stringList.last{ it.length == 3}) // will print «the»

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

1
2
3
4
val strLenThree = stringList.last( fun(string): Boolean {
    return string.length == 3
})
print(strLenThree) // will print «the»

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

В конце лямбда-секции в этом уроке мы использовали метку, чтобы указать, из какой функции возвращаться. Использование анонимной функции вместо лямбды внутри функции forEach() решает эту проблему проще. Возвращаемое выражение возвращается из анонимной функции, а не из окружающей, которая в нашем случае являетсяroundFunction surroundingFunction() .

01
02
03
04
05
06
07
08
09
10
11
fun surroundingFunction() {
    val intList = listOf(1, 2, 3, 4, 5)
    intList.forEach ( fun(number) {
        if (number % 2 == 0) {
            return
        }
    })
    println(«End of surroundingFunction()») // statement executed
}
 
surroundingFunction() // will print «End of surroundingFunction()»

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

01
02
03
04
05
06
07
08
09
10
11
12
fun printCircumferenceAndArea(radius: Double): Unit {
 
    fun calCircumference(radius: Double): Double = (2 * Math.PI) * radius
    val circumference = «%.2f».format(calCircumference(radius))
 
    fun calArea(radius: Double): Double = (Math.PI) * Math.pow(radius, 2.0)
    val area = «%.2f».format(calArea(radius))
 
    print(«The circle circumference of $radius radius is $circumference and area is $area»)
}
 
printCircumferenceAndArea(3.0) // The circle circumference of 3.0 radius is 18.85 and area is 28.27

Как вы можете calCircumference() приведенного выше фрагмента кода, у нас есть две однострочные функции: calCircumference() и calArea() вложенные в printCircumferenceAndAread() . Вложенные функции могут вызываться только изнутри функции, а не снаружи. Опять же, использование вложенных функций делает нашу программу более модульной и аккуратной.

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

1
2
3
4
5
6
7
8
9
fun printCircumferenceAndArea(radius: Double): Unit {
 
    fun calCircumference(): Double = (2 * Math.PI) * radius
    val circumference = «%.2f».format(calCircumference())
 
    fun calArea(): Double = (Math.PI) * Math.pow(radius, 2.0)
    val area = «%.2f».format(calArea())
    // …
}

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

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

Давайте посмотрим, как создать инфиксную функцию в классе. В приведенном ниже примере кода мы создали класс Student с изменяемым kotlinScore экземпляра kotlinScore . Мы создали функцию infix, используя модификатор infix перед ключевым словом fun . Как вы можете видеть ниже, мы создали addKotlinScore() функцию addKotlinScore() которая берет оценку и добавляет в kotlinScore экземпляра kotlinScore .

1
2
3
4
5
6
7
class Student {
    var kotlinScore = 0.0
     
    infix fun addKotlinScore(score: Double): Unit {
        this.kotlinScore = kotlinScore + score
    }
}

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

1
2
3
val student = Student()
student addKotlinScore 95.00
print(student.kotlinScore) // will print «95.0»

В приведенном выше коде мы вызывали инфиксную функцию, целевой объект — student , а двойной 95.00 — это параметр, передаваемый в функцию.

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

1
2
3
4
«Chike» should startWith(«ch»)
myList should contain(myElement)
«Chike» should haveLength(5)
myMap should haveKey(myKey)

В Kotlin мы можем сделать создание экземпляра Pair более лаконичным, используя функцию to infix вместо конструктора Pair . (За кулисами также создается экземпляр Pair .) Обратите внимание, что функция to также является функцией расширения (мы обсудим это подробнее в следующем посте).

1
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

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

1
2
val nigeriaCallingCodePair = 234 to «Nigeria»
val nigeriaCallingCodePair2 = Pair(234, «Nigeria») // Same as above

Как видно из приведенного выше кода, использование функции to infix более кратко, чем непосредственное использование конструктора Pair для создания экземпляра Pair . Помните, что при использовании функции to infix 234 является целевым объектом, а String «Nigeria» является параметром, передаваемым функции. Кроме того, обратите внимание, что мы также можем сделать это для создания типа Pair :

1
val nigeriaCallingCodePair3 = 234.to(«Nigeria») // same as using 234 to «Nigeria»

В посте « Диапазоны и коллекции» мы создали коллекцию карт в Котлине, предоставив ей список пар: первое значение — это ключ, а второе — значение. Давайте также сравним создание карты с помощью функции to infix и конструктора Pair для создания отдельных пар.

1
val callingCodesMap: Map<Int, String> = mapOf(234 to «Nigeria», 1 to «USA», 233 to «Ghana»)

В приведенном выше коде мы создали разделенный запятыми список типов Pair с помощью функции to infix и передали их в mapOf() . Мы также можем создать ту же карту, напрямую используя конструктор Pair для каждой пары.

1
val callingCodesPairMap: Map<Int, String> = mapOf(Pair(234, «Nigeria»), Pair(1, «USA»), Pair(233, «Ghana»))

Как вы можете видеть снова, придерживание функции to infix имеет меньше шума, чем использование конструктора Pair .

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

  • функции верхнего уровня
  • лямбда-выражения или функциональные литералы
  • функции-члены
  • анонимные функции
  • локальные или вложенные функции
  • инфиксные функции

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

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

  • Android SDK
    Как использовать API Google Cloud Vision в приложениях для Android
    Ашраф Хатхибелагал
  • Джава
    Шаблоны дизайна Android: шаблон наблюдателя
  • Android SDK
    Добавление анимации на основе физики в приложения для Android
    Ашраф Хатхибелагал
  • Android SDK
    Android O: проверка номера телефона с помощью SMS-токенов
  • Android SDK
    Что такое Android Instant Apps?
    Джессика Торнсби