На этот раз мы рассмотрим функции Kotlin, которые часто упускают из виду в пошаговых руководствах или в других местах, но все равно крутые и могут быть полезными! Некоторые из нас могут знать о некоторых особенностях, но часто забывают о них. О существовании других функций мы забываем до тех пор, пока о них не упоминают, вероятно, потому, что мы не используем их так часто (или очень редко). Надеюсь, вы увидите хотя бы одну функцию, о существовании которой забыли!
Встроенные объекты
Наиболее известно использование встроенных/анонимных объектов в Kotlin в местах, где нам нужно отправить объект (в смысле этого слова в Java), который реализует интерфейс, конкретный объект нужен только один раз в вашем коде, а лямбда-выражение будет недостаточно (потому что интерфейс не функционален, т.е. имеет несколько методов). Вероятно, это было немного запутанным, если вы не видели этого раньше. Трудно найти хорошие примеры этого, так как это происходит не так часто (вероятно, чаще происходит на Android с его обработчиками кликов и многим другим). Давайте просто используем Runnable в качестве быстрого примера, хотя вы могли бы легко использовать лямбду:
val myThread = Thread(object : Runnable {
override fun run() {
println("Using an anonymous inner class/object")
}
})
Теперь вы можете спросить: как это забытая функция? Не похоже, что это редкость! Вы правы, поэтому я начал этот раздел с «наиболее известного использования…». Теперь к возможности, о которой мы часто забываем. На самом деле вы можете создавать переменные, которые являются просто объектами, почти так же, как вы привыкли в JavaScript!
val myObj = object {
val name = "somename"
val pair = getRandomPoint()
val somelist = listOf("Element", "Another element")
}
println(myObj.name)
println(myObj.pair)
println(myObj.somelist)
У вас также могут быть функции, но если вы не реализуете интерфейс, эти методы не будут видны вне объекта. Исключением из этого правила являются методы, которые вы найдете в классе Any, которые вы также можете переопределить внутри встроенного/анонимного объекта (например, toString). Есть и несколько других ограничений. Одним из неприятных моментов было то, что я не мог деструктурировать приведенную выше пару и получить доступ к элементам:
val myObj = object {
val (x,y) = getRandomPoint()
}
// NOT ALLOWED! :(
println(myObj.x)
println(myObj.y)
Это может быть исправлено в будущих версиях Kotlin. Вы также не можете использовать встроенные объекты на верхнем уровне в файле, и они должны использоваться внутри функции или класса. (по крайней мере, из моего опыта работы со сценариями с использованием KScript).
Теперь вы можете спросить: Где эта последняя функция полезна? Это может быть полезно в качестве возвращаемых значений в закрытых методах классов или в качестве рабочего хранилища переменных для их группировки. Я думаю, что в целом нам следует избегать этого для больших проектов, так как это может немного ухудшить читаемость по мере роста проекта. С другой стороны, для скриптов на Kotlin, я думаю, может оказаться полезным сохранять лаконичность кода и создавать логические группы. Вот где я вижу самое большое применение для него.
хвост
Начнем с простейшей рекурсивной реализации факториала:
fun factorial(n : Int) : Int {
if(n <= 0) {
return 1
}
return n*factorial(n-1)
}
Помните, что каждый раз, когда выполняется рекурсивный вызов, создается новая страница стека со значениями переменных (здесь новое значение n), что требует больше памяти. В приведенной выше реализации мы видим, что таких новых страниц стека будет много в зависимости от значения n. Нам также нужно вернуться после завершения рекурсивных вызовов (т. е. достичь базового случая в if-check) и вычислить умножения.
Как мы можем улучшить? С концепцией хвостовой рекурсии. Что такое хвостовая рекурсия? Хвостовая рекурсия — это оптимизация компилятора (или на других языках, таких как Scheme, оптимизация интерпретатора), которая выполняется для повторного использования страницы стека каждый раз и позволяет избежать обратного отслеживания, необходимого для классических рекурсивных функций. При выполнении этих условий вы получаете уже не рекурсивный процесс, а итеративный (как и для обычных циклов). Звучит потрясающе, верно? Вам НЕОБХОДИМО написать свои функции таким образом, чтобы они не нуждались в возврате, чтобы это работало, поэтому большая ответственность лежит на ВАС! После того, как вы написали свою функцию таким образом, вы можете использовать ключевое слово tailrec. Один из способов реализовать факториал таким образом — использовать необязательный аргумент со значением по умолчанию (возможно, написать в документации, как это работает, особенно если он предназначен для использования другими!):
tailrec fun factorial(n : Int, sum : Int = 1) : Int {
if(n <= 0) {
return sum
}
return factorial(n-1, sum*n)
}
Это только один из возможных способов, и вы, вероятно, можете найти несколько лучших способов решения этой проблемы.
Вы можете задаться вопросом; что произойдет, если я использую ключевое слово tailrec без выполнения приведенного выше условия, поскольку последний вызов является хвостовым вызовом (т. Е. Откат не требуется). Вы получите предупреждение компилятора о том, что рекурсивный вызов не является хвостовым вызовом, вот так просто :)
Функции расширения для лямбда-выражений
Лямбды — это экземпляры классов за кулисами, а это значит, что вы можете создавать на них функции расширения! Если вы никогда не задумывались об этом, это может показаться запутанным. Как будет выглядеть имя класса? Давайте создадим один тип лямбда (принимая два целых числа и возвращая новое целое число), а затем создадим для него функцию расширения. Сначала простая функция добавления:
val add : (Int,Int) -> (Int) = { num1,num2 ->
num1 + num2
}
Внимательно посмотрите на тип надстройки. Это класс, для которого мы можем создавать функции расширения! Давайте создадим новую функцию расширения, которая возвращает ту же функцию, но выполнение начнется с простого оператора печати:
fun ((Int,Int) -> (Int)).logged() : ((Int,Int) -> (Int)) {
return { num1,num2 ->
println("Calling function ${this.toString()}")
this(num1,num2)
}
}
Теперь мы можем увидеть его в использовании:
var addLogged = add.logged()
println("2+3 = ${addLogged(2,3)}")
Как вы можете догадаться, вывод будет таким:
Calling function (kotlin.Int, kotlin.Int) -> kotlin.Int 2+3 = 5
Это был очень простой пример, но с этой функциональностью можно делать и более забавные вещи. Может быть, вы хотите создать мемоизированную версию вашей функции? Или создать композиции функций (например, создать функцию f(g(x)) для двух функций f и g)? Оглянитесь вокруг, особенно в области функционального программирования, и вы можете найти более интересные варианты использования.
Делегирование свойств/переменных
Резюме делегирования в классах
Большинство людей знают о делегировании на уровне класса, но если вы этого не знаете, давайте сделаем очень краткий обзор. При расширении класса или реализации интерфейса связь имеет тип «является» (например, Toyota — это автомобиль, сибирский хаски — это собака и т. д.). Композиция, хранящая отношения в переменной, относится к типу «имеет» (например, менеджер управляет/заставляет работать программиста, у кошки есть человек, который дает им еду и т. д.). Делегирование — это особый случай композиции, когда мы делегируем работу другому классу. Вы, вероятно, не хотите, чтобы ваш Кот реализовал человеческий интерфейс только для того, чтобы иметь возможность получать еду…? Вы хотите делегировать работу слуге/владельцу кошки. Как это выглядит в Котлине?
interface Human {
// usually the implementation would be implementation specific
fun getFood() {
println("Getting food")
}
}
// implementations that in the real world probably implements their own getFood
class GrownUp : Human
class Cat(val name : String, servant : Human) : Human by servant
// usage
val me = GrownUp()
val myCat = Cat("Mittens", me)
myCat.getFood()
Это выглядит так же, как Cat реализует интерфейс Human, но за кулисами делегирует работу человеку. В этом сила делегирования для классов, и поэтому его часто называют специальной формой композиции (поскольку использование немного отличается от базовой композиции).
NB! Просто для ясности. Приведенный выше пример очень прост, чтобы проиллюстрировать это. Если вы все еще не уверены в этом и хотите увидеть больше примеров, я предлагаю заглянуть в официальную документацию. Это было предназначено только для подведения итогов, чтобы подготовить вас к главному, поэтому я предположил, что вам просто нужно освежить в памяти :)
свойства/переменные
Приведенный выше пример делегирования, вероятно, был вам знаком, так как он четко представлен во многих текстах Kotlin. Знаете ли вы, что вы также можете использовать делегаты для свойств/переменных? Это простой способ добавить некоторую дополнительную функциональность к данному типу. Допустим, вам нужна переменная с числом, но она может быть только четной. Или строка, которая может быть только в нижнем регистре. Давайте посмотрим, как можно реализовать такой глупый пример:
// Even number
class EvenNumber(private var num : Int) {
operator fun getValue(thisRef : Any?, prop : KProperty<*>) : Int {
return num
}
operator fun setValue(thisRef: Any?, prop : KProperty<*>, newValue : Int) {
if(newValue % 2 == 0) {
num = newValue
} else {
num = newValue - 1
}
}
}
var evenNum : Int by EvenNumber(23)
println(evenNum)
evenNum = 11
println(evenNum)
evenNum = 2
println(evenNum)
// string always lower case
class LowerCaseString(private var str : String) {
operator fun getValue(thisRef : Any?, prop : KProperty<*>) : String {
return str
}
operator fun setValue(thisRef: Any?, prop : KProperty<*>, newValue : String) {
str = newValue.lowercase()
}
}
var myStr : String by LowerCaseString("Hello there")
println(myStr)
myStr = "hi"
println(myStr)
myStr = "MY HANDS ARE TYPING WORDS"
println(myStr)
Вы можете заметить, что делегатам не нужно реализовывать интерфейс, но им все равно нужно реализовывать вышеуказанные методы (значения/валы, вероятно, не нуждаются в сеттере).
Теперь давайте посмотрим, как выглядит вывод:
23 10 2 Hello there hi my hands are typing words
NB! Вы можете заметить, что setValue не вызывается в конструкторе, и я хочу, чтобы вы были осторожны. Если вы хотите применить его в этот момент, я бы предложил использовать явный конструктор или блок инициализации.
Эти примеры намеренно немного глупы, но они показывают несколько простых вариантов использования проверки для делегатов. Теперь, когда вы знаете, как они работают, вы, вероятно, можете придумать более забавные вещи для их использования :)
Похвальный отзыв
Ссылочное равенство с использованием ===
Допустим, вы реализовали метод equals, и он проверяет структурное равенство (например, поля равны друг другу). Что, если вы хотите проверить, что это один и тот же объект, нам нужно удалить метод equals? НЕТ! Вместо этого вы можете использовать ===, и вы проверяете, являются ли объекты одними и теми же объектами в памяти (т.е. одной и той же ссылкой).