Расширенное введение в язык программирования 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> в Java
  • where немного отличается. Переданный тип должен одновременно удовлетворять всем условиям пункта 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
}
  1. launch — это конструктор сопрограмм. Он запускает новую сопрограмму одновременно с остальным кодом, которая продолжает работать независимо. Вот почему Hello было напечатано первым.
  2. delay — это специальная функция приостановки. Он приостанавливает сопрограмму на определенное время. Приостановка сопрограммы не блокирует базовый поток, но позволяет запускать другие сопрограммы и использовать базовый поток для своего кода.
  3. 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, чтобы узнать больше о росте, геймификации и советах и ​​рекомендациях для независимых разработчиков.