Расширенное введение в язык программирования Kotlin
О примечаниях
Я разрабатываю мобильные приложения более 6 лет, и я разрабатываю приложения с использованием кроссплатформенных фреймворков более 2 лет. Сейчас я знакомлюсь с Kotlin вместе с Android Programming with Kotlin.
Я подготовил эти заметки для себя, когда впервые начал изучать язык программирования Kotlin. Эти заметки можно рассматривать как расширенное введение в Kotlin.
Я подготовил заметки в Notion — я настоятельно рекомендую вам использовать Notion, чтобы структурировать свои заметки, это потрясающе — для личного использования и теперь делюсь с вами здесь.
Вы найдете эти темы в примечаниях:
- Типы данных: изменяемые переменные и переменные только для чтения, приведение типов, нулевая безопасность, числа, символы, строки, логические значения, массивы.
- Операторы:арифметические, реляционные, присваивания, унарные, логические, побитовые.
- Функции: локальные функции, лямбда-выражения, встроенные функции.
- Условные операторы: если-иначе, когда
- Циклы
- Объектно-ориентированное программирование: классы, объекты, конструкторы, инкапсуляция, модификаторы доступа, наследование, обобщения
- Асинхронное программирование: многопоточность, обратные вызовы, будущее/обещание, Rx, сопрограммы.
Введение
Это строго статически типизированный язык программирования общего назначения, работающий на JVM. Kotlin в основном нацелен на виртуальную машину Java (JVM), но также транспилируется в JavaScript. Kotlin/JS предоставляет возможность транспилировать ваш код Kotlin, стандартную библиотеку Kotlin и любые совместимые зависимости с JavaScript. Рекомендуемый способ использования Kotlin/JS — через плагины kotlin.js
и kotlin.multiplatform
Gradle.
Переменные
Изменяемые переменные
Изменяемость означает, что переменной можно присвоить другое значение после первоначального присвоения.
var a:Int = 1
var b = 1
var c:Int
c = 1
Только для чтения/константные переменные
val a:Int = 1
val b = 1
val c:Int // Type is required when no initializer is provided
c = 1
Типы данных
Встроенный тип данных Kotlin можно разделить на следующие категории:
- Число
- Характер
- Нить
- логический
- Множество
Числа
val a: Int = 10000
val d: Double = 100.00
val f: Float = 100.00f
val l: Long = 1000000004
val s: Short = 10
val b: Byte = 1
Персонажи
Символы представлены типом Char
. Литералы символов заключаются в одинарные кавычки: '1'
Если значение символьной переменной является цифрой, вы можете явно преобразовать ее в число Int, используя функцию digitToInt().
Струны
Строки в Kotlin представлены типом String
. Как правило, строковое значение представляет собой последовательность символов в двойных кавычках ("
).
Элементы строки — это символы, к которым можно получить доступ с помощью операции индексации: s[i]
. Вы можете перебирать эти символы с помощью цикла for
:
for (c in str) { println(c) }
Строки неизменяемы. После инициализации строки вы не можете изменить ее значение или присвоить ей новое значение. Все операции по преобразованию строк возвращают свои результаты в новом объекте
String
, оставляя исходную строку без изменений.
Шаблоны строк
val s = "abc"
println("$s.length is ${s.length}") // prints "abc.length is 3"
Булевы значения
Тип Boolean
представляет логические объекты, которые могут иметь два значения: true
и false
.
Boolean
имеет аналог, допускающий значение NULL, Boolean?
, который также имеет значение null
.
Встроенные операции над логическими значениями включают:
||
– дизъюнкция (логическое ИЛИ)&&
– союз (логическое И)!
negation (логическое НЕ)
||
и &&
работают лениво.
val myTrue: Boolean = true
val myFalse: Boolean = false
val boolNull: Boolean? = null
Массивы
Массивы в Kotlin представлены классом Array
. Он имеет функции get
и set
, которые превращаются в []
в соответствии с соглашениями о перегрузке операторов, и свойство size
, а также другие полезные функции-члены:
class Array<T> private constructor() { val size: Int operator fun get(index: Int): T operator fun set(index: Int, value: T): Unit operator fun iterator(): Iterator<T> // ... }
Типовые проверки и приведения
Используйте оператор is
или его инвертированную форму !is
для выполнения проверки во время выполнения, которая определяет, соответствует ли объект заданному типу:
if (obj is String) { print(obj.length) } if (obj !is String) { // same as !(obj is String) print("Not a String") } else { print(obj.length) }
«Безопасный» (обнуляемый) оператор приведения
Чтобы избежать исключений, используйте безопасный оператор приведения as?
, который возвращает null
в случае ошибки.
val x: String? = y as? String
Нулевая безопасность
var a: String = "abc" // Regular initialization means non-null by default a = null // compilation error
var b: String? = "abc" // can be set to null b = null // ok
Безопасные звонки
Ваш второй вариант доступа к свойству переменной, допускающей значение NULL, — это использование оператора безопасного вызова ?.
.
val a = "Kotlin"
val b: String? = null
println(b?.length) // This returns b.length if b is not null, and null otherwise.
println(a?.length) // Unnecessary safe call
Операторы
- Арифметические операторы
- Реляционные операторы
- Операторы присваивания
- Унарные операторы
- Логические операторы
- Побитовые операции
Арифметические операторы
Реляционные операторы
Операторы присваивания
Унарные операторы
Унарные операторы требуют только одного операнда; они выполняют различные операции, такие как увеличение/уменьшение значения на единицу, отрицание выражения или инвертирование значения логического значения.
fun main(args: Array<String>) { var x: Int = 40 var b:Boolean = true
println("+x = " + (+x)) println("-x = " + (-x)) println("++x = " + (++x)) println("--x = " + (--x)) println("!b = " + (!b)) }
/*
Prints:
+x = 40 -x = -40 ++x = 41 --x = 40 !b = false
*/
Логические операторы
Побитовые операторы
В Kotlin нет побитовых операторов, но Kotlin предоставляет список вспомогательных функций для выполнения побитовых операций.
Функции
Функции Kotlin объявляются с использованием ключевого слова fun
:
fun double(x: Int): Int { return 2 * x }
Функции Kotlin являются первоклассными, что означает, что их можно хранить в переменных и структурах данных, а также передавать в качестве аргументов и возвращать из других функций более высокого порядка. Вы можете выполнять любые операции над функциями, которые возможны для других значений, не являющихся функциями.
Аргументы по умолчанию
fun read(
b: ByteArray,
off: Int = 0,
len: Int = b.size,
) { /*...*/ }
Именованные аргументы
При вызове функции вы можете назвать один или несколько ее аргументов. Это может быть полезно, когда у функции много аргументов и трудно связать значение с аргументом, особенно если это логическое значение или значение null
.
fun myFun( str: String, normalizeCase: Boolean = true, upperCaseFirstLetter: Boolean = true, divideByCamelHumps: Boolean = false, wordSeparator: Char = ' ', ) { /*...*/ }
// calling the function with named variables myFun( "String!", false, upperCaseFirstLetter = false, divideByCamelHumps = true, '_' )
В JVM: вы не можете использовать синтаксис именованных аргументов при вызове функций Java, поскольку байт-код Java не всегда сохраняет имена параметров функции.
Локальные функции
Kotlin поддерживает локальные функции, которые являются функциями внутри других функций:
fun dfs(graph: Graph) { fun dfs(current: Vertex, visited: MutableSet<Vertex>) { if (!visited.add(current)) returnfor (v in current.neighbors) dfs(v, visited) } dfs(graph.vertices[0], HashSet()) }
лямбды
Синтаксис:
{variable with type -> body of the function}
val upperCase = { str: String -> str.toUpperCase() }
println( upperCase("hello, world!") )
//prints HELLO, WORLD!
Встроенные функции
Функция inline объявляется с ключевым словом inline. Использование встроенной функции повышает производительность функции более высокого порядка. Встроенная функция указывает компилятору копировать параметры и функции в место вызова.
Функции высшего порядка означают функции, которые принимают другую функцию в качестве аргумента, например:
fun nonInlined(block: () -> Unit) {
println("before")
block()
println("after")
}
Экземпляр Function
не будет создан, вместо этого код вызова block
внутри встроенной функции будет скопирован на сайт вызова, поэтому вы получите что-то вроде этого в байт-коде.
Поток управления
если еще
if (condition1) {
// code block A to be executed if condition1 is true
} else if (condition2) {
// code block B to be executed if condition2 is true
} else {
// code block C to be executed if condition1 and condition2 are false
}
Тернарный оператор
К сожалению, в Котлине не поддерживаются тернарные операторы.
Эквивалентно допустим следующий синтаксис:
var v = if (a) b else c
Когда
Когда похоже на switch-case в Java
val day = 2
val result = when (day) { 1 -> "Monday" 2 -> "Tuesday" 3 -> "Wednesday" 4 -> "Thursday" 5 -> "Friday" 6 -> "Saturday" 7 -> "Sunday" else -> "Invalid day." }
when (day) { 1, 2, 3, 4, 5 -> println("Weekday") else -> println("Weekend") }
when (day) { in 1..5 -> println("Weekday") else -> println("Weekend") }
Петли
Цикл Kotlin for перебирает все, что предоставляет итератор
for (item in collection) { // body of loop }
for (item in 1..5) { println(item) }
Цикл Kotlin while похож на цикл while в Java.
while (condition) { // body of the loop }
var i = 5; while (i > 0) { println(i) i-- }
и цикл do-while
do{ // body of the loop } while( condition )
var i = 5; do{ println(i) i-- }while(i > 0)
Разрыв и продолжение
Оператор Kotlin break используется для выхода из цикла при выполнении определенного условия. Этот цикл может быть циклом for, while или do…while.
var i = 0;
while (i++ < 100) {
println(i)
if( i == 3 ){
break
}
}
Оператор Kotlin continue прерывает итерацию цикла между ними (пропускает часть, следующую за оператором continue, до конца цикла) и продолжает следующую итерацию в цикле.
var i = 0;
while (i++ < 6) {
if( i == 3 ){
continue
}
println(i)
}
Объектно-ориентированного программирования
Классы
Основной синтаксис:
class myClass {
// property (data member)
private var name: String = "You"
// member function
fun printMe() {
print("You rock! Yes, " + name)
}
}
fun main(args: Array<String>) {
val obj = myClass() // create obj object of myClass class
obj.printMe()
}
В Kotlin нет ключевого слова
new
.
Вложенные классы
По определению, когда класс был создан внутри другого класса, он называется вложенным классом. В Kotlin вложенный класс по по умолчанию статический, поэтому к нему можно получить доступ без создания какого-либо объекта этого класса.
fun main(args: Array<String>) {
val demo = Outer.Nested()
print(demo.foo())
}
class Outer {
class Nested {
fun foo() = "I am hiding inside"
}
}
Конструкторы
Класс в Kotlin может иметь основной конструктор и один или несколько дополнительных конструкторов. . Первичный конструктор является частью заголовка класса и идет после имени класса и необязательных параметров типа.
class Person public @Inject constructor(firstName: String) { /*...*/ }
Если основной конструктор не имеет аннотаций или модификаторов видимости, ключевое слово constructor
можно опустить:
class Person(firstName: String) { constructor(i: Int) { println("Constructor $i") } /*...*/ }
Модификаторы инкапсуляции и доступа
Если вы не укажете модификатор видимости, по умолчанию будет использоваться
public
, что означает, что ваши объявления будут видны везде
private
означает, что член виден только внутри этого класса (включая все его члены).protected
означает, что элемент имеет ту же видимость, что и член, отмеченный какprivate
, но он также виден в подклассах.internal
означает, что любой клиент внутри этого модуля, который видит объявляющий класс, видит егоinternal
членов.public
означает, что любой клиент, который видит объявляющий класс, видит егоpublic
членов.
Наследование
open class Base(p: Int)
class Derived(p: Int) : Base(p)
Переопределение методов
Kotlin требует явных модификаторов для переопределяемых членов и переопределений:
open class Shape { open fun draw() { /*...*/ } fun fill() { /*...*/ } } class Circle() : Shape() { override fun draw() { /*...*/ } }
Переопределение свойств
Механизм переопределения работает со свойствами так же, как и с методами.
open class Shape { open val vertexCount: Int = 0 }
class Rectangle : Shape() { override val vertexCount = 4 }
interface Shape { val vertexCount: Int }
class Rectangle(override val vertexCount: Int = 4) : Shape // Always has 4 vertices
class Polygon : Shape { override var vertexCount: Int = 0 // Can be set to any number later }
Абстрактный класс
Класс может быть объявлен abstract
вместе с некоторыми или всеми его членами. Абстрактный член не имеет реализации в своем классе. Вам не нужно аннотировать абстрактные классы или функции с помощью open
.
abstract class Polygon { abstract fun draw() }
class Rectangle : Polygon() { override fun draw() { // draw the rectangle } }
Ключевое слово open означает "открыто для расширения". Открытая аннотация класса является противоположностью final в Java: она позволяет другим наследовать от этого класса. По умолчанию все классы в Kotlin являются final, что соответствует эффективному Java, пункт 17: проектируйте и документируйте для наследования или запретите его.
Вы также должны четко указать методы, которые вы хотите сделать переопределяемыми, также помеченные open
:
open class Base {
open fun v() {}
fun nv() {}
}
Дженерики
Общие классы
fun main(args: Array<String>) { var objject = genericsExample<String>("KOTLIN") var objject1 = genericsExample<Int>(10) }
class genericsExample<T>(input:T) { init { println("I am getting called with the value "+input) } }
Общие функции
Функции могут иметь общие параметры, которые указываются с помощью угловых скобок перед именем функции:
fun <T> singletonList(item: T): List<T> { /*...*/ }
Дженерики: вход, выход, где
List<out T>
в Kotlin эквивалентноList<? extends T>
в Java.List<in T>
в Kotlin эквивалентноList<? super T>
в Javawhere
немного отличается. Переданный тип должен одновременно удовлетворять всем условиям пунктаwhere
. В приведенном выше примере типT
должен реализовывать обаCharSequence
иComparable
.
fun <T> copyWhenGreater(list: List<T>, threshold: T): List<String> where T : CharSequence, T : Comparable<T> { return list.filter { it > threshold }.map { it.toString() } }
Асинхронное программирование
Многопоточность — это функция, позволяющая одновременно выполнять две или более частей программы для максимального использования ЦП. В Kotlin у нас есть несколько способов выполнения многопоточности, как в java.
- Резьба
- Обратные вызовы
- Фьючерсы, обещания и прочее
- Реактивные расширения
- Корутины
Резьба
fun postItem(item: Item) { val token = preparePost() val post = submitPost(token, item) processPost(post) }
fun preparePost(): Token { // makes a request and consequently blocks the main thread return token }
- Нитки не дешевые. Потоки требуют переключения контекста, что является дорогостоящим.
- Темы не бесконечны. Количество потоков, которые могут быть запущены, ограничено базовой операционной системой. В серверных приложениях это может стать серьезным узким местом.
- Темы не всегда доступны. Некоторые платформы, такие как JavaScript, даже не поддерживают потоки.
- Темы непростые. Отладка потоков, избегание условий гонки — распространенные проблемы, с которыми мы сталкиваемся при многопоточном программировании.
Обратные вызовы
С обратными вызовами идея состоит в том, чтобы передать одну функцию в качестве параметра другой функции и вызвать эту функцию после завершения процесса.
fun postItem(item: Item) { preparePostAsync { token -> submitPostAsync(token, item) { post -> processPost(post) } } }
fun preparePostAsync(callback: (Token) -> Unit) { // make request and return immediately // arrange callback to be invoked later }
- Сложность вложенных обратных вызовов. Обычно функция, используемая в качестве обратного вызова, часто нуждается в собственном обратном вызове. Это приводит к серии вложенных обратных вызовов, которые приводят к непонятному коду. Узор часто называют названной рождественской елкой (скобки представляют собой ветви дерева).
- Обработка ошибок сложна. Модель вложенности несколько усложняет обработку ошибок и их распространение.
Фьючерсы, обещания
fun postItem(item: Item) { preparePostAsync() .thenCompose { token -> submitPostAsync(token, item) } .thenAccept { post -> processPost(post) }
}
fun preparePostAsync(): Promise<Token> { // makes request and returns a promise that is completed later return promise }
- Другая модель программирования. Подобно обратным вызовам, модель программирования отходит от нисходящего императивного подхода к композиционной модели с цепочками вызовов. Традиционные программные структуры, такие как циклы, обработка исключений и т. д., обычно больше не действуют в этой модели.
- Различные API. Обычно необходимо изучить совершенно новый API, такой как
thenCompose
илиthenAccept
, которые также могут различаться на разных платформах. - Конкретный тип возврата. Тип возвращаемого значения отходит от фактических данных, которые нам нужны, и вместо этого возвращает новый тип
Promise
, который необходимо исследовать самостоятельно. - Обработка ошибок может быть сложной. Распространение и объединение ошибок не всегда просто.
Реактивные расширения (Rx)
Идея Rx состоит в том, чтобы двигаться к тому, что называется observable streams
, благодаря чему мы теперь думаем о данных как о потоках (бесконечных объемах данных), и эти потоки можно наблюдать. С практической точки зрения, Rx — это просто шаблон наблюдателя с рядом расширений, которые позволяют нам работать с данными.
Котлин Корутины
Подход Kotlin к работе с асинхронным кодом заключается в использовании сопрограмм, что является идеей приостанавливаемых вычислений, то есть идеей, что функция может приостановить свое выполнение в какой-то момент и возобновить позже.
Однако одно из преимуществ сопрограмм заключается в том, что когда дело касается разработчика, написание неблокирующего кода практически ничем не отличается от написания блокирующего кода. Сама по себе модель программирования практически не меняется.
fun postItem(item: Item) { launch { val token = preparePost() val post = submitPost(token, item) processPost(post) } }
suspend fun preparePost(): Token { // makes a request and suspends the coroutine return suspendCoroutine { /* ... */ } }
Этот код запустит длительную операцию, не блокируя основной поток. preparePost
— это то, что называется suspendable function
, поэтому перед ним стоит ключевое слово suspend
. Как указано выше, это означает, что функция будет выполняться, приостанавливать выполнение и возобновлять выполнение в какой-то момент времени.
- Сигнатура функции остается точно такой же. Единственная разница в том, что к нему добавляется
suspend
. Однако возвращаемый тип — это тип, который мы хотим вернуть. - Код по-прежнему пишется так, как если бы мы писали синхронный код, сверху вниз, без необходимости в каком-либо специальном синтаксисе, за исключением использования функции с именем
launch
, которая, по сути, запускает сопрограмму (описанную в других руководствах). - Модель программирования и API остаются прежними. Мы можем продолжать использовать циклы, обработку исключений и т. д., и нет необходимости изучать полный набор новых API.
- Он не зависит от платформы. Нацеливаемся ли мы на JVM, JavaScript или любую другую платформу, код, который мы пишем, одинаков. Под прикрытием компилятор заботится об адаптации его к каждой платформе.
fun main() = runBlocking { // this: CoroutineScope
launch { // launch a new coroutine and continue
delay(1000L) // non-blocking delay for 1 second (default time unit is ms)
println("World!") // print after delay
}
println("Hello") // main coroutine continues while a previous one is delayed
}
launch
— это конструктор сопрограмм. Он запускает новую сопрограмму одновременно с остальным кодом, которая продолжает работать независимо. Вот почему Hello было напечатано первым.delay
— это специальная функция приостановки. Он приостанавливает сопрограмму на определенное время. Приостановка сопрограммы не блокирует базовый поток, но позволяет запускать другие сопрограммы и использовать базовый поток для своего кода.runBlocking
также является построителем сопрограмм, который соединяет не сопрограммный мир обычного забавного main() и кода с сопрограммами внутри фигурных скобок runBlocking { ... }. Это выделяется в IDE следующим образом: подсказка CoroutineScope сразу после открывающей фигурной скобки runBlocking. Если вы удалите или забудете runBlocking в этом коде, вы получите ошибку при вызове запуска, поскольку запуск объявлен только в CoroutineScope.
fun main() = runBlocking { // this: CoroutineScope launch { doWorld() } println("Hello") }
// this is your first suspending function suspend fun doWorld() { delay(1000L) println("World!") }
Надеюсь, эти заметки помогут вашему прогрессу! Не забудьте ознакомиться с дополнительными темами, такими как Аннотации, отражение, подробнее о сопрограммах и руководством по написанию Effective Kotlin». чище код Kotlin!
Хлопайте, подписывайтесь и оставляйте свои комментарии ниже!
Вы также можете подписаться на меня в Twitter и Linkedin, чтобы узнать больше о росте, геймификации и советах и рекомендациях для независимых разработчиков.