Источники
• Официальный сайт языка Kotlin (англ.)
• Исходный код компилятора на GitHub
• Исходники англ. документации
• Раздел на reddit.com, посвященный языку Kotlin
Сообщество
• @KotlinLangRu - Telegram чат посвященный языку Kotlin и переводу документации
• @kotlin_lang - Сообщество разработчиков на Kotlin
Локальные группы:
Актуальные задачи
• Перевести статьи из радела "Java Interop": Calling Java from Kotlin, Calling Kotlin from
Java
• Перевести статьи из радела "FAQ": FAQ
• Перевести статьи из раздела "Tools": KDoc, Kapt, Gradle, Maven, Ant, OSGi, Compiler
Plugins
• Пометить ключевые слова конструкцией <b class="keyword">keyword-
here</b>
Основной синтаксис
Определение имени пакета
Имя пакета указывается в начале исходного файла, так же как и в Java:
package my.demo
import java.util.*
// ...
См. Пакеты.
Объявление функции
Функция принимает два аргумента Int и возвращает Int:
return a + b
print(a + b)
print(a + b)
См. Функции.
val a: Int = 1
Изменяемая переменная:
x += 1
Комментарии
Также, как Java и JavaScript, Kotlin поддерживает однострочные комментарии.
// однострочный комментарий
/* Блочный комментарий
из нескольких строк. */
if (args.size == 0) return
if (a > b)
return a
else
return b
}
Также if может быть использовано как выражение (т. е. if ... else возвращает значение):
// ...
if (args.size < 2) {
return
val x = parseInt(args[0])
val y = parseInt(args[1])
print(x * y)
или
// ...
if (x == null) {
if (y == null) {
return
print(x * y)
См. Null-безопасность.
if (obj is String) {
return obj.length
return null
или
return null
return obj.length
}
или даже
return obj.length
return null
print(arg)
или
for (i in args.indices)
print(args[i])
var i = 0
print(args[i++])
1 -> print("One")
Использование интервалов
Проверка на вхождение числа в интервал с помощью оператора in:
if (x in 1..y-1)
print("OK")
if (x !in 0..array.lastIndex)
print("Out")
Итерация по интервалу:
for (x in 1..5)
print(x)
print(x)
print(x)
См. Интервалы.
Использование коллекций
Итерация по коллекции:
for (name in names)
println(name)
when {
names
.filter { it.startsWith("A") }
.sortedBy { it }
.map { it.toUpperCase() }
.forEach { print(it) }
Идиомы
Набор различных часто используемых идиом в языке Kotlin. Если у вас есть любимая
идиома, вы можете поделится ею здесь. Для этого нужно выполнить pull request.
Или короче:
Форматирование строк
println("Name $name")
Read-only список
val list = listOf("a", "b", "c")
map["key"] = value
Ленивые свойства
val p: String by lazy {
Функции-расширения
fun String.spaceToCamelCase() { ... }
Создание синглтона
object Resource {
println(files?.size)
println(files?.size ?: "empty")
data?.let {
"Red" -> 0
"Green" -> 1
"Blue" -> 2
count()
throw IllegalStateException(e)
"one"
} else if (param == 2) {
"two"
} else {
"three"
return 42
"Red" -> 0
"Green" -> 1
"Blue" -> 2
fun penDown()
fun penUp()
penDown()
for(i in 1..4) {
forward(100.0)
turn(90.0)
penUp()
println(reader.readText())
Правила наименований
При возникновении сомнений по умолчанию используются следующие правила:
Двоеточие
В тех случаях, когда двоеточие разделяет тип и подтип, перед двоеточием ставится
пробел. Если же двоеточие ставится между сущностью и типом, то пробел опускается:
interface Foo<out T : Any> : Bar {
Лямбда-выражения
В лямбда-выражениях фигурные скобки, а также стрелка и параметры отделяются
пробелами. Желательно передавать лямбду за пределами скобок.
Объявление классов
Классы с небольшим количеством аргументов можно писать на одной строчке:
class Person(
id: Int,
name: String,
surname: String
) : Human(id, name) {
// ...
class Person(
id: Int,
name: String,
surname: String
) : Human(id, name),
KotlinMaker {
// ...
Для параметров конструктора может использоваться как обычный отступ, так и двойной.
Тип Unit
Если возвращаемым типом является Unit, то его можно явно не указывать:
Функции vs Свойства
В некоторых случаях, функции без аргументов могут быть взаимозаменяемы с
неизменяемыми (read-only) свойствами. Несмотря на схожую семантику, есть некоторые
стилистические соглашения, указывающие на то, когда лучше использовать одно из этих
решений.
• не выбрасывает исключений
• имеет O(1) сложность
• не требует больших затрат на выполнение (или результат вычислений кэшируется
при первом вызове)
• возвращает одинаковый результат
Основные типы
В Kotlin всё является объектом, в том смысле, что пользователь может вызвать функцию
или получить доступ к свойству любой переменной. Некоторые типы являются
встроенными, т.к. их реализация оптимизирована, хотя для пользователя они выглядеть
как обычные классы. В данном разделе описывается большинство этих типов: числа,
символы, логические переменные и массивы.
Числа
Kotlin обрабатывает численные типы примерно так же, как и Java, хотя некоторые
различия всё же присутствуют. Например, отсутствует неявное расширяющее
преобразование для чисел, а литералы в некоторых случаях немного отличаются.
Double 64
Float 32
Long 64
Int 32
Short 16
Byte 8
Символьные постоянные
В языке Kotlin присутствуют следующие виды символьных постоянных (констант) для
целых значений:
Представление
Обычно платформа Java хранит числа в виде примитивных типов JVM; если же нам
необходима ссылка, которая может принимать значение null (например, Int?), то
используются обёртки. В приведённом ниже примере показано использование обёрток.
Явные преобразования
Из-за разницы в представлениях меньшие типы не являются подтипами бОльших типов.
В противном случае у нас возникли бы сложности:
• toByte(): Byte
• toShort(): Short
• toInt(): Int
• toLong(): Long
• toFloat(): Float
• toDouble(): Double
• toChar(): Char
Арифметические действия
Kotlin поддерживает обычный набор арифметических действий над числами, которые
объявлены членами соответствующего класса (тем не менее, компилятор оптимизирует
вызовы вплоть до соответствующих инструкций). См. Перегрузка операторов.
Что касается битовых операций, то вместо особых обозначений для них используются
именованные функции, которые могут быть вызваны в инфиксной форме, к примеру:
Символы
Символы в Kotlin представлены типом Char. Напрямую они не могут рассматриваться в
качестве чисел
// ...
if (c !in '0'..'9')
throw IllegalArgumentException("Вне диапазона")
Логический тип
Тип Boolean представляет логический тип данных и принимает два
значения: true и false.
Массивы
Массивы в Kotlin представлены классом Array, обладающим
функциями get и set (которые обозначаются [] согласно соглашению о перегрузке
операторов), и свойством size, а также несколькими полезными встроенными
функциями:
// ...
// создаёт массив типа Array<String> со значениями ["0", "1", "4", "9", "1
6"]
Также в Kotlin есть особые классы для представления массивов примитивных типов без
дополнительных затрат на оборачивание: ByteArray, ShortArray, IntArray и т.д.
Данные классы не наследуют класс Array, хотя и обладают тем же набором методов и
свойств. У каждого из них есть соответствующая фабричная функция:
Строки
Строки в Kotlin представлены типом String. Строки являются неизменяемыми. Строки
состоят из символов, которые могут быть получены по порядковому номеру: s[i].
Проход по строке выполняется циклом for:
for (c in str) {
println(c)
Строковые литералы
В Kotlin представлены два типа строковых литералов: строки с экранированными
символами и обычные строки, могущие содержать символы новой строки и
произвольный текст. Экранированная строка очень похожа на строку в Java:
print(c)
"""
Строковые шаблоны
Строки могут содержать шаблонные выражения, т.е. участки кода, которые выполняются,
а полученный результат встраивается в строку. Шаблон начинается со знака доллара ($) и
состоит либо из простого имени (например, переменной):
val i = 10
val s = "abc"
Пакеты
Файл с исходным кодом может начинаться с объявления пакета:
package foo.bar
fun baz() {}
class Goo {}
// ...
Импорт
Помимо импорта по умолчанию каждый файл может содержать свои собственные
объявления импорта. Синтаксис импорта описан а разделе Грамматика.
При совпадении имён мы можем разрешить коллизию используя ключевое слово as для
локального переименования совпадающей сущности:
Управляющие инструкции
Условное выражение if
В языке Kotlin ключевое слово if является выражением, т.е. оно возвращает значение. Это
позволяет отказаться от тернарного оператора (условие ? условие истинно : условие
ложно), поскольку выражению if вполне по силам его заменить.
// обычное использование
var max = a
if (a < b)
max = b
// с блоком else
if (a > b)
max = a
else
max = b
// в виде выражения
"Ветви" выражения if могут содержать несколько строк кода, при этом последнее
выражение является значением блока:
print("возвращаем a")
else {
print("возвращаем b")
when (x) {
Значение ветки else вычисляется в том случае, когда ни одно из условий в других ветках
не удовлетворено. Если when используется как выражение, то ветка else является
обязательной, за исключением случаев, в которых компилятор может убедиться, что
ветки покрывают все возможные значения.
when (x) {
when (x) {
Также можно проверять вхождение аргумента в интервал in или !in или его наличие в
коллекции:
when (x) {
Помимо этого Кotlin позволяет с момощью is или !is проверить тип аргумента. Обратите
внимание, что благодаря умным приведениям вы можете получить доступ к методам и
свойствам типа без дополнительной проверки:
when удобно использовать вместо цепочки условий вида if-else if. При отстутствии
аргумента условия работают как простые логические выражения, а тело ветки
выполняется при его истинности:
when {
Циклы for
Цикл for обеспечивает перебор всех значений, поставляемых итератором. Для этого
используется следующий синтаксис:
print(item)
// ...
Как отмечено выше, цикл for позволяет проходить по всем элементам объекта,
имеющего итератор, например:
Если при проходе по массиву или списку необходим порядковый номер элемента,
используйте следующий подход:
for (i in array.indices)
print(array[i])
Обратите внимание, что данная "итерация по ряду" компилируется в более
производительный код без создания дополнительных объектов.
Циклы while
Ключевые слова while и do..while работают как обычно:
while (x > 0) {
x--
do {
val y = retrieveData()
Операторы перехода
В Kotlin определено три оператора перехода:
// ...
Теперь мы можем уточнить значения операторов break или continue с помощью меток:
for (j in 1..100) {
if (...)
break@loop
Оператор break, отмеченный @loop, переводит выполнение кода к той его части,
которая находится сразу после соответствующей метки loop@.
Оператор continue продолжает цикл со следующей его итерации.
Возврат к меткам
В Kotlin функции могут быть вложены друг в друга с помощью анонимных объектов,
локальных функций (ориг.:local functions) и function literals. Подходящий return позволит
вернуться из внешней функции. Одним из самых удачных применений этой
синтаксической конструкции служит возврат из лямбда-выражения. Подумайте над этим
утверждением, читая данный пример:
fun foo() {
ints.forEach {
if (it == 0) return
print(it)
fun foo() {
ints.forEach lit@ {
if (it == 0) return@lit
print(it)
fun foo() {
ints.forEach {
if (it == 0) return@forEach
print(it)
fun foo() {
ints.forEach(fun(value: Int) {
if (value == 0) return
print(value)
})
return@a 1
что значит "верни 1 в метке @a, а не "верни выражение с меткой (@a 1)".
Классы и наследование
Классы
Классы в Kotlin объявляются с помощью использования ключевого слова class:
class Invoice {
Объявление класса состоит из имени класса, заголовка (указания типов его параметров,
первичного конструктора и т.п) и тела класса, заключённого в фигурные скобки. И
заголовок, и тело класса являются необязательными составляющими: если у класса нет
тела, фигурные скобки могут быть опущены.
class Empty
Конструкторы
Класс в Kotlin может иметь первичный конструктор (primary constructor) и один или
более вторичных конструкторов (secondary constructors). Первичный конструктор
является частью заголовка класса, его объявление идёт сразу после имени класса (и
необязательных параметров):
init {
class Person(val firstName: String, val lastName: String, var age: Int) {
// ...
}
Второстепенные конструкторы
В классах также могут быть объявлены дополнительные конструкторы (secondary
constructors), перед которыми используется ключевое слово constructor:
class Person {
constructor(parent: Person) {
parent.children.add(this)
parent.children.add(this)
Обращаем ваше внимание на то, что в Kotlin не используется ключевое слово new.
Члены класса
Классы могут содержать в себе:
Наследование
Для всех классов в языке Koltin родительским суперклассом является класс Any. Он также
является родительским классом для любого класса, в котором не указан какой-либо
другой родительский класс:
Если у класса есть основной конструктор, базовый тип может (и должен) быть
проинициализирован там же, с использованием параметров первичного конструктора.
Ключевое слово open является противоположностью слову final в Java: оно позволяет
другим классам наследоваться от данного. По умолчанию, все классы в Kotlin имеют
статус final, что отвечает Effective Java, Item 17: Design and document for inheritance or else
prohibit it.
fun nv() {}
Член класса, помеченный override, является сам по себе open, т.е. он может быть
переопределён в производных классах. Если вы хотите запретить возможность
переопределения такого члена, используйте final:
}
Стойте! Как мне теперь хакнуть свои библиотеки?
При нашем подходе к переопределению классов и их членов (которые по дефолту final)
будет сложно унаследоваться от чего-нибудь внутри используемых вами библиотек для
того, чтобы переопределить не предназначенный для этого метод и внедрить туда свой
гнусный хак.
• Опыт поколений говорит о том, что, в любом случае, лучше не позволять внедрять
такие хаки
• Люди успешно используют другие языки (C++, C#), которые имеют аналогичных
подход к этому вопросу
• Если кто-то действительно хочет хакнуть, пусть напишет свой код на Java и
вызовет его в Kotlin (см. Java-совместимость)
Правила переопределения
В Kotlin правила наследования имплементации определены следующим образом: если
класс перенимает большое количество имплементаций одного и того члена от
ближайших родительских классов, он должен переопределить этот член и обеспечить
свою собственную имплементацию (возможно, используя одну из унаследованных). Для
того, чтобы отметить супертип (родительский класс), от которого мы унаследовали
данную имплементацию, мы используем ключевое слово super. Для уточнения имя
родительского супертипа используются треугольные скобки, например super<Base>:
open class A {
interface B {
Абстрактные классы
Класс и некоторые его члены могут быть объявлены как abstract. Абстрактный член не
имеет реализации в его классе. Обратите внимание, что нам не надо аннотировать
абстрактный класс или функцию словом open - это подразумевается и так.
Объекты-помощники
В Kotlin, в отличие от Java или C#, в классах не бывает статических методов. В
большинстве случаев рекомендуется использовать функции на уровне пакета
(ориг.: "package-level functions").
Если вам нужно написать функцию, которая может быть использована без создания
экземпляра класса, но имела бы доступ к данным внутри этого класса (к примеру,
фабричный метод), вы можете написать её как член объявления объекта внутри этого
класса.
Прочие классы
Также обратите внимание на:
Свойства и поля
Объявление свойств
Классы в Kotlin могут иметь свойства: изменяемые (mutable) и неизменяемые (read-only)
— var и valсоответственно.
Для того, чтобы воспользоваться свойством, мы просто обращаемся к его имени (как
в Java):
result.street = address.street
// ...
return result
Геттеры и сеттеры
Полный синтаксис объявления свойства выглядит так:
[<getter>]
[<setter>]
Инициализатор property_initializer, геттер и сеттер можно не указывать. Также
необязательно указывать тип свойства, если он может быть выведен из контекста или
наследован от базового класса.
Примеры:
val simple: Int? // имеет тип Int, стандартный геттер, должен быть инициал
изирован в конструкторе
Мы можем самостоятельно описать методы доступа, как и обычные функции, прямо при
объявлении свойств. Например, пользовательский геттер:
get() = this.size == 0
get() = this.toString()
set(value) {
Если вам нужно изменить область видимости метода доступа или пометить его
аннотацией, при этом не внося изменения в реализацию по умолчанию, вы можете
объявить метод доступа без объявления его тела:
var counter = 0
set(value) {
Backing field будет сгенерировано для свойства, если оно использует стандартную
реализацию как минимум одного из методов доступа. Или в случае, когда
пользовательский метод доступа ссылается на него через идентификатор field.
get() = this.size == 0
Backing Properties
Если вы хотите предпринять что-то такое, что выходит за рамки вышеуказанной схемы
"неявного backing field", вы всегда можете использовать backing property:
get() {
if (_table == null) {
}
Такой подход ничем не отличается от подхода в Java, так как доступ к приватным
свойствам со стандартными геттерами и сеттерами оптимизируется таким образом, что
вызов функции не происходит.
subject = TestSubject()
Переопределение свойств
См. Переопределение членов класса
Делегированные свойства
Самый распространённый тип свойств просто считывает (или записывает) данные
из backing field. Тем не менее, с пользовательскими геттерами и сеттерами мы можем
реализовать совершенно любое поведение свойства. В реальности, существуют
общепринятые шаблоны того, как могут работать свойства. Несколько примеров:
Интерфейсы
Интерфейсы в Kotlin очень похожи на интерфейсы в Java 8. Они могут содержать
абстрактные методы, методы с реализацией. Главное отличие интерфейсов от
абстрактных классов заключается в невозможности хранения переменных экземпляров.
Они могут иметь свойства, но те должны быть либо абстрактными, либо предоставлять
реализацию методов доступа.
interface MyInterface {
fun bar()
fun foo() {
// необязательное тело
}
}
Реализация интерфейсов
Класс или объект могут реализовать любое количество интерфейсов:
// тело
Свойства в интерфейсах
Вы можете объявлять свойства в интерфейсах. Свойство, объявленное в интерфейсе,
может быть либо абстрактным, либо иметь свою реализацию методов доступа. Свойства
в интерфейсах не могут иметь backing fields, соответственно, методы доступа к таким
свойствам не могут обращаться к backing fields.
interface MyInterface {
get() = "foo"
fun foo() {
print(prop)
interface A {
fun bar()
interface B {
class C : A {
class D : A, B {
super<A>.foo()
super<B>.foo()
Модификаторы доступа
Классы, объекты, интерфейсы, конструкторы, функции, свойства и их сеттеры могут
иметь модификаторы доступа (у геттеров всегда такая же видимость, как у свойств, к
которым они относятся). В Kotlinпредусмотрено четыре модификатора
доступа: private, protected, internal и public. Если явно не используется никакого
модификатора доступа, то по умолчанию применяется public.
Пакеты
Функции, свойства, классы, объекты и интерфейсы могут быть объявлены на самом
"высоком уровне" прямо внутри пакета:
package foo
fun baz() {}
class Bar {}
Примеры:
package foo
Классы и интерфейсы
Для членов, объявленых в классе:
• private означает видимость только внутри этого класса (включая его членов);
• protected --- то же самое, что и private + видимость в субклассах;
• internal --- любой клиент внутри модуля, который видит объявленный класс,
видит и его internal члены;
• public --- любой клиент, который видит объявленный класс, видит
его public члены.
Примеры:
private val a = 1
internal val c = 3
// a не видно
// b, c и d видно
Конструкторы
Для указания видимости главного конструктора класса используется следующий
синтаксис (кстати, надо добавить ключевое слово constructor):
Локальные объявления
Локальные переменные, функции и классы не могут иметь модификаторов доступа.
Модули
Модификатор доступа internal означает, что этот член видно в рамках его модуля.
Модуль - это набор скомпилированных вместе Kotlin файлов:
Расширения (extensions)
Аналогично таким языкам программирования, как C# и Gosu, Kotlin позволяет
расширять класс путём добавления нового функционала. Не наследуясь от такого класса
и не используя паттерн "Декоратор". Это реализовано с помощью специальных
выражений, называемых расширения. Kotlin поддерживает функции-
расширения и свойства-расширения.
Функции-расширения
Для того, чтобы объявить функцию-расширение, нам нужно указать в качестве
приставки возвращаемый тип, то есть тип, который мы расширяем. Следующий пример
добавляет функцию swap к MutableList<Int>:
this[index1] = this[index2]
this[index2] = tmp
}
val l = mutableListOf(1, 2, 3)
Разумеется, эта функция имеет смысл для любого MutableList<T>, и мы можем сделать
её обобщённой:
this[index1] = this[index2]
this[index2] = tmp
open class C
class D: C()
fun printFoo(c: C) {
println(c.foo())
}
printFoo(D())
Этот пример выведет нам "с" на экран потому, что вызванная функция-расширение
зависит только от объявленного параметризованного типа c, который
является C классом.
class C {
class C {
Свойства-расширения
Аналогично функциям, Kotlin поддерживает расширения свойств:
get() = size - 1
Так как расширения на самом деле не добавляют никаких членов к классам, свойство-
расширение не может иметь backing field. Вот почему запрещено использовать
инициализаторы для свойств-расширений. Их поведение может быть определено
только явным образом, с указанием геттеров/сеттеров.
Пример:
class MyClass {
fun MyClass.Companion.foo() {
// ...
Как и обычные члены вспомогательного объекта, они могут быть вызваны с помощью
имени класса в качестве точки доступа:
MyClass.foo()
package foo.bar
fun Baz.goo() { ... }
Для того, чтобы использовать такое расширение вне пакета, в котором оно было
объявлено, нам надо импортировать его на стороне вызова:
package com.example.usage
// или
baz.goo()
class D {
class C {
fun D.foo() {
fun caller(d: D) {
class C {
fun D.foo() {
open class D {
class D1 : D() {
open class C {
println("D.foo in C")
println("D1.foo in C")
}
fun caller(d: D) {
class C1 : C() {
println("D.foo in C1")
println("D1.foo in C1")
Мотивация
В Java мы привыкли к классам с названием "*Utils": FileUtils, StringUtils и т.п.
Довольно известным следствием этого является java.util.Collections. Но вот
использование таких утилитных классов в своём коде - не самое приятное мероприятие:
// Java
// Java
// Java
list.swap(list.binarySearch(otherList.max()), list.max())
Но мы же не хотим реализовывать все методы класса List, так? Вот для чего и нужны
расширения.
Классы данных
Нередко мы создаём классы, единственным назначением которых является хранение
данных. Функционал таких классов зависит от самих данных, которые в них хранятся.
В Kotlin класс может быть отмечен словом data:
Такой класс называется классом данных. Компилятор автоматически извлекает все члены
данного класса из свойств, объявленных в первичном конструкторе:
Если какая-либо из этих функций явно определена в теле класса (или унаследована от
родительского класса), то генерироваться она не будет.
Для того, чтобы поведение генерируемого кода соответствовало здравому смыслу, классы
данных должны быть оформлены с соблюдением некоторых требований:
Начиная с версии 1.1, классы данных могут расширять другие классы (см. примеры
в Sealed classes)
```
Копирование
Довольно часто нам приходится копировать объект с изменением только некоторых его
свойств. Для этой задачи генерируется функция copy(). Для написанного выше
класса User такая реализация будет выглядеть следующим образом:
Изолированные классы
Изолированные классы используются для отражения ограниченных иерархий классов,
когда значение может иметь тип только из ограниченного набора, и никакой другой. Они
являются, по сути, расширением enum-классов: набор значений enum типа также
ограничен, но каждая enum-константа существует только в единственном экземпляре, в
то время как наследник изолированного класса может иметь множество экземпляров,
которые могут нести в себе какое-то состояние.
(Пример выше использует одну новую возможность Kotlin 1.1: расширение классов,
включая изолированные, классами данных) Обратите внимание, что классы, которые
расширяют наследников изолированного класса (непрямые наследники) могут быть
помещены где угодно, не обязательно в том же файле.
Обобщения (Generics)
Как и в Java, в Kotlin классы тоже могут иметь generic типы:
class Box<T>(t: T) {
var value = t
Для того, чтобы создать объект такого класса, необходимо предоставить тип в качестве
аргумента:
Вариативность
Одним из самых сложных мест в системе типов Java являются маски (ориг. wildcards)
(см. Java Generics FAQ). А в Kotlin этого нет. Вместо этого, у нас есть две другие
вещи: вариативность на уровне объявленияи проекции типов.
Для начала давайте подумаем на тему, зачем Java нужны эти странные маски. Проблема
описана в книге Effective Java, Item 28: Use bounded wildcards to increase API
flexibility. Обобщающие типы в Java, прежде всего, неизменны. Это значит,
что List<String> не является подтипом List<Object>. Почему так? Если бы List был
изменяемым, единственно лучшим решением для следующей задачи был бы массив,
потому что после компиляции данный код вызвал бы ошибку в рантайме:
// Java
Таким образом, Java запрешает подобные вещи, гаранитируя тем самым безопасность в
период выполнения кода. Но у такого подхода есть свои последствия. Рассмотрим,
например, метод addAllинтерфейса Collection. Какова сигнатура данного метода?
Интуитивно мы бы указали её таким образом:
// Java
// Java
(В Java нам этот урок дорого стоил, см. Effective Java, Item 25: Prefer lists to arrays)
Вот почему сигнатура addAll() на самом деле такая:
// Java
Маска для аргумента ? extends T указвает на то, что это метод принимает коллекцию
объектов некого типа T, а не сам T. Это значит, что мы можем безопасно читать объекты
типа T из содержимого (элементы коллекции являются экземплярами подкласса T), но не
можем их изменять, потому что не знаем, какие объекты соответствуют этому
неизвестному типу T. Минуя это ограничение, мы достигаем желаемого
результата: Collection<String> является подтипом Collection<? extends
Object>. Выражаясь более "умными словами", маска с extends-связкой
(верхнее связывание) делает тип ковариантным (ориг. covariant).
Ключом к пониманию, почему этот трюк работает, является довольно простая мысль:
использование коллекции String'ов и чтение из неё Objectов нормально только в
случае, если вы берёте элементы из коллекции. Наоборот, если вы
только вносите элементы в коллекцию, то нормально брать коллекцию Object'ов и
помещать в неё Stringи: в Java есть List<? super String>, супертипList<Object>'a.
// Java
interface Source<T> {
T nextT();
// Java
// ...
Чтобы исправить это, нам нужно объявить объекты типа Source<? extends Object>,
что в каком-то роде бессмысленно, потому что мы можем вызывать у переменных только
те методы, что и ранее, стало быть более сложный тип не добавляет значения. Но
компилятор не знает этого.
// ...
Общее правило таково: когда параметр T класса С объявлен как out, он может
использоваться только в out-местах в членах C. Но зато C<Base> может быть
родителем C<Derived>, и это будет безопасно.
Мы верим, что слова in и out говорят сами за себя (так как они довольно успешно
используются в C# уже долгое время), таким образом, мнемоника, приведённая выше, не
так уж и нужна, и её можно перефразировать следущим образом:
Проекции типов
Вариативность на месте использования
Объявлять параметризованный тип T как out очень удобно: при его использовании не
будет никаких проблем с подтипами. И это действительно так в случае с классами,
которые могут быть ограничены на только возвращение T. А как быть с теми классами,
которые ещё и принимают T? Пример: класс Array
assert(from.size == to.size)
for (i in from.indices)
to[i] = from[i]
Тогда единственная вещь, в которой мы хотим удостовериться, это то, что copy() не
сделает ничего плохого. Мы хотим запретить методу записывать в from, и мы можем это
сделать:
// ...
// ...
"Звёздные" проекции
Иногда возникает ситуация, когда вы ничего не знаете о типе аргумента, но всё равно
хотите использовать его безопасным образом. Этой безопасности можно добиться путём
определения такой проекции параметризованного типа, при которой его экземпляр
будет подтипом этой проекции.
Kotlin предоставляет так называемый star-projection синтаксис для этого:
Обобщённые функции
Функции, как и классы, могут иметь типовые параметры. Типовые параметры
помещаются перед именем функции:
// ...
// ...
Для вызова обобщённой функции, укажите тип аргументов на месте вызова после имени
функции:
val l = singletonList<Int>(1)
Обобщённые ограничения
Набор всех возможных типов, которые могут быть переданы в качестве параметра,
может быть ограничен с помощью обобщённых ограничений.
Верхние границы
Самый распространённый тип ограничений - верхняя граница, которая соответствует
ключевому слову extends из Java:
// ...
По умолчанию (если не указана явно) верняя граница — Any?. Только одна верхняя
граница может быть указана в угловых скобках. В случае, если один параметризованный
тип требует больше чем одной верхней границы, нам нужно использовать
разделяющее where-условие:
where T : Comparable,
T : Cloneable {
Вложенные классы
Классы могут быть вложены в другие классы
class Outer {
class Nested {
fun foo() = 2
Внутренние классы
Класс может быть отмечен как внутренний с помощью слова inner, тем самым он будет
иметь доступ к членам внешнего класса. Внутренние классы содержат ссылку на объект
внешнего класса:
class Outer {
window.addMouseListener(object: MouseAdapter() {
// ...
// ...
})
Перечисляемые типы
Наиболее базовый пример использования enum — это реализация типобезопасных
перечислений
Инициализация
Так как константы являются экземплярами enum-класса, они могут быть
инициализированы
RED(0xFF0000),
GREEN(0x00FF00),
BLUE(0x0000FF)
Анонимные классы
Enum-константы также могут объявлять свои собственные анонимные классы
WAITING {
},
TALKING {
};
Работа с enum-константами
Так же как и в Java, enum-классы в Kotlin имеют стандартные методы для вывода списка
объявленных констант и для получения enum-константы по её имени. Ниже приведены
сигнатуры этих методов:
EnumClass.values(): Array<EnumClass>
Анонимные объекты и
объявление объектов
Иногда нам необходимо получить экземпляр некоторого класса с незначительной
модификацией, желательно без написания нового подкласса. Java справляется с этим с
помощью вложенных анонимных классов. Kotlin несколько улучшает данный подход.
window.addMouseListener(object : MouseAdapter() {
// ...
// ...
})
Если у супертипа есть конструктор, то в него должны быть переданы соответсвующие
параметры. Множество супертипов может быть указано после двоеточия в виде списка,
заполненного через запятую:
interface B {...}
override val y = 15
Если всё-таки нам нужен просто объект без всяких там родительских классов, то можем
указать:
var x: Int = 0
var y: Int = 0
print(adHoc.x + adHoc.y)
Код внутри объявленного объекта может обращаться к переменным за скобками так же,
как вложенные анонимные классы в Java
var clickCount = 0
var enterCount = 0
window.addMouseListener(object : MouseAdapter() {
clickCount++
enterCount++
}
})
// ...
object DataProviderManager {
// ...
get() = // ...
DataProviderManager.registerDataProvider(...)
// ...
// ...
}
ПРИМЕЧАНИЕ: объявление объекта не может иметь локальный характер (т.е. быть
вложенным непосредственно в функцию), но может быть вложено в объявление другого
объекта или какого-либо невложенного класса.
Вспомогательные объекты
Объявление объекта внутри класса может быть отмечено ключевым словом companion:
class MyClass {
class MyClass {
companion object {
val x = MyClass.Companion
interface Factory<T> {
fun create(): T
class MyClass {
}
}
Делегирование
Делегирование класса
Шаблон делегирования является хорошей альтернативой наследованию, и Kotlin
поддерживает его нативно, освобождая вас от необходимости написания шаблонного
кода.
interface Base {
fun print()
val b = BaseImpl(10)
Derived(b).print() // prints 10
}
Ключевое слово by в оглавлении Derived, находящееся после типа делегируемого
класса, говорит о том, что объект b типа Base будет храниться внутри
экземпляра Derived, и компилятор сгенерирует у Derived соответствующие методы
из Base, которые при вызове будут переданы объекту b
Делегированные свойства
За помощь в переводе спасибо официальному блогу JetBrains на Хабрахабре
• ленивые свойства (lazy properties): значение вычисляется один раз, при первом
обращении
• свойства, на события об изменении которых можно подписаться (observable
properties)
• свойства, хранимые в ассоциативном списке, а не в отдельных полях
class Example {
class Delegate {
}
Когда мы читаем значение свойства p, вызывается метод getValue() класса Delegate,
причем первым параметром ей передается тот объект, у которого запрашивается
свойство p, а вторым — объект-описание самого свойства p (у него можно, в частности,
узнать имя свойства). Например:
val e = Example()
println(e.p)
e.p = "NEW"
Стандартные делегаты
Стандартная библиотека Kotlin предоставляет несколько полезных видов делегатов:
println("computed!")
"Hello"
println(lazyValue)
println(lazyValue)
}
computed!
Hello
Hello
Observable свойства
Функция Delegates.observable() принимает два аргумента: начальное значение
свойства и обработчик (лямбда), который вызывается при изменении свойства. У
обработчика три параметра: описание свойства, которое изменяется, старое значение и
новое значение.
import kotlin.properties.Delegates
class User {
user.name = "first"
user.name = "second"
"age" to 25
))
println(user.age) // Prints 25
memoizedFoo.doSomething()
Переменная memoizedFoo будет вычислена только при первом обращении к ней. Если
условие someCondition будет ложно, значение переменной не будет вычислено вовсе.
Для read-only свойства (например val), делегат должен предоставлять функцию getValue,
которая принимает следующие параметры:
• thisRef — должен иметь такой же тип или быть наследником типа хозяина
свойства (для расширений — тип, который расширяется)
• property — должен быть типа KProperty<*> или его родительского типа. Эта
функция должна возвращать значение того же типа, что и свойство (или его
родительского типа).
Функции getValue() и/или setValue() могут быть предоставлены либо как члены
класса-делегата, либо как его расширения. Последнее полезно когда вам нужно
делегировать свойство объекту, который изначально не имеет этих функций. Обе эти
функции должны быть отмечены с помощью ключевого слова operator.
Translation Rules
Для каждого делегированного свойства компилятор Kotlin "за кулисами" генерирует
вспомогательное свойство и делегирует его. Например, для свойства prop генерируется
скрытое свойство prop$delegate, и исполнение геттеров и сеттеров просто делегируется
этому дополнительному свойству:
class C {
class C {
Заметьте, что синтаксис this::prop для обращения к bound callable reference напрямую
в коде программы доступен только с Kotlin версии 1.1
Предоставление делегата
Примечание: Предоставление делегата доступно в Kotlin начиная с версии 1.1
thisRef: MyUI,
prop: KProperty<*>
): ReadOnlyProperty<MyUI, T> {
checkProperty(thisRef, prop.name)
// создание делегата
class MyUI {
• thisRef — должен иметь такой же тип, или быть наследником типа хозяина
свойства (для расширений — тип, который расширяется)
• property — должен быть типа KProperty<*> или его родительского типа. Эта
функция должна возвращать значение того же типа, что и свойство (или его
родительского типа)
Не будь этой возможности внедрения между свойством и делегатом, для достижения той
же функциональности вам бы пришлось передавать имя свойства явно, что не очень
удобно:
class MyUI {
id: ResourceID<T>,
propertyName: String
): ReadOnlyProperty<MyUI, T> {
checkProperty(this, propertyName)
// создание делегата
class C {
class C {
Функции
Объявление функций
В Kotlin функции объявляются с помощью ключевого слова fun
Применение функций
При вызове функции используется традиционный подход
Инфиксная запись
Функции так же могут быть вызваны при помощи инфиксной записи, при условии, что:
...
1 shl 2
// то же самое, что
1.shl(2)
Параметры
Параметры функции записываются аналогично системе обозначений в языке
Pascal, имя:тип. Параметры разделены запятыми. Каждый параметр должен быть явно
указан.
Аргументы по умолчанию
Параметры функции могут иметь значения по умолчанию, которые используются в
случае, если аргумент функции не указан при её вызове. Это позволяет снизить уровень
перегруженности кода по сравнению с другими языками.
...
open class A {
class B : A() {
...
}
мы можем вызвать её, используя аргументы по умолчанию
reformat(str)
Однако, при вызове этой функции без аргументов по умолчанию, получится что-то вроде
reformat(str,
normalizeCase = true,
upperCaseFirstLetter = true,
divideByCamelHumps = false,
wordSeparator = '_'
if (name != null)
println("Hello ${name}")
else
println("Hi there!")
...
}
Функции с одним выражением
Когда функция возвращает одно-единственное выражение, фигурные скобки { } могут
быть опущены, и тело функции может быть описано после знака =
result.add(t)
return result
Внутри функции параметр с меткой vararg и типом T виден как массив элементов T,
таким образом переменная ts в вышеуказанном примере имеет тип Array<out T>.
Только один параметр может быть помечен меткой vararg. Если параметр с
именем vararg не стоит на последнем месте в списке аргументов, значения для
соответствующих параметров могут быть переданы с использованием named
argument синтаксиса. В случае, если параметр является функцией, для этих целей можно
вынести лямбду за фигурные скобки.
Локальные функции
Koltin поддерживает локальные функции. Например, функции, вложенные в другие
функции
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v, visited)
dfs(graph.vertices[0], HashSet())
if (!visited.add(current)) return
for (v in current.neighbors)
dfs(v)
dfs(graph.vertices[0])
}
Функции-элементы
Функции-элементы - это функции, объявленные внутри классов или объектов
class Sample() {
// ...
var x = 1.0
while (true) {
val y = Math.cos(x)
if (x == y) return y
x = y
Высокоуровневые функции и
лямбды
Функции высшего порядка
Высокоуровневая функция - это функция, которая принимает другую функцию в качестве
входного аргумента, либо имеет функцию в качестве возвращаемого результата.
Хорошим примером такой функции является lock(), которая берёт залоченный объект и
функцию, применяет лок, выполняет функцию и отпускает lock:
lock.lock()
try{
return body()
}
finally {
lock.unlock()
Давайте проанализируем этот блок. Параметр body имеет функциональный тип: () -> T,
то есть предполагается, что это функция, которая не имеет никаких входных аргументов
и возвращает значение типа T. Она вызывается внутри блока try, защищена lock, и её
результат возвращается функцией lock().
Если мы хотим вызвать метод lock(), мы можем подать другую функцию в качестве
входящего аргумента (более подробно читайте Ссылки на функции):
lock (lock) {
sharedResource.operation()
result.add(transform(item))
return result
}
Обратите внимание, что параметры могут быть проигнорированы при вызове функции в
том случае, если лямбда является единственным аргументом для её вызова.
ints.map { it * 2 }
Инлайн функции
Иногда необходимо улучшить производительность высокоуровневых функций,
используя инлайн функции.
Лямбда-выражения и анонимные
функции
Лямбда-выражения или анонимные функции являются "функциональными
константами"(ориг. "functional literal"), то есть функциями, которые не были объявлены, но
сразу были переданы в качестве выражения. Рассмотрим следующий пример:
Функция max - высокоуровневая функция, так как она принимает другую функцию в
качестве входного аргумента. Этот второй аргумент является выражением, которое само
по себе представляет из себя функцию, то есть functional literal.
Типы функций
Для того, чтобы функция принимала другую функцию в качестве входного параметра,
нам необходимо указать её (входящей функции) тип. К примеру, вышеуказанная
функция max определена следующим образом:
fun <T> max(collection: Collection<T>, less: (T, T) -> Boolean): T? {
max = it
return max
Параметр 'less' является (T, T) -> Boolean типом, то есть функцией, которая
принимает два параметра типа T и возвращает 'Boolean':'true', если первый параметр
меньше, чем второй.
В теле функции, строка 4, less используется в качестве функции: она вызывается путём
передачи двух аргументов типа T.
Тип функции может быть написан так, как указано выше, или же может иметь
определённые параметры, если вы хотите обозначить значения каждого из параметров.
Синтаксис лямбда-выражений
Полная синтаксическая форма лямбда-выражений, таких как literals of function types, может
быть представлена следующим образом:
Если мы вынесем все необязательные объявления, то, что останется, будет выглядеть
следующим образом:
Обычное дело, когда лямбда-выражение имеет только один параметр. Если Kotlin может
определить сигнатуру метода сам, он позволит нам не объявлять этот единственный
параметр, и объявит его сам под именем it:
ints.filter { it > 0 } //Эта константа имеет тип '(it: Int) -> Boolean'
ints.filter {
ints.filter {
return@filter shouldFilter
Анонимные функции
Единственной особенностью синтаксиса лямбда-выражений, о которой ещё не было
сказано, является способность определять и назначать возвращаемый функцией тип. В
большинстве случаев в этом нет особой необходимости, потому что он может быть
вычислен автоматически. Однако, если у вас есть потребность в определении
возвращаемого типа, вы можете воспользоваться альтернативным синтаксисом:
return x + y
Замыкания
Лямбда-выражение или анонимная функция (так же, как и локальная функция или object
expression) имеет доступ к своему замыканию, то есть к переменным, объявленным вне
этого выражения или функции. В отличае от Java, переменные, захваченные в
замыкании, могут быть изменены:
var sum = 0
sum += it
print(sum)
1.sum(2)
Синтаксис анонимной функции позволяет вам явно указать тип приёмника. Это может
быть полезно в случае, если вам нужно объявить переменную типа нашей функции для
использования в дальнейшем.
class HTML {
return html
lock(l) { foo() }
Вместо создания объекта функции для параметра и генерации вызова, компилятор мог
бы выполнить что-то подобное этому коду:
l.lock()
try {
foo()
finally {
l.unlock()
// ...
Модификатор inline влияет и на функцию, и на лямбду, переданную ей: они обе будут
встроены в место вызова.
noinline
В случае, если вы хотите, чтобы только некоторые лямбды, переданные inline-функции,
были встроены, вам необходимо отметить модификатором noinline те функции-
параметры, которые встроены не будут:
// ...
Когда как встраиваемые лямбды могут быть вызваны только внутри inline-функций или
переданы в качестве встраиваемых аргументов, с noinline-функциями можно работать
без ограничений: хранить внутри полей, передавать куда-либо и т.д.
Нелокальные return
В Kotlin мы можем использовать обыкновенный, безусловный return только для выхода
из именованной функции или анонимной функции. Это значит, что для выхода из
лямбды нам нужно использовать label. Обычный return запрещён внутри лямбды,
потому что она не может заставить внешнюю функцию завершиться.
fun foo() {
ordinaryFunction {
}
Но если функция, в которую передана лямбда, встроена, то return также будет встроен,
поэтому так делать можно:
fun foo() {
inlineFunction {
ints.forEach {
return false
// ...
var p = parent
while (p != null && !clazz.isInstance(p)) {
p = p?.parent
@Suppress("UNCHECKED_CAST")
return p as T
myTree.findParentOfType(MyTreeNodeType::class.java)
Что мы на самом деле хотим, так это передать этой функции тип, то есть вызвать её вот
так:
myTree.findParentOfType<MyTreeNodeType>()
var p = parent
p = p?.parent
return p as T
Хотя рефлексия может быть не нужна во многих случаях, мы всё ещё можем
использовать её с параметром вещественного типа:
println(membersOf<StringBuilder>().joinToString("\n"))
}
Обычная функция (не отмеченная как встроенная) не может иметь параметры
вещественного типа. Тип, который не имеет представление во времени исполнения
(например, параметр невещественного или фиктивного типа вроде Nothing), не может
использоваться в качестве аргумента для параметра вещественного типа.
Сопрограммы
Сопрограммы являются экспериментальными в Kotlin 1.1. Детали
см. ниже
Некоторые API инициируют долго протекающие операции (такие как сетевой ввод-вывод,
файловый ввод-вывод, интенсивная обработка на CPU или GPU и др.), которые требуют
блокировки вызывающего кода в ожидании завершения операций. Сопрограммы
обеспечивают возможность избежать блокировки исполняющегося потока путём
использования более дешёвой и управляемой операции: приостановки(suspend)
сопрограммы.
Останавливаемые функции
Приостановка происходит в случае вызова функции, обозначенной специальным
модификатором suspend:
...
async {
doSomething(foo)
...
async {
...
...
}
Больше информации о том, как действительно работают
функции async/await вkotlinx.coroutines, может быть найдено здесь.
interface Base {
Aннотация @RestrictsSuspension
Расширяющие функции (и анонимные функции) также могут быть маркированы
как suspend, подобно и всем остальным (регулярным) функциям. Это позволяет
создавать DSL и другие API, которые пользователь может расширять. В некоторых
случаях автору библиотеки необходимо запретить пользователю добавлять новые
пути приостановки сопрограммы.
@RestrictsSuspension
Внутреннее функционирование
сопрограмм
Мы не стремимся здесь дать полное объяснение того, как сопрограммы работают под
капотом, но примерный смысл того, что происходит, очень важен.
Более детально о том, как работают сопрограммы, можно узнать в этом проектном
документе. Похожие описания async / await в других языках (таких как C# или
ECMAScript 2016) актуальны и здесь, хотя особенности их языковых реализаций могут
существенно отличаться от сопрограмм Kotlin.
Стандартные API
Сопрограммы представлены в трёх их главных ингредиентах:
Более детальная информация о использовании этих API может быть найдена здесь.
• buildSequence()
• buildIterator()
import kotlin.coroutines.experimental.*
fun main(args: Array<String>) {
//sampleStart
var a = 0
var b = 1
yield(1)
while (true) {
yield(a + b)
val tmp = a + b
a = b
b = tmp
//sampleEnd
println(fibonacciSeq.take(8).toList())
import kotlin.coroutines.experimental.*
print("START ")
for (i in 1..5) {
yield(i)
print("STEP ")
print("END")
//sampleEnd
Запустите приведенный выше код, чтобы убедиться, что если мы будем печатать первые
три элемента, цифры чередуются со STEP-ами по ветвям цикла. Это означает, что
вычисления действительно ленивые. Для печати 1 мы выполняем только до
первого yield(i) и печатаем START ходу дела. Затем, для печати 2, нам необходимо
переходить к следующему yield(i), и здесь печатать STEP. То же самое и для 3. И
следующий STEP никогда не будет напечатан (точно так же как и END), поскольку мы
никогда не запрашиваем дополнительных элементов последовательности.
import kotlin.coroutines.experimental.*
//sampleStart
yield(0)
yieldAll(1..10)
//sampleEnd
}
import kotlin.coroutines.experimental.*
//sampleStart
if (x % 2 != 0) yield(x)
//sampleEnd
Эти библиотеки являются удобными API, которые делают основные задачи простыми.
Также они содержат законченные примеры того, как создавать библиотеки, построенные
на сопрограммах.
Мульти-декларации
Иногда удобно деструктуризировать объект на несколько переменных, например:
println(name)
println(age)
// вычисления
Пример: мульти-декларации и
ассоциативные списки
Пожалуй, самый хороший способ итерации по ассоциативному списку:
Тип List<out T> в Kotlin — интерфейс, который предоставляет read-only операции, такие
как size, get, и другие. Так же, как и в Java, он наследуется от Collection<T>, а значит и
от Iterable<T>. Методы, которые изменяют список, добавлены в
интерфейс MutableList<T>. То же самое относится и к Set<out
T>/MutableSet<T>, Map<K, out V>/MutableMap<K, V>.
numbers.add(4)
assert(strings.size == 3)
class Controller {
items.first() == 1
items.last() == 4
Также, обратите внимание на такие утилиты как sort, zip, fold, reduce.
Интервалы
Интервалы оформлены с помощью функций rangeTo и имеют оператор в виде ..,
который дополняется in и !in. Они применимы ко всем сравниваемым (comparable) типам,
но для целочисленных примитивов есть оптимизированная реализация. Вот несколько
примеров применения интервалов.
println(i)
А что, если вы хотите произвести итерацию в обратном порядке? Это просто. Можете
использовать функцию downTo(), определённую в стандартной библиотеке:
println(i)
Целочисленные последовательности
(IntProgression, LongProgression, CharProgression) являются арифметическими.
Последовательности определены элементами first, last и ненулевым
значением increment. Элемент first является первым, последующими являются
элементы, полученные при инкрементации предыдущего элемента с
помощью increment. Если последовательность не является пустой, то
элемент last всегда достигается в результате инкрементации.
Последовательность является подтипом Iterable<N>, где N - это Int, Long или Char.
Таким образом, её можно использовать в циклах for и функциях типа map, filter и т.п.
Итерация Progression идентична индексируемому циклу for в Java/JavaScript
// ...
class Int {
//...
//...
//...
}
Числа с плавающей точкой (Double, Float) не имеют своего оператора rangeTo. Такой
оператор обозначен для них в дженериках типа Comparable стандартной библиотеки:
downTo()
Экстеншн-функция downTo() задана для любой пары целочисленных типов, вот два
примера:
reversed()
Функция reversed() расширяет класс *Progression* таким образом, что все
экземпляры этого класса возвращают обратные последовательности при её вызове.
step()
Функция-расширение step() также определена для классов *Progression*. Она
возвращает последовательность с изменённым значением шага step (параметр
функции). Значение шага всегда должно быть положительным числом для того, чтобы
функция никогда не меняла направления своей итерации.
}
fun CharProgression.step(step: Int): CharProgression {
if (obj is String) {
print(obj.length)
print("Not a String")
else {
print(obj.length)
Умные приведения
Во многих случаях в Kotlin вам не нужно использовать явные приведения, потому что
компилятор следит за is-проверками для неизменяемых значений и вставляет
приведения автоматически, там, где они нужны:
fun demo(x: Any) {
if (x is String) {
или в случаях, когда приводимая переменная находится справа от оператора && или ||:
when (x) {
Заметьте, что null не может быть приведен к String, так как String не является nullable,
т.е. если y - null, код выше выбросит исключение. Чтобы соответствовать семантике
приведений в Java, нам нужно указать nullable тип в правой части приведения:
Заметьте, что несмотря на то, что справа от as? стоит non-null тип String, результат
приведения является nullable.
Если ключевое слово this не имеет определителей, то оно ссылается на область самого
глубокого замыкания. Чтобы сослаться на this в одной из внешних областей,
используются метки-определители:
this с определителем
Чтобы получить доступ к this из внешней области (класса, функции-расширения, или
именованных литералов функций с принимающим объектом) мы пишем this@label,
где @label - это метка области, из которой нужно получить this:
class A { // неявная метка @A
val d1 = this
Равенство
В Kotlin есть два типа равенства:
Равенство ссылок
Равенство ссылок проверяется с помощью оператора === (и его отрицания !==).
Выражение a === bявляется истиной тогда и только тогда, когда a и b указывают на
один и тот же объект.
Равенство структур
Структурное равенство проверяется оператором == (и его отрицанием !=). Условно,
выражение a == b транслируется в:
Т.е. если a не null, вызывается функция equals(Any?), иначе (т.е. если a указывает
на null) bссылочно сравнивается с null. Заметьте, что в явном сравнении с null для
оптимизации нет смысла: a == null будет автоматически транслироваться в a ===
null.
Перегрузка операторов
Язык Kotlin позволяет нам реализовывать предопределённый набор операторов для
наших типов. Эти операторы имеют фиксированное символическое представление
(вроде + или *) и фиксированные приоритеты. Для реализации оператора мы
предоставляем функцию-член или функцию-расширение с фиксированным именем и с
соответствующим типом, т. е. левосторонним типом для бинарных операций или типом
аргумента для унарных оперций. Функции, которые перегружают операторы, должны
быть отмечены модификатором operator.
Унарные операторы
Унарные префиксные операторы
Выражение Транслируется в
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()
Примечание: эти операции, как и все остальные, оптимизированы для основных типов и
не вносят накладных расходов на вызовы этих функций для них.
Инкремент и декремент
Выражение Транслируется в
Для префиксной формы ++a или --a разрешение работает подобно, но результатом будет:
• Присвоение результата вычисления a.inc() непосредственно a,
• Возвращается новое значение a как общий результат вычисления выражения.
Бинарные операции
Арифметические операции
Выражение Транслируется в
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a..b a.rangeTo(b)
Отметим, что операция rem поддерживается только начиная с Kotlin 1.1. Kotlin 1.0
использует только операцию mod, которая отмечена как устаревшая в Kotlin 1.1.
Пример
Ниже приведен пример класса Counter, начинающего счёт с заданного значения, которое
может быть увеличено с помощью перегруженного оператора +.
}
Оператор in
Выражение Транслируется в
a in b b.contains(a)
a !in b !b.contains(a)
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
Оператор вызова
Выражение Транслируется в
a() a.invoke()
a(i) a.invoke(i)
Выражение Транслируется в
a(i, j) a.invoke(i, j)
Присвоения с накоплением
Выражение Транслируется в
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.modAssign(b)
Операторы сравнений
Выражение Транслируется в
Kotlin призван исключить ошибки подобного рода из нашего кода. NPE могу возникать
только в случае:
Система типов Kotlin различает ссылки на те, которые могут иметь значение null (nullable
ссылки) и те, которые таковыми быть не могут (non-null ссылки). К примеру, переменная
часто используемого типа String не может быть null:
Для того, чтобы разрешить null значение, мы можем объявить эту строковую переменную
как String?:
b = null // ok
val l = a.length
Проверка на null
Первый способ. Вы можете явно проверить b на null значение и обработать два варианта
по отдельности:
} else {
print("Empty string")
Обратите внимание: это работает только в том случае, если b является неизменной
переменной (ориг.: immutable). Например, если это локальная переменная, значение
которой не изменяется в период между его проверкой и использованием. Также такой
переменной может служить val. В противном случае может так оказаться, что
переменная b изменила своё значение на null после проверки.
Безопасные вызовы
Вторым способом является оператор безопасного вызова ?.:
kotlin b?.length
Этот код возвращает b.lenght в том, случае, если b не имеет значение null. Иначе он
возвращает null. Типом этого выражения будет Int?.
Такие безопасные вызовы полезны в цепочках. К примеру, если Bob, Employee (работник),
может быть прикреплён (или нет) к отделу Department, и у отдела может быть
управляющий, другой Employee. Для того, чтобы обратиться к имени этого управляющего
(если такой есть), напишем:
bob?.department?.head?.name
Такая цепочка вернёт null в случае, если одно из свойств имеет значение null.
Элвис-оператор
Если у нас есть nullable ссылка r, мы можем либо провести проверку этой ссылки и
использовать её, либо использовать non-null значение x:
val l = b?.length ?: -1
Так как throw и return тоже являются выражениями в Kotlin, их также можно использовать
справа от Элвис-оператора. Это может быть крайне полезным для проверки аргументов
функции:
// ...
Оператор !!
Для любителей NPE сушествует ещё один способ. Мы можем написать b!! и это вернёт
нам либо non-null значение b (в нашем примере вернётся String), либо выкинет NPE:
val l = b!!.length
В случае, если вам нужен NPE, вы можете заполучить её только путём явного указания.
Исключения
Классы исключений
Все исключения в Kotlin являются наследниками класса Throwable. У каждого
исключения есть сообщение, трассировка стека, а также причина, по которой это
исключение вероятно было вызвано.
Для того, чтобы возбудить исключение явным образом, используйте оператор throw
try {
// some code
// handler
finally {
В коде может быть любое количество блоков catch (такие блоки могут и вовсе
отсутствовать). Блоки finallyмогут быть опущены. Однако, должен быть использован как
минимум один блок catch или finally.
Возвращаемым значением будет либо последнее выражение в блоке try, либо последнее
выражение в блоке catch (или блоках). Содержимое finally блока никак не повлияет на
результат try-выражения.
Проверяемые исключения
В языке Kotlin нет проверяемых исключений. Для этого существует целый ряд причин, но
мы рассмотрим простой пример.
О чём говорит нам сигнатура? О том, что каждый раз, когда я присоединяю строку к чему-
то (к StringBuilder, какому-нибудь логу, сообщению в консоль и т.п) , мне необходимо
отлавливать исключения типа IOExceptions. Почему? Потому, что данная операция
может вызывать IO (Input-Output: Ввод-Вывод) (Writer также реализует
интерфейс Appendable)... Данный факт постоянно приводит к написанию подобного
кода:
try {
log.append(message)
catch (IOException e) {
И это плохо. См. Effective Java, Item 65: Don't ignore exceptions (не игнорируйте исключения).
Тип Nothing
Вы можете использовать выражение throw в качестве части элвис-выражения:
throw IllegalArgumentException(message)
При вызове такой функции компилятор будет в курсе, что исполнения кода далее не
последует:
Совместимость с Java
См. раздел, посвещённый исключениям, в "Совместимости с Java" Java Interoperability
section.
Аннотации
Объявление аннотаций
Аннотации являются специальной формой синтаксических метаданных, добавленных в
исходный код. Для объявления аннотации используйте модификатор annotation перед
именем класса:
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION,
AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
Использование
@Fancy class Foo {
return (@Fancy 1)
Если вам нужно пометить аннотацией первичный конструктор класса, следует добавить
ключевое слово constructor при объявлении конструктора и вставить аннотацию перед
ним:
// ...
class Foo {
@Inject set
Конструкторы
Аннотации могут иметь конструкторы, принимающие параметры:
Если вам нужно определить класс как аргумент аннотации, используйте Kotlin класс
(KClass). Компилятор Kotin автоматически сконвертирует его в Java класс, так что код на
Java сможет видеть аннотации и их аргументы.
import kotlin.reflect.KClass
Лямбды
Аннотации также можно использовать с лямбдами. Они будут применены
к invoke() методу, в который генерируется тело лямбды. Это полезно для фреймворков
вроде Quasar, который использует аннотации для контроля многопоточности.
Аннотации с указаниями
Когда вы помечаете свойство или первичный конструктор аннотацией, из
соответствующего Kotlin-элемента генерируются несколько Java-элементов, и поэтому в
сгенерированном байт-коде элемент появляется в нескольких местах. Чтобы указать, в
каком именно месте аннотация должна быть сгенерирована, используйте следующий
синтаксис:
Тот же синтаксис может быть использован для аннотации целого файла. Для этого
отметьте аннотацию словом file и вставьте её в начале файла: перед указанием пакета
или перед импортами, если файл находится в пакете по умолчанию:
@file:JvmName("Foo")
package org.jetbrains.demo
class Example {
@set:[Inject VisibleForTesting]
• file
• property (такие аннотации не будут видны в Java)
• field
• get (геттер)
• set (сеттер)
• receiver (параметр-приёмник расширения)
• param (параметр конструктора)
• setparam (параметр сеттера)
• delegate (поле, которое хранит экземпляр делегата для делегированного
свойства)
• param
• property
• field
Java-аннотации
Java-аннотации на 100% совместимы в Kotlin:
import org.junit.Test
import org.junit.Assert.*
import org.junit.Rule
import org.junit.rules.*
class Tests {
val f = tempFolder.newFile()
assertEquals(42, getTheAnswer())
// Java
int intValue();
String stringValue();
// Kotlin
Также, как и в Java, параметр value — особый случай; его значение может быть
определено без явного указания имени.
// Java
// Kotlin
@AnnWithValue("abc") class C
Если аргумент value в Java является массивом, в Kotlin он становится vararg параметром:
// Java
String[] value();
// Kotlin
// Java
String[] names();
// Kotlin
// Java
int value();
// Kotlin
val i = ann.value
Рефлексия
Рефлексия — это набор возможностей языка и библиотек, который позволяет
интроспектировать программу (обращаться к её структуре) во время её исполнения. В
Kotlin функции и свойства первичны, и поэтому их интроспекция (например, получение
имени или типа во время исполнения) сильно переплетена с использованием
функциональной или реактивной парадигмы.
Ссылки на классы
Самая базовая возможность рефлексии — это получение ссылки на Kotlin класс. Чтобы
получить ссылку на статический Kotlin класс, используйте синтаксис литерала класса:
val c = MyClass::class
Обратите внимание, что ссылка на Kotlin класс это не то же самое, что и ссылка на Java
класс. Для получения ссылки на Java класс, используйте
свойство .java экземпляра KClass.
Ссылки на функции
Когда у нас есть именованная функция, объявленная следующим образом:
Также вместо этого вы можете указать нужный контекст путём сохранения ссылки на
функцию в переменной, тип которой задан явно:
fun <A, B, C> compose(f: (B) -> C, g: (A) -> B): (A) -> C {
Ссылки на свойства
Для доступа к свойствам как первичным объектам в Kotlin мы по-прежнему можем
использовать оператор :::
var x = 1
fun main(args: Array<String>) {
::x.set(2)
Ссылка на свойство может быть использована там, где ожидается функция без
параметров:
Для функции-расширения:
get() = this[length - 1]
import kotlin.reflect.jvm.*
Ссылки на конструктор
К конструкторам можно обратиться так же, как и к методам или свойствам. Они могут
быть использованы везде, где ожидается объект функционального типа. Обращение к
конструкторам происходит с помощью оператора :: и имени класса. Рассмотрим
функцию, которая принимает функциональный параметр без параметров и
возвращает Foo:
class Foo
Используя ::Foo, конструктор класса Foo без аргументов, мы можем просто вызывать
функцию таким образом:
function(::Foo)
Привязанные функции
Вы можете сослаться на экземпляр метода конкретного объекта.
Вместо вызова метода matches напрямую, мы храним ссылку на него. Такие ссылки
привязаны к объектам, к которым относятся:
Типобезопасные строители
Идея строителей (builders) довольна популярна в сообществе Groovy. Строители
позволяют объявлять данные в полудекларативном виде. Строители хороши
для генерации XML, вёрстки компонентов UI, описания 3D сцен и многого другого...
В отличие от Groovy, Kotlin проверяет типы строителей, что делает их более приятными в
использовании в большинстве юзкейсов.
html {
head {
body {
// смешанный контент
p {
+"Немного"
b {+"смешанного"}
+"документации Kotlin."
p {+"немного текста"}
p {
+arg
Теперь давайте вернёмся к вопросу почему мы можем писать вот такой код:
html {
// ...
html.init()
return html
Эта функция принимает один параметр-функцию под названием init. Тип этой
функции: HTML.() -> Unit — функциональный тип с объектом-приёмником. Это значит,
что нам нужно передать экземпляр класса HTML (приёмник) в функцию, и мы сможем
обращаться к членам объекта в теле этой функции. Обращение происходит через
ключевое слово this:
html {
this.head { /* ... */ }
this.body { /* ... */ }
Теперь this может быть опущено, и мы получим что-то, что уже очень похоже на
строителя:
html {
head { /* ... */ }
body { /* ... */ }
Итак, что же делает этот вызов? Давайте посмотрим на тело функции html, объявленной
выше. Она создаёт новый экземпляр HTML, затем инициализирует его путём вызова
функции, которая была передана в аргументе (в нашем примере это сводится к
вызову head и body у объекта HTML), и после этого возвращает его значение. Это в
точности то, что и должен делать строитель.
Функции head и body в классе HTML объявлены схоже с функцией html. Единственное
отличие в том, что они добавляют отстроенные экземпляры в
коллекцию children заключающего экземпляра HTML:
head.init()
children.add(head)
return head
body.init()
children.add(body)
return body
На самом деле эти две функции делают одно и тоже, поэтому мы можем использовать
обобщённую версию, initTag:
tag.init()
children.add(tag)
return tag
Ещё одна вещь, которую следует обсудить, это добавление текста в тело тэга. В примере
выше мы используем такой синтаксис:
html {
head {
title {+"XML кодирование с Kotlin"}
// ...
Итак, мы просто добавляем строку в тело тэга, приписав + перед текстом, что ведёт к
вызову префиксной операции unaryPlus(). Эта операция определена с
помощью функции-расширения unaryPlus(), которая является членом абстрактного
класса TagWithText (родителя Title).
fun String.unaryPlus() {
children.add(TextElement(this))
html {
head {
// ...
Для решения этой проблемы в Kotlin 1.1 был введен специальный механизм для
управления областью приёмника.
Чтобы заставить компилятор запускать контрольные области, нам нужно только
аннотировать типы всех получателей, используемых в DSL, той же маркерной
аннотацией. Например, для HTML Builders мы объявляем аннотацию @HTMLTagMarker:
@DslMarker
В нашем DSL все классы тэгов расширяют один и тот же суперкласс Tag. Нам достаточно
аннотировать @HtmlTagMarker только суперкласс, и после этого компилятор Kotlin
обработает все унаследованные классы в соответствии с аннотацией:
@HtmlTagMarker
Нам не нужно помечать классы HTML или Head аннотацией @HtmlTagMarker, потому что
их суперкласс уже аннотирован:
После добавления этой аннотации, компилятор Kotlin знает, какие неявные приёмники
являются частью того же DSL, и разрешает обращаться только к членам ближайших
приёмников:
html {
head {
// ...
Обратите внимание, что всё ещё возможно вызывать члены внешнего приёмника, но для
этого вам нужно указать этот приёмник явно:
html {
head {
// ...
}
Полное описание
пакета com.example.html
Перед вами содержание пакета com.example.html (представлены только элементы,
использованные в примере выше). Он строит HTML дерево и активно
использует расширения и лямбды с приёмниками.
package com.example.html
interface Element {
builder.append("$indent$text\n")
@DslMarker
@HtmlTagMarker
tag.init()
children.add(tag)
return tag
}
override fun render(builder: StringBuilder, indent: String) {
builder.append("$indent<$name${renderAttributes()}>\n")
for (c in children) {
builder.append("$indent</$name>\n")
builder.append(" $attr=\"$value\"")
return builder.toString()
render(builder, "")
return builder.toString()
children.add(TextElement(this))
a.href = href
class B : BodyTag("b")
class P : BodyTag("p")
class H1 : BodyTag("h1")
class A : BodyTag("a") {
get() = attributes["href"]!!
set(value) {
attributes["href"] = value
}
html.init()
return html
Псевдонимы типов
Примечание: Псевдонимы типов доступны в Kotlin начиная с версии 1.1
class A {
class B {
Сравнение с языком
программирования Java
Некоторые проблемы Java, решённые в
Kotlin
Koltin решает целый ряд проблем, от которых страдает Java:
Редактирование статей
Редактировать статьи можно на GitHub. Например, чтобы отредактировать эту страницу
откройте ссылку https://github.com/phplego/kotlinlang.ru/edit/master/how-to-edit.md или
нажмите "Редактировать на GitHub" справа вверху.
Участники
Oleg Dubrov
Krasnodar
Moscow, Russia
Artem
Russia
Sergei Tsypanov
Olej
Ukraine, Kharkov
Grigory Bondarenko
Alexey Pyltsyn
Rostov-on-Don, Russia
Andrew Black
Ukraine
Andrey Netyaga
Saint Petersburg
Alex Ilyenko
Kiev, Ukraine
MaxF
Russia
Петров Александр
Novosibirsk
Последние изменения
05 июля 2017 г., 13:09:50 phplego: Merge pull request #47 from FilenkovMaxim/patch-1 Update basic-
syntax.md 9cd5660
23 июня 2017 г., 23:27:04 phplego: Update INDEX.md добавлено - Перевести статьи из радела "Java
Interop" 7fa1375
20 июня 2017 г., 20:55:28 Temon137: Update INDEX.md Были проверены и исправлены все
якоря. d4c14a8
20 июня 2017 г., 19:07:17 Temon137: Update INDEX.md Убран пункт "Статьи, требующие
проверки". 1a8e21d
20 июня 2017 г., 14:48:30 Temon137: Update coroutines.md Исправления текста с целью улучшения
читаемости. Некоторые исправления неточного перевода. 6ec6968
20 июня 2017 г., 11:30:06 Temon137: Update operator-overloading.md Добавлен отсутствующий
пример. b998622
20 июня 2017 г., 10:56:52 Temon137: Update type-safe Строки 246-254: переведены непереведённые
строки. 5bbfb94
20 июня 2017 г., 9:13:32 Temon137: Вторая попытка исправления комментариев 59f2f98
20 июня 2017 г., 8:43:23 Temon137: Попытка исправления комментария Почему-то не скрывается
закомментированный английский текст. 22a9574
19 июня 2017 г., 20:14:00 Temon137: Update delegated-properties.md Исправления ошибок и удаление
дублированного предлоэения (строка 140). a159972
19 июня 2017 г., 20:12:57 Temon137: Update data-classes.md 1ccf25c
19 июня 2017 г., 15:15:32 phplego: Merge pull request #36 from Temon137/patch-13 Update
properties.md 7811f27
19 июня 2017 г., 15:14:13 phplego: Merge pull request #37 from Temon137/patch-12 Update
classes.md a7a64ad
19 июня 2017 г., 15:13:01 phplego: Merge pull request #38 from Temon137/patch-11 Update
returns.md 384ab30
19 июня 2017 г., 15:12:29 phplego: Merge pull request #39 from Temon137/patch-10 Update control-
flow.md f23b741
19 июня 2017 г., 15:11:20 phplego: Merge pull request #40 from Temon137/patch-9 Update
packages.md af5d945
19 июня 2017 г., 15:10:15 phplego: Merge pull request #41 from Temon137/patch-8 Update basic-
types.md bb5a657
19 июня 2017 г., 15:06:57 phplego: Merge pull request #45 from Temon137/patch-14 Update
interfaces.md 90dd7c6
19 июня 2017 г., 15:06:42 phplego: Merge pull request #43 from Temon137/patch-6 Update
idioms.md fd94e0c
19 июня 2017 г., 15:05:14 phplego: Merge pull request #42 from Temon137/patch-7 Update coding-
conventions.md 1c03e40
19 июня 2017 г., 15:04:25 phplego: Merge pull request #44 from Temon137/patch-5 Update basic-
syntax.md db53111
16 июня 2017 г., 20:20:23 phplego: убран пункт "прочая работа" 00c981e
13 июня 2017 г., 12:37:57 phplego: Merge pull request #35 from y2k/master Добавить ссылку на
@kotlin_lang телеграм чат4bf55df
13 июня 2017 г., 10:46:19 y2k: add @kotlin_lang telegram chat 21e9fa9
12 июня 2017 г., 11:59:55 phplego: Merge pull request #33 from Temon137/patch-3 Create multi-
declarations.md 2109a11
12 июня 2017 г., 11:31:40 phplego: Merge pull request #34 from Temon137/patch-4 Исправление
орфографических ошибок.1a065f9
12 июня 2017 г., 11:28:39 phplego: Merge pull request #32 from Temon137/patch-2 Исправления
орфографических ошибок.7efcf80