Хитрости и приемы эффективного программирования на Kotlin

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

Функции области видимости

Их цель  —  дать блоку кода контекст объекта. Таких функций пять: let, run, with, apply и also.

Применение

let: для вызова одной и более функций в результатах цепочек вызовов.

Without let
val basicSalary = getBasicSalary()
calculateHRA(basicSalary)
calculateDA(basicSalary)
calculateTA(basicSalary)
With let
getBasicSalary().let{
calculateHRA(it)
calculateDA(it)
calculateTA(it)
}

apply: при обращении к нескольким свойствам объекта.

Without apply
val user = User()
user.name = "Simform"
user.email = "[email protected]"
With apply
val user = User().apply{
name = "Simform"
email = "[email protected]"
}

with: для вызова нескольких методов в одном объекте.

Without
calculateHRA(basicSalary)
calculatePF(basicSalary)
Using with
with(basicSalary){
calculateHRA(this)
calculatePF(this)
}

run: для инициализации объекта и вычисления возвращаемого значения.

val user = User().run{
name = "Simform"
formatAddress()
}

also: для обращений к объекту, а не к его свойствам и функциям, или во избежание затенения обращения к this из внешней области видимости.

val numbers = mutableListOf("one", "two", "three")
numbers.also {
println("The list elements before adding new one:$it")
}
.add("four")

Класс данных (POJO/POCO):

data class User(val name:String, val email:String)

По умолчанию дает такую функциональность:
1. Геттеры (и сеттеры в случае с var переменными) для всех свойств.
2. equals().
3. hashCode().
4. toString().
5. copy().
6. component1(), component2(), …, для всех свойств (см. Data classes).

Параметры по умолчанию

Задают параметру функции значения по умолчанию. Если второй параметр не передается, функция принимает значение по умолчанию 0. Либо использует передаваемое значение, если таковое имеется.

Так становится возможной перегрузка методов без необходимости писать несколько их сигнатур для разного количества аргументов.

fun add(var one: Int, var two: Int = 0)

Функции-расширения

Определяются для конкретных типов данных и вызываются аналогично функциям-членам с оператором (.).

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

Позволяют добавлять больше функций в классы, к которым нет доступа, например встроенные классы (Int, String, ArrayList и т. д.). В Kotlin много встроенных функций-расширений (toString(), filter(), map(), toInt() и т. д.).

fun Int.returnSquare(): Int { return this * this }

// Вызов функции-расширения
val number = 5
println("Square is ${number.returnSquare()}")   // вывод: 25

Примечание: ключевое слово this указывает на объект получателя (в данном случае Int).

Оператор безопасного вызова (?.)

Применяется:

  1. В переменных типа, допускающего значение null, для безопасного их использования в коде и без nullPoiterException .
val files = getFiles()
print(files?.size) // Выводит размер, когда файлы не «null»

2. Вместе с let для вызова нескольких методов или выполнения операторов, когда переменная или выражение не null.

val user = getUser()
user?.let{
// Выполняется, когда пользователь не «null»
saveUserToDB(it)
}

Оператор Элвиса (?:)

Without 
if(user.name != null) {
userName = user.name
} else {
throw IllegalStateException()
}
With
val userName = user.name?: throw IllegalStateException()

run: несколько операторов выполняется, когда значение null.

user.name?.let{
// Выполняется, когда не «null»
} : run {
// Выполняется, когда «null»
}

Функции одного выражения

Просто возвращают значения:

fun isOdd(number:Int) = if(number%2 ==0) false else true

when: похожа на switch из Java, но более гибкая.

when(number:Int) {
5 -> "Greater than five"
in 6..10 -> "In range of 6 to 10"
else -> "This is else"
}

Оператор интервалов (..)

for( i in 1..10) // от 1 до 10
for( i in 1 until 10) // от 1 до 9 (10 не входит)
for( i in 1..10 step 2) // 1,3,5,7,9
for( i in 10 downTo 1) // 10,9,8....,1

Проверка экземпляра с помощью оператора (is)

if(10 is Int)  // true
if(10 is Boolean) // false
if("string" !is Int) // true

Лямбда-функции

Это анонимные функции, рассматриваемые как значения. Передаются в качестве аргументов и сохраняются как переменные.

{argumentName: argumentType -> // тело лямбды}

Имя и тип аргумента обычно опускают. А вот тело лямбды указывается обязательно. Тип последней его строки  —  возвращаемый тип лямбды.

val double: (Int)-> Int = {number:Int -> number * number}
println(double(5)) // вывод: 25

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

Классные встроенные функции Kotlin

filter: отфильтровывает list, set или map с заданным предикатом и возвращает их с соответствующими ему элементами. Смотрите все варианты filter здесь.

val numbers = listOf(1,2,3,4,5)

numbers.filter {element-> element%2 == 0 } // список вывода [2,4]

numbers.filterIndexed{index,element->(index != 0) && (element< 5)}
// [2,3,4]

numbers.filterNot {element-> element <= 3 } // [4,5]

map: применяет заданный предикат или функцию преобразования к каждому элементу коллекции и возвращает новую коллекцию.

val numbers = listOf(1,2,3,4,5)

numbers.map { it * 3 } // список вывода [3,6,9,12,15]

numbers.mapIndexed { index,value -> value * index } // [0,2,6,12,20]

// следующие функции используются для получения значений, отличных от «null»

numbers.mapNotNull {value-> if ( value == 2) null else value * 3 }
// [3,9,12,15]

numbers.mapIndexedNotNull { 
    index, value -> if (index == 0) null else value * index 
}
// [2,6,12,20]

val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11)

numbersMap.mapKeys { it.key.uppercase() } //{KEY1=1,KEY2=2,KEY3=3,KEY11=11}

println(numbersMap.mapValues { it.value + it.key.length })
//{key1=5, key2=6, key3=7, key11=16}

zip: создает список пар с элементами одного индекса из двух заданных списков.

val colors = listOf("red", "brown", "grey")
val animals = listOf("fox", "bear", "wolf")
println(colors.zip(animals)) 

// Вывод:   [(red,fox),(brown,bear),(grey,wolf)]

unzip создает из списка пар два списка:

val numberPairs = listOf("one" to 1, "two" to 2, "three" to 3, "four" to 4)
println(numberPairs.unzip())

// Вывод: ([one, two, three, four], [1, 2, 3, 4])

joinToString(): создает строку со всеми добавленными элементами.
joinTo(): то же и добавляет строку к заданной строке в аргументе.

val numbers = listOf("one", "two", "three", "four")
// Вывод: one, two, three, four

val listString = StringBuffer("The list of numbers: ")
numbers.joinTo(listString)
// Вывод: список чисел: one, two, three, four

flatten(): создает из списка списков один список.

val listOfList = [[1,2],[3,4,5],[6,7]]
println(listOfList.flatten()) // Вывод: [1,2,3,4,5,6,7]

any: принимает лямбду и проверяет, соответствует ли заданный предикат в ней любому из элементов списка. Если да  —  возвращает true, нет  —  тогда false.
all: если заданный предикат соответствует всем элементам коллекции  —  возвращает true, нет  —  тогда false.
none: если заданный предикат не соответствует ни одному из элементов коллекции  —  возвращает true, соответствует  —  тогда false.

val numbers = listOf("one", "two", "three", "four")

numbers.any { it.endsWith("e") } // Вывод: true
numbers.none { it.endsWith("a") }  // true
numbers.all { it.endsWith("e") } // false

partition: возвращает пару списков (один с элементами, соответствующими условию, а другой  —  с несоответствующими).
slice: создает список с заданным индексом.
chunked: тоже создает список списков, но с заданным размером.

val numbers = listOf("one", "two", "three", "four")

numbers.partition { it.length > 3 } 
// (["one","two"],["three","four"])

numbers.slice(1..3)  // ["two","three","four"]

numbers.chuncked(2) // [["one","two"],["three", "four"]]

take: получает указанное количество элементов, начиная с первого.
takeLast: то же, начиная с последнего.
drop: берет все элементы, кроме заданного количества первых элементов.
dropLast: то же, кроме заданного количества последних элементов. Больше методов см. здесь.

val numbers = listOf("one", "two", "three", "four", "five", "six")
println(numbers.take(3)) // [one, two, three]
println(numbers.takeLast(3)) //[four, five, six]
println(numbers.drop(1)) // [two, three, four, five, six]
println(numbers.dropLast(5)) // [one]

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

val numbers = listOf("one", "two", "three", "four", "five")

println(numbers.groupBy { it.first().uppercase() })

// Вывод: {O=[one], T=[two, three], F=[four, five]}

average: возвращает среднее всех элементов списка.
sum: возвращает их сумму.
count: возвращает их количество.
minOrNull: возвращает минимальное значение списка, а в случае пустого списка  —  значение NULL.
maxOrNull: то же самое, но вместо минимального значения возвращается максимальное.

val numbers = listOf(6, 42, 10, 4)

println("Count: ${numbers.count()}")    // Вывод: 4
println("Max: ${numbers.maxOrNull()}")  //42
println("Min: ${numbers.minOrNull()}")  //4
println("Average: ${numbers.average()}")//15.5
println("Sum: ${numbers.sum()}")        //62

Есть также функции, относящиеся к коллекциям:


1. Спискам.
2. Множествам.
3. Картам.

Функции для избежания ошибки indexOutOfBound

elementAtOrNull(): возвращает значение null, когда указанная позиция за пределами коллекции.
elementAtOrElse(): возвращает результат лямбды в заданном значении, когда указанная позиция за пределами коллекции.

Функции для избежания ошибки numberFormatException

toIntOrNull(): преобразует строку в целое число и возвращает null, когда возникает исключение.
toDoubleOrNull(): то же, но преобразует в число двойной точности.
toFloatOrNull(): то же, но преобразует в число с плавающей запятой.

Примечание: во избежание появления NullPointerException (исключения нулевого указателя) нужно самостоятельно обрабатывать значения null.

Заключение

Это был краткий обзор функционала Kotlin, и кое-что в него не вошло. Другие крутые функции здесь

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Abhishek Ippakayal: Kotlin Tips and Tricks for Efficient Programming

Предыдущая статьяВыбор между SQL и NoSQL: ACID и CAP, схема и транзакции
Следующая статьяУспешный релиз ПО: распространенные ошибки перед запуском продукта