Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: Char
|
||||
description: Char как отдельный тип, код символа, проверки категорий и работа с цифрами
|
||||
tags:
|
||||
- kotlin
|
||||
- stdlib
|
||||
---
|
||||
|
||||
`Char` - один символ в одинарных кавычках. Хранит кодовую единицу UTF-16 (16 бит).
|
||||
|
||||
```kotlin
|
||||
val c: Char = 'A'
|
||||
val tab = '\t'
|
||||
val quote = '\''
|
||||
val unicode = '\u00A9' // ©
|
||||
```
|
||||
|
||||
Ключевое отличие от Java: **`Char` не является числом**. Арифметику с ним делать можно, но неявного превращения в `Int` нет.
|
||||
|
||||
```kotlin
|
||||
val c = 'A'
|
||||
// val n: Int = c // ошибка компиляции
|
||||
val n: Int = c.code // 65 — явно
|
||||
println(c + 1) // B — Char + Int = Char
|
||||
println('z' - 'a') // 25 — Char - Char = Int
|
||||
println(c.toString()) // A
|
||||
```
|
||||
|
||||
| Выражение | Результат | Тип |
|
||||
|---|---|---|
|
||||
| `'A' + 1` | `'B'` | `Char` |
|
||||
| `'z' - 'a'` | `25` | `Int` |
|
||||
| `'A'.code` | `65` | `Int` |
|
||||
| `65.toChar()` | `'A'` | `Char` |
|
||||
|
||||
## Проверки категорий
|
||||
```kotlin
|
||||
println('a'.isLetter()) // true
|
||||
println('5'.isDigit()) // true
|
||||
println('a'.isLetterOrDigit()) // true
|
||||
println(' '.isWhitespace()) // true
|
||||
println('a'.isLowerCase()) // true
|
||||
println('A'.isUpperCase()) // true
|
||||
```
|
||||
Все эти функции учитывают Unicode: `'я'.isLetter()` тоже `true`.
|
||||
|
||||
## Регистр
|
||||
```kotlin
|
||||
println('a'.uppercaseChar()) // A — возвращает Char
|
||||
println('A'.lowercaseChar()) // a
|
||||
println('a'.uppercase()) // "A" — возвращает String
|
||||
println('ß'.uppercase()) // "SS" — два символа, поэтому String
|
||||
```
|
||||
Есть две версии: `uppercaseChar()` даёт `Char`, `uppercase()` - `String`. Вторая нужна, потому что в Unicode регистр не всегда меняется один-к-одному.
|
||||
|
||||
## Цифры
|
||||
```kotlin
|
||||
println('7'.digitToInt()) // 7 — число, а не код символа
|
||||
println('f'.digitToInt(16)) // 15
|
||||
println('x'.digitToIntOrNull()) // null — безопасная версия
|
||||
println(7.digitToChar()) // '7'
|
||||
```
|
||||
Это главный способ разобрать цифру. Старый трюк из Java (`c - '0'`) тоже работает, но `digitToInt()` понятнее и проверяет корректность:
|
||||
```kotlin
|
||||
println('7' - '0') // 7 — работает
|
||||
// println('x'.digitToInt()) // IllegalArgumentException
|
||||
```
|
||||
|
||||
## Диапазоны и сравнение
|
||||
```kotlin
|
||||
println('a' in 'a'..'z') // true
|
||||
println('5' in '0'..'9') // true
|
||||
for (c in 'a'..'e') print(c) // abcde
|
||||
println('a' < 'b') // true — по коду
|
||||
println(('a'..'e').toList()) // [a, b, c, d, e]
|
||||
```
|
||||
|
||||
## Строки и Char
|
||||
```kotlin
|
||||
val s = "Kotlin"
|
||||
println(s[0]) // K — элемент строки это Char
|
||||
println(s.first()) // K
|
||||
println(s.toCharArray().size) // 6
|
||||
println(String(charArrayOf('a','b'))) // ab — собрать строку из массива
|
||||
println(s.toList()) // [K, o, t, l, i, n]
|
||||
println("abc".map { it + 1 }) // [b, c, d]
|
||||
println(charArrayOf('a','b').concatToString()) // ab
|
||||
```
|
||||
|
||||
Приписать `Char` к строке можно сложением - слева обязательно строка:
|
||||
```kotlin
|
||||
println("ab" + 'c') // abc
|
||||
// println('c' + "ab") // ошибка: Char.plus ожидает Int
|
||||
println("${'c'}ab") // cab — через шаблон
|
||||
```
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `char` - числовой тип | `Char` - отдельный тип |
|
||||
> | `int n = c;` работает | нужен `c.code` |
|
||||
> | `Character.isDigit(c)` | `c.isDigit()` |
|
||||
> | `Character.toUpperCase(c)` | `c.uppercaseChar()` |
|
||||
> | `Character.getNumericValue(c)` | `c.digitToInt()` |
|
||||
> | `c - '0'` для цифры | `c.digitToInt()` |
|
||||
> | `switch (c)` по числам | `when (c)` по символам |
|
||||
>
|
||||
> В Java `char` участвует в арифметике как число, из-за чего `System.out.println('a' + 1)` печатает `98`, а не `b`. В Kotlin тот же код даёт `b` - тип сохраняется.
|
||||
>
|
||||
> Все утилиты из `Character` стали методами `Char`, поэтому пишутся через точку и находятся автодополнением.
|
||||
>
|
||||
> Сравнить: [Character в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Character.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`Char` не вмещает символы за пределами BMP.** Эмодзи и редкие иероглифы кодируются двумя `Char` (surrogate pair):
|
||||
> ```kotlin
|
||||
> val emoji = "😀"
|
||||
> println(emoji.length) // 2
|
||||
> println(emoji[0].isLetter()) // false — половина пары бессмысленна
|
||||
> println(emoji.codePointAt(0)) // 128512 — полный код символа
|
||||
> ```
|
||||
> Для посимвольной обработки текста с эмодзи нужны кодовые точки, а не `Char`.
|
||||
>
|
||||
> **`digitToInt()` бросает исключение, а не возвращает -1:**
|
||||
> ```kotlin
|
||||
> println('x'.digitToIntOrNull()) // null — безопасно
|
||||
> // println('x'.digitToInt()) // IllegalArgumentException
|
||||
> ```
|
||||
>
|
||||
> **`'1'.code` - это 49, а не 1.** Путать код символа с его числовым значением - классическая ошибка:
|
||||
> ```kotlin
|
||||
> println('1'.code) // 49
|
||||
> println('1'.digitToInt()) // 1
|
||||
> ```
|
||||
>
|
||||
> **`Char` в `when` требует `else`** - перечислить все 65 536 значений компилятор не может.
|
||||
>
|
||||
> **`uppercase()` возвращает `String`, и это не придирка.** `'ß'.uppercase()` даёт `"SS"` - два символа. Если нужен именно `Char`, используйте `uppercaseChar()`, но помните, что для `'ß'` он вернёт сам `'ß'`.
|
||||
|
||||
Далее: [Числа и переполнение](Числа%20и%20переполнение.md)
|
||||
@@ -0,0 +1,218 @@
|
||||
---
|
||||
title: Деструктуризация
|
||||
description: Разбор объекта на переменные, componentN, деструктуризация в лямбдах и циклах
|
||||
tags:
|
||||
- kotlin
|
||||
- syntax
|
||||
---
|
||||
|
||||
Деструктуризация - разбор объекта на отдельные переменные одной конструкцией.
|
||||
|
||||
```kotlin
|
||||
data class Point(val x: Int, val y: Int)
|
||||
|
||||
fun main() {
|
||||
val (x, y) = Point(1, 2)
|
||||
println("$x $y") // 1 2
|
||||
}
|
||||
```
|
||||
|
||||
Компилятор превращает это в вызовы `component1()`, `component2()` и так далее:
|
||||
```kotlin
|
||||
data class Point(val x: Int, val y: Int)
|
||||
|
||||
fun main() {
|
||||
val p = Point(1, 2)
|
||||
val x = p.component1()
|
||||
val y = p.component2()
|
||||
println("$x $y") // 1 2
|
||||
}
|
||||
```
|
||||
|
||||
## Что можно деструктурировать
|
||||
Любой тип, у которого есть функции `componentN()`. Автоматически они появляются у:
|
||||
- [data class](../Базовые%20типы%20ООП/Data%20class.md) - по свойствам основного конструктора
|
||||
- `Pair` и `Triple`
|
||||
- `Map.Entry` - ключ и значение
|
||||
- списков и массивов (первые 5 элементов)
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
val (a, b) = Pair(1, "x")
|
||||
val (c, d, e) = Triple(1, 2, 3)
|
||||
val (first, second) = listOf("a", "b", "c") // третий игнорируется
|
||||
println("$a $b | $c $d $e | $first $second")
|
||||
}
|
||||
```
|
||||
|
||||
## Свой componentN
|
||||
Для обычного класса операторы объявляются вручную:
|
||||
```kotlin
|
||||
class Range2(val from: Int, val to: Int) {
|
||||
operator fun component1() = from
|
||||
operator fun component2() = to
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val (from, to) = Range2(1, 10)
|
||||
println("$from..$to") // 1..10
|
||||
}
|
||||
```
|
||||
Слово `operator` обязательно.
|
||||
|
||||
## Пропуск компонентов
|
||||
```kotlin
|
||||
data class User(val id: Int, val name: String, val email: String)
|
||||
|
||||
fun main() {
|
||||
val (_, name, _) = User(1, "Иван", "i@x.ru")
|
||||
println(name) // Иван
|
||||
}
|
||||
```
|
||||
Подчёркивание означает «этот компонент не нужен» - `componentN()` для него даже не вызовется.
|
||||
|
||||
Если нужны только первые компоненты, лишние просто не перечисляют:
|
||||
```kotlin
|
||||
data class User(val id: Int, val name: String, val email: String)
|
||||
|
||||
fun main() {
|
||||
val (id, name) = User(1, "Иван", "i@x.ru") // email не берём
|
||||
println("$id $name") // 1 Иван
|
||||
}
|
||||
```
|
||||
|
||||
## В циклах
|
||||
```kotlin
|
||||
fun main() {
|
||||
val map = mapOf("a" to 1, "b" to 2)
|
||||
for ((key, value) in map) {
|
||||
println("$key=$value") // a=1, затем b=2
|
||||
}
|
||||
|
||||
val list = listOf("x", "y")
|
||||
for ((index, item) in list.withIndex()) {
|
||||
println("$index:$item") // 0:x, затем 1:y
|
||||
}
|
||||
}
|
||||
```
|
||||
Это самое частое применение деструктуризации.
|
||||
|
||||
## В лямбдах
|
||||
```kotlin
|
||||
fun main() {
|
||||
val map = mapOf("a" to 1, "b" to 2)
|
||||
map.forEach { (k, v) -> println("$k$v") } // a1 b2
|
||||
println(map.map { (k, v) -> "$k=$v" }) // [a=1, b=2]
|
||||
map.forEach { (_, v) -> print(v) } // 12
|
||||
println()
|
||||
|
||||
val points = listOf(1 to 2, 3 to 4)
|
||||
println(points.map { (x, y) -> x + y }) // [3, 7]
|
||||
}
|
||||
```
|
||||
Параметр в скобках `{ (k, v) -> }` - это **один** параметр, который разбирается, а не два отдельных. Без скобок `{ k, v -> }` лямбда ожидала бы два аргумента, и код не скомпилировался бы.
|
||||
|
||||
## Возврат нескольких значений
|
||||
Классическое применение - функция, которая должна вернуть пару значений:
|
||||
```kotlin
|
||||
data class Result2(val value: Int, val error: String?)
|
||||
|
||||
fun parse(s: String): Result2 =
|
||||
s.toIntOrNull()?.let { Result2(it, null) } ?: Result2(0, "не число")
|
||||
|
||||
fun main() {
|
||||
val (value, error) = parse("42")
|
||||
println("$value $error") // 42 null
|
||||
|
||||
val (v2, e2) = parse("abc")
|
||||
println("$v2 $e2") // 0 не число
|
||||
}
|
||||
```
|
||||
`Pair` для этого тоже подходит, но свой `data class` лучше: имена компонентов осмысленны, а не `first`/`second`.
|
||||
|
||||
## Деструктуризация по именам (Kotlin 2.3+)
|
||||
Классическая деструктуризация позиционная - порядок переменных должен совпадать с порядком свойств. Это хрупко: перестановка свойств в `data class` молча меняет смысл кода.
|
||||
|
||||
С Kotlin 2.3 разрабатывается деструктуризация по именам. Синтаксис меняется так:
|
||||
- `val [a, b] = obj` - **квадратные** скобки, позиционно (как было)
|
||||
- `val (name, email) = obj` - **круглые** скобки, теперь **по именам** свойств
|
||||
|
||||
```kotlin
|
||||
data class User(val name: String, val email: String)
|
||||
|
||||
fun main() {
|
||||
val user = User("ИМЯ", "ПОЧТА")
|
||||
|
||||
val (email, name) = user // по именам — порядок не важен
|
||||
println("name=$name email=$email") // name=ИМЯ email=ПОЧТА
|
||||
|
||||
val [first, second] = user // позиционно
|
||||
println("first=$first") // first=ИМЯ
|
||||
}
|
||||
```
|
||||
|
||||
Фича пока за флагом компилятора:
|
||||
```bash
|
||||
kotlinc -Xname-based-destructuring=complete App.kt
|
||||
```
|
||||
Значения флага: `only-syntax` (включить синтаксис `[]` и полную форму), `name-mismatch` (предупреждать, когда имена в позиционной деструктуризации не совпадают со свойствами), `complete` (включить короткую форму по именам).
|
||||
|
||||
> [!warning] Это ломающее изменение
|
||||
> Круглые скобки меняют смысл: был порядок, станут имена. Код `val (email, name) = user` сегодня присвоит `email` первое свойство, а после включения фичи - свойство с именем `email`. Пока флаг не включён, вся деструктуризация в вашем коде позиционная.
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> В Java деструктуризации объектов нет. Ближайший аналог - record patterns из Java 21:
|
||||
> ```java
|
||||
> record Point(int x, int y) {}
|
||||
> if (obj instanceof Point(int x, int y)) { ... }
|
||||
> ```
|
||||
> Но это работает только в `instanceof` и `switch`, а не при обычном объявлении переменных. Разбор `Map.Entry` в цикле в Java невозможен - нужно писать `e.getKey()` и `e.getValue()`.
|
||||
>
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `var k = e.getKey(); var v = e.getValue();` | `val (k, v) = e` |
|
||||
> | `for (var e : map.entrySet())` + геттеры | `for ((k, v) in map)` |
|
||||
> | `p.x(); p.y();` | `val (x, y) = p` |
|
||||
> | нет возврата нескольких значений | `Pair`, `Triple`, `data class` |
|
||||
>
|
||||
> Сравнить: [Record в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовые%20типы%20ООП/Record.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Деструктуризация позиционная, а не по именам.** Это главная опасность:
|
||||
> ```kotlin
|
||||
> data class User(val name: String, val email: String)
|
||||
>
|
||||
> fun main() {
|
||||
> val (name, email) = User("Иван", "i@x.ru")
|
||||
> println("$name / $email") // Иван / i@x.ru
|
||||
> }
|
||||
> ```
|
||||
> Если позже в `data class` поменять порядок свойств на `(email, name)`, этот код **продолжит компилироваться**, но переменные получат чужие значения. Компилятор не поможет, потому что типы совпадают. Поэтому порядок свойств в `data class` - часть публичного API.
|
||||
>
|
||||
> **У обычного класса деструктуризации нет** - только у `data class` или при ручном объявлении `componentN()`.
|
||||
>
|
||||
> **В лямбде нужны скобки:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val map = mapOf("a" to 1)
|
||||
> map.forEach { (k, v) -> println("$k$v") } // верно: один параметр-пара
|
||||
> map.forEach { e -> println("${e.key}${e.value}") } // тоже верно
|
||||
> }
|
||||
> ```
|
||||
> Запись `{ k, v -> }` для `forEach` на `Map` не скомпилируется.
|
||||
>
|
||||
> **Список короче числа переменных - исключение в рантайме:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val list = listOf(1)
|
||||
> val (a, b) = list // компилируется!
|
||||
> println("$a $b") // IndexOutOfBoundsException
|
||||
> }
|
||||
> ```
|
||||
> У `List` есть `component1()`..`component5()`, и компилятор не знает фактическую длину. С `data class` такой проблемы нет.
|
||||
>
|
||||
> **`component1()` вызывается даже для неиспользуемой переменной** - если это не `_`. Для свойства с тяжёлым геттером лишняя работа.
|
||||
>
|
||||
> **Деструктуризация `Pair` теряет смысл имён.** `val (a, b) = getCoordinates()` не говорит, что где. Лучше `data class Coordinates(val lat: Double, val lon: Double)`.
|
||||
|
||||
Далее: [Приведение типов](Приведение%20типов.md)
|
||||
@@ -0,0 +1,273 @@
|
||||
---
|
||||
title: Лямбды и функциональные типы
|
||||
description: Синтаксис лямбд, it, функциональные типы, ссылки на функции и анонимные функции
|
||||
tags:
|
||||
- kotlin
|
||||
- functional
|
||||
---
|
||||
|
||||
Функциональный тип описывает функцию как значение: `(параметры) -> результат`.
|
||||
|
||||
```kotlin
|
||||
val square: (Int) -> Int = { x -> x * x }
|
||||
val greet: (String) -> String = { "Привет, $it" }
|
||||
val action: () -> Unit = { println("сделано") }
|
||||
val sum: (Int, Int) -> Int = { a, b -> a + b }
|
||||
|
||||
fun main() {
|
||||
println(square(4)) // 16
|
||||
println(greet("мир")) // Привет, мир
|
||||
action() // сделано
|
||||
println(sum(2, 3)) // 5
|
||||
}
|
||||
```
|
||||
|
||||
## Синтаксис лямбды
|
||||
Лямбда всегда в фигурных скобках:
|
||||
```kotlin
|
||||
{ параметры -> тело }
|
||||
```
|
||||
```kotlin
|
||||
val f = { x: Int, y: Int -> x + y } // типы указаны в лямбде
|
||||
val g: (Int) -> Int = { it * 2 } // тип указан у переменной
|
||||
```
|
||||
Тип нужно указать либо у переменной, либо у параметров лямбды - где-то компилятор должен его увидеть.
|
||||
|
||||
Результат - **последнее выражение** тела, `return` не нужен и в обычной лямбде запрещён:
|
||||
```kotlin
|
||||
val describe: (Int) -> String = { n ->
|
||||
val prefix = if (n > 0) "плюс" else "минус"
|
||||
"$prefix $n" // это и есть результат
|
||||
}
|
||||
|
||||
fun main() { println(describe(5)) } // плюс 5
|
||||
```
|
||||
|
||||
## it - неявный параметр
|
||||
Если параметр один, его можно не объявлять:
|
||||
```kotlin
|
||||
val double: (Int) -> Int = { it * 2 }
|
||||
println(listOf(1, 2, 3).map { it * 10 }) // [10, 20, 30]
|
||||
```
|
||||
`it` удобен в короткой лямбде. Если тело длинное или лямбды вложены - давайте имя:
|
||||
```kotlin
|
||||
val users = listOf("Иван", "Пётр")
|
||||
println(users.map { name -> name.uppercase() }) // [ИВАН, ПЁТР]
|
||||
```
|
||||
|
||||
## Лямбда последним аргументом
|
||||
Если последний параметр функции - функция, лямбду выносят за скобки:
|
||||
```kotlin
|
||||
fun measure(name: String, block: () -> Unit) {
|
||||
val start = System.nanoTime()
|
||||
block()
|
||||
println("$name занял ${System.nanoTime() - start} нс")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
measure("цикл") { // лямбда вне скобок
|
||||
repeat(1000) { }
|
||||
}
|
||||
measure("пусто", { }) // так тоже можно, но не принято
|
||||
}
|
||||
```
|
||||
Когда функция принимает **только** лямбду, круглые скобки опускаются совсем. Именно на этом правиле построены `forEach`, `let`, `apply` и все DSL:
|
||||
```kotlin
|
||||
listOf(1, 2).forEach { println(it) }
|
||||
```
|
||||
Подробнее: [DSL](../../Функциональный%20Kotlin/DSL.md)
|
||||
|
||||
## Неиспользуемые параметры
|
||||
```kotlin
|
||||
val map = mapOf("a" to 1)
|
||||
map.forEach { (_, value) -> println(value) } // 1 — ключ не нужен
|
||||
```
|
||||
Подчёркивание говорит компилятору, что параметр намеренно игнорируется.
|
||||
|
||||
## Ссылки на функции - ::
|
||||
Вместо лямбды, которая только вызывает существующую функцию, используют ссылку:
|
||||
```kotlin
|
||||
fun isEven(n: Int) = n % 2 == 0
|
||||
|
||||
fun main() {
|
||||
val nums = listOf(1, 2, 3, 4)
|
||||
println(nums.filter { isEven(it) }) // [2, 4] — лямбда-обёртка
|
||||
println(nums.filter(::isEven)) // [2, 4] — ссылка, короче
|
||||
nums.forEach(::println)
|
||||
}
|
||||
```
|
||||
|
||||
Виды ссылок:
|
||||
```kotlin
|
||||
class User(val name: String) {
|
||||
fun greet() = "Привет, $name"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val byName: (User) -> String = User::name // ссылка на свойство
|
||||
val greeter: (User) -> String = User::greet // на метод класса
|
||||
val factory: (String) -> User = ::User // на конструктор
|
||||
|
||||
val u = User("Иван")
|
||||
println(byName(u)) // Иван
|
||||
println(greeter(u)) // Привет, Иван
|
||||
println(factory("Пётр").name) // Пётр
|
||||
|
||||
val bound = u::greet // привязанная к объекту
|
||||
println(bound()) // Привет, Иван
|
||||
}
|
||||
```
|
||||
|
||||
## Анонимные функции
|
||||
Когда нужен явный `return` или явный тип результата:
|
||||
```kotlin
|
||||
val parse = fun(s: String): Int? {
|
||||
if (s.isBlank()) return null // return работает
|
||||
return s.toIntOrNull()
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(parse(" ")) // null
|
||||
println(parse("42")) // 42
|
||||
}
|
||||
```
|
||||
Отличие от лямбды: `return` выходит из самой анонимной функции, а в лямбде он запрещён (или выходит из внешней функции, если лямбда inline).
|
||||
|
||||
## Функциональные типы в подробностях
|
||||
```kotlin
|
||||
val a: () -> Unit // без параметров, без результата
|
||||
val b: (Int) -> String // один параметр
|
||||
val c: (Int, Int) -> Int // два параметра
|
||||
val d: ((Int) -> Int) -> Int // принимает функцию
|
||||
val e: (Int) -> ((Int) -> Int) // возвращает функцию
|
||||
val f: String.(Int) -> String // с получателем (receiver)
|
||||
val g: ((Int) -> Int)? // nullable
|
||||
val h: suspend () -> Unit // suspend-функция
|
||||
```
|
||||
|
||||
Тип с получателем вызывается как метод:
|
||||
```kotlin
|
||||
val repeatTimes: String.(Int) -> String = { n -> this.repeat(n) }
|
||||
|
||||
fun main() {
|
||||
println("ab".repeatTimes(2)) // abab
|
||||
}
|
||||
```
|
||||
На этом построены `apply`, `buildString` и DSL. Подробнее: [Receivers и context parameters](../../Функциональный%20Kotlin/Receivers%20и%20context%20parameters.md)
|
||||
|
||||
## Замыкания
|
||||
Лямбда захватывает переменные из окружения - **включая изменяемые**:
|
||||
```kotlin
|
||||
fun counter(): () -> Int {
|
||||
var count = 0 // захвачена по ссылке
|
||||
return { ++count }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val next = counter()
|
||||
println(next()) // 1
|
||||
println(next()) // 2
|
||||
println(next()) // 3
|
||||
}
|
||||
```
|
||||
Это важное отличие от Java, где захватить можно только effectively final переменную.
|
||||
|
||||
```kotlin
|
||||
fun main() {
|
||||
var sum = 0
|
||||
listOf(1, 2, 3).forEach { sum += it } // в Java так нельзя
|
||||
println(sum) // 6
|
||||
}
|
||||
```
|
||||
|
||||
## Вызов
|
||||
```kotlin
|
||||
val f: (Int) -> Int = { it + 1 }
|
||||
|
||||
fun main() {
|
||||
println(f(1)) // 2 — обычный вызов
|
||||
println(f.invoke(1)) // 2 — то же самое
|
||||
val g: ((Int) -> Int)? = null
|
||||
println(g?.invoke(1)) // null — безопасный вызов nullable-функции
|
||||
}
|
||||
```
|
||||
|
||||
## Что происходит под капотом
|
||||
Функциональный тип компилируется в интерфейс `FunctionN`:
|
||||
```kotlin
|
||||
val f: (Int) -> Int = { it * 2 } // объект класса Function1<Int, Int>
|
||||
```
|
||||
Это значит аллокацию на каждую лямбду, если она захватывает переменные. Для функций высшего порядка в горячем коде существует `inline` - он подставляет тело лямбды в место вызова, и объекта не создаётся вовсе.
|
||||
|
||||
Подробнее: [Inline-функции](../../Функциональный%20Kotlin/Inline-функции.md), [Kotlin в байт-коде](../../Взаимодействие%20с%20Java/Kotlin%20в%20байт-коде.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `x -> x * 2` | `{ x -> x * 2 }` или `{ it * 2 }` |
|
||||
> | `Function<Integer,Integer>` | `(Int) -> Int` |
|
||||
> | `BiFunction<A,B,C>` | `(A, B) -> C` |
|
||||
> | `Supplier<T>` | `() -> T` |
|
||||
> | `Consumer<T>` | `(T) -> Unit` |
|
||||
> | `Predicate<T>` | `(T) -> Boolean` |
|
||||
> | `Runnable` | `() -> Unit` |
|
||||
> | `String::length` | `String::length` |
|
||||
> | нет неявного параметра | `it` |
|
||||
> | захват только effectively final | захват изменяемых переменных |
|
||||
> | лямбда только для SAM-интерфейса | функциональный тип первого класса |
|
||||
>
|
||||
> В Java лямбда - это реализация функционального интерфейса, поэтому для каждой формы нужен свой тип (`Function`, `BiFunction`, `Supplier`, `Consumer`, `Predicate`…). В Kotlin тип функции записывается напрямую: `(Int, String) -> Boolean` не требует придумывать имя.
|
||||
>
|
||||
> Захват изменяемых переменных - практическая разница. Java требует `final` или effectively final, вынуждая использовать массив-обёртку `int[] sum = {0}`. В Kotlin `var sum = 0` захватывается как есть (компилятор оборачивает его в `Ref`-объект).
|
||||
>
|
||||
> Сравнить: [Функции в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`return` в лямбде запрещён** - он относился бы к внешней функции:
|
||||
> ```kotlin
|
||||
> fun find(items: List<Int>): Int? {
|
||||
> items.forEach {
|
||||
> if (it > 1) return it // выход из find, а не из лямбды!
|
||||
> } // работает только потому, что forEach inline
|
||||
> return null
|
||||
> }
|
||||
>
|
||||
> fun main() { println(find(listOf(1, 2, 3))) } // 2
|
||||
> ```
|
||||
> Это «нелокальный return», он разрешён только в inline-функциях. Для выхода из самой лямбды нужна метка:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> listOf(1, 2, 3).forEach {
|
||||
> if (it == 2) return@forEach // пропустить итерацию, как continue
|
||||
> println(it) // 1, затем 3
|
||||
> }
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`it` во вложенных лямбдах затеняется:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val nested = listOf(listOf(1, 2))
|
||||
> // println(nested.map { it.map { it * 2 } }) // непонятно, какое it
|
||||
> println(nested.map { row -> row.map { v -> v * 2 } }) // [[2, 4]]
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Каждая не-inline лямбда с захватом - объект.** В цикле на миллион итераций это миллион аллокаций. Стандартные операции (`map`, `filter`, `forEach`) объявлены `inline`, поэтому бесплатны; свои функции высшего порядка в горячем коде тоже стоит делать `inline`.
|
||||
>
|
||||
> **Лямбда, захватившая `this`, держит объект в памяти** - классическая утечка в Android, когда колбэк переживает активити.
|
||||
>
|
||||
> **Ссылка на перегруженную функцию неоднозначна:**
|
||||
> ```kotlin
|
||||
> fun show(x: Int) = "int"
|
||||
> fun show(x: String) = "str"
|
||||
>
|
||||
> fun main() {
|
||||
> // val f = ::show // ошибка: какая из двух?
|
||||
> val f: (Int) -> String = ::show // тип разрешает неоднозначность
|
||||
> println(f(1)) // int
|
||||
> }
|
||||
> ```
|
||||
|
||||
Дополнительно: [Функции высшего порядка](../../Функциональный%20Kotlin/Функции%20высшего%20порядка.md)
|
||||
Далее: [Деструктуризация](Деструктуризация.md)
|
||||
@@ -0,0 +1,249 @@
|
||||
---
|
||||
title: Массивы
|
||||
description: Array, специализированные IntArray, сравнение через contentEquals и почему обычно нужен List
|
||||
tags:
|
||||
- kotlin
|
||||
- stdlib
|
||||
---
|
||||
|
||||
Массив - фиксированная по размеру последовательность элементов. В Kotlin это класс `Array<T>`, а не синтаксическая конструкция.
|
||||
|
||||
```kotlin
|
||||
val a = arrayOf(1, 2, 3)
|
||||
val b = Array(3) { it * 2 } // [0, 2, 4] — по лямбде от индекса
|
||||
val c = arrayOfNulls<String>(3) // [null, null, null]
|
||||
val d = emptyArray<Int>()
|
||||
|
||||
println(a[0]) // 1
|
||||
a[0] = 10
|
||||
println(a.size) // 3
|
||||
println(a.joinToString()) // 10, 2, 3
|
||||
```
|
||||
Размер задаётся при создании и не меняется. Элементы изменяемы всегда - даже если массив объявлен через `val`.
|
||||
|
||||
## Специализированные массивы
|
||||
Для чисел есть отдельные классы без упаковки:
|
||||
```kotlin
|
||||
val ints = intArrayOf(1, 2, 3)
|
||||
val zeros = IntArray(5) // [0, 0, 0, 0, 0] — заполнен нулями
|
||||
val squares = IntArray(5) { it * it } // [0, 1, 4, 9, 16]
|
||||
val doubles = doubleArrayOf(1.0, 2.0)
|
||||
val chars = charArrayOf('a', 'b')
|
||||
val bools = BooleanArray(3) // [false, false, false]
|
||||
```
|
||||
|
||||
| Класс | Байт-код | Аналог в Java |
|
||||
|---|---|---|
|
||||
| `IntArray` | `int[]` | `int[]` |
|
||||
| `LongArray` | `long[]` | `long[]` |
|
||||
| `DoubleArray` | `double[]` | `double[]` |
|
||||
| `CharArray` | `char[]` | `char[]` |
|
||||
| `BooleanArray` | `boolean[]` | `boolean[]` |
|
||||
| `Array<Int>` | `Integer[]` | `Integer[]` |
|
||||
|
||||
Разница принципиальна: `Array<Int>` хранит **упакованные** объекты, `IntArray` - примитивы. В горячем коде это заметно по памяти и скорости.
|
||||
|
||||
```kotlin
|
||||
val boxed: Array<Int> = arrayOf(1, 2, 3) // Integer[] — аллокация на элемент
|
||||
val raw: IntArray = intArrayOf(1, 2, 3) // int[] — без упаковки
|
||||
println("${boxed.size} ${raw.size}")
|
||||
```
|
||||
|
||||
## Сравнение - только contentEquals
|
||||
`==` на массивах сравнивает **ссылки**, а не содержимое. Это главная ловушка:
|
||||
```kotlin
|
||||
val a = arrayOf(1, 2, 3)
|
||||
val b = arrayOf(1, 2, 3)
|
||||
println(a == b) // false — разные объекты!
|
||||
println(a.contentEquals(b)) // true — так правильно
|
||||
println(a.contentHashCode()) // хеш по содержимому
|
||||
```
|
||||
Для вложенных массивов - глубокие версии:
|
||||
```kotlin
|
||||
val x = arrayOf(arrayOf(1), arrayOf(2))
|
||||
val y = arrayOf(arrayOf(1), arrayOf(2))
|
||||
println(x.contentEquals(y)) // false — сравнивает ссылки вложенных
|
||||
println(x.contentDeepEquals(y)) // true
|
||||
```
|
||||
|
||||
## Печать
|
||||
`toString()` массива бесполезен:
|
||||
```kotlin
|
||||
val a = arrayOf(1, 2, 3)
|
||||
println(a) // [Ljava.lang.Integer;@1b6d3586
|
||||
println(a.contentToString()) // [1, 2, 3]
|
||||
println(a.joinToString()) // 1, 2, 3
|
||||
println("$a".startsWith("[L")) // true — шаблон вызывает тот же toString
|
||||
```
|
||||
Внутри строкового шаблона всегда нужен `contentToString()` или `joinToString()`.
|
||||
|
||||
## Обход
|
||||
```kotlin
|
||||
val a = arrayOf("x", "y", "z")
|
||||
for (item in a) print(item) // xyz
|
||||
for (i in a.indices) print(a[i]) // xyz
|
||||
for ((i, v) in a.withIndex()) print("$i$v ") // 0x 1y 2z
|
||||
a.forEach { print(it) } // xyz
|
||||
a.forEachIndexed { i, v -> print("$i$v ") }
|
||||
println()
|
||||
println(a.lastIndex) // 2
|
||||
```
|
||||
|
||||
## Операции
|
||||
Массивы поддерживают те же операции, что коллекции:
|
||||
```kotlin
|
||||
val a = intArrayOf(3, 1, 2)
|
||||
println(a.sorted()) // [1, 2, 3] — возвращает List!
|
||||
println(a.sortedArray()) // новый IntArray
|
||||
a.sort() // сортировка на месте
|
||||
println(a.contentToString()) // [1, 2, 3]
|
||||
println(a.sum()) // 6
|
||||
println(a.average()) // 2.0
|
||||
println(a.max()) // 3
|
||||
println(a.filter { it > 1 }) // [2, 3] — List
|
||||
println(a.map { it * 2 }) // [2, 4, 6] — List
|
||||
println(a.reversed()) // [3, 2, 1] — List
|
||||
println(a.reversedArray().contentToString()) // [3, 2, 1]
|
||||
println(a.contains(2)) // true
|
||||
println(2 in a) // true
|
||||
println(a.indexOf(2)) // 1
|
||||
```
|
||||
Обратите внимание: `filter`, `map`, `sorted`, `reversed` возвращают `List`, а не массив. Версии, сохраняющие тип, имеют суффикс `Array`.
|
||||
|
||||
## Копирование и изменение размера
|
||||
Размер массива неизменен, поэтому «добавление» - это создание нового:
|
||||
```kotlin
|
||||
val a = intArrayOf(1, 2, 3)
|
||||
println((a + 4).contentToString()) // [1, 2, 3, 4] — новый массив
|
||||
println(a.copyOf().contentToString()) // [1, 2, 3]
|
||||
println(a.copyOf(5).contentToString()) // [1, 2, 3, 0, 0] — дополнен нулями
|
||||
println(a.copyOf(2).contentToString()) // [1, 2] — обрезан
|
||||
println(a.copyOfRange(1, 3).contentToString()) // [2, 3]
|
||||
println(a.sliceArray(0..1).contentToString()) // [1, 2]
|
||||
a.fill(0)
|
||||
println(a.contentToString()) // [0, 0, 0]
|
||||
```
|
||||
|
||||
## Преобразование в коллекции и обратно
|
||||
```kotlin
|
||||
val a = arrayOf(1, 2, 3)
|
||||
val list: List<Int> = a.toList()
|
||||
val set = a.toSet()
|
||||
val back: Array<Int> = list.toTypedArray()
|
||||
val raw: IntArray = list.toIntArray()
|
||||
println("${list} ${set} ${back.size} ${raw.size}")
|
||||
```
|
||||
|
||||
## Многомерные массивы
|
||||
Отдельного типа нет - это массив массивов:
|
||||
```kotlin
|
||||
val grid = Array(3) { IntArray(4) } // 3x4, заполнен нулями
|
||||
grid[1][2] = 5
|
||||
println(grid[1].contentToString()) // [0, 0, 5, 0]
|
||||
|
||||
val table = Array(2) { r -> IntArray(2) { c -> r * 2 + c } }
|
||||
println(table.joinToString { it.contentToString() }) // [0, 1], [2, 3]
|
||||
```
|
||||
Строки могут иметь разную длину (рваный массив):
|
||||
```kotlin
|
||||
val jagged = arrayOf(intArrayOf(1), intArrayOf(1, 2, 3))
|
||||
println(jagged.joinToString { it.size.toString() }) // 1, 3
|
||||
```
|
||||
|
||||
## Когда массив, а когда List
|
||||
Массив нужен редко. Используйте его, если:
|
||||
- важна производительность и нужны примитивы без упаковки (`IntArray`)
|
||||
- работаете с API, требующим массив (`vararg`, Java-библиотеки)
|
||||
- размер фиксирован и известен заранее
|
||||
|
||||
В остальных случаях - `List`: он не даёт менять себя случайно, имеет больше операций и корректный `equals`.
|
||||
|
||||
```kotlin
|
||||
val list = listOf(1, 2, 3)
|
||||
println(list == listOf(1, 2, 3)) // true — работает из коробки
|
||||
```
|
||||
Подробнее: [Коллекции](../../Коллекции/Коллекции.md)
|
||||
|
||||
## vararg
|
||||
Параметр с переменным числом аргументов внутри функции - массив:
|
||||
```kotlin
|
||||
fun sum(vararg nums: Int): Int {
|
||||
var total = 0
|
||||
for (n in nums) total += n // nums это IntArray
|
||||
return total
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(sum(1, 2, 3)) // 6
|
||||
println(sum()) // 0
|
||||
val arr = intArrayOf(1, 2)
|
||||
println(sum(*arr)) // 3 — spread-оператор
|
||||
}
|
||||
```
|
||||
Подробнее: [Функции](Функции.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `int[] a = {1,2,3}` | `val a = intArrayOf(1,2,3)` |
|
||||
> | `new int[5]` | `IntArray(5)` |
|
||||
> | `a.length` | `a.size` |
|
||||
> | `Arrays.toString(a)` | `a.contentToString()` |
|
||||
> | `Arrays.equals(a,b)` | `a.contentEquals(b)` |
|
||||
> | `Arrays.deepEquals(a,b)` | `a.contentDeepEquals(b)` |
|
||||
> | `Arrays.sort(a)` | `a.sort()` |
|
||||
> | `Arrays.copyOf(a,n)` | `a.copyOf(n)` |
|
||||
> | `Arrays.fill(a,0)` | `a.fill(0)` |
|
||||
> | `int[]` и `Integer[]` | `IntArray` и `Array<Int>` |
|
||||
> | `f(arr)` для varargs | `f(*arr)` — нужен spread |
|
||||
>
|
||||
> Статические методы `Arrays` стали методами массива - находятся автодополнением, а не поиском по документации.
|
||||
>
|
||||
> Разделение `IntArray` / `Array<Int>` сделано намеренно: в Java `Integer[]` и `int[]` тоже разные, но это легко не заметить. В Kotlin разные имена не позволяют спутать упакованный массив с примитивным.
|
||||
>
|
||||
> **Массивы в Kotlin инвариантны**, в Java - ковариантны. Java разрешает `Object[] o = new String[1]; o[0] = 1;` и падает в рантайме (`ArrayStoreException`). В Kotlin `Array<Any> = Array<String>` не скомпилируется. Подробнее: [Вариантность](../../Дженерики/Вариантность.md)
|
||||
>
|
||||
> Сравнить: [Массивы в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Массивы.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`==` на массивах сравнивает ссылки.** Самая частая ошибка:
|
||||
> ```kotlin
|
||||
> val a = arrayOf(1, 2)
|
||||
> println(a == arrayOf(1, 2)) // false
|
||||
> println(a.contentEquals(arrayOf(1, 2))) // true
|
||||
> ```
|
||||
> По этой же причине массив нельзя использовать как ключ `Map` или в `Set` - `hashCode` считается по ссылке.
|
||||
>
|
||||
> **Массив внутри `data class` ломает `equals`:**
|
||||
> ```kotlin
|
||||
> data class Packet(val data: ByteArray)
|
||||
>
|
||||
> fun main() {
|
||||
> val p1 = Packet(byteArrayOf(1))
|
||||
> println(p1 == Packet(byteArrayOf(1))) // false!
|
||||
> }
|
||||
> ```
|
||||
> Сгенерированный `equals` сравнивает массив по ссылке, а не по содержимому. Поэтому в `data class` с массивом `equals`/`hashCode` пишут руками. Подробнее: [Data class](../Базовые%20типы%20ООП/Data%20class.md)
|
||||
>
|
||||
> **`println(array)` печатает мусор** вида `[Ljava.lang.Integer;@1b6d3586`. То же в строковых шаблонах.
|
||||
>
|
||||
> **`val` не защищает элементы:**
|
||||
> ```kotlin
|
||||
> val a = intArrayOf(1, 2)
|
||||
> a[0] = 99 // разрешено
|
||||
> println(a.contentToString()) // [99, 2]
|
||||
> ```
|
||||
> Неизменяемость даёт `List`, а не `val`.
|
||||
>
|
||||
> **`Array<Int>` вместо `IntArray` - скрытая просадка.** Каждый элемент упакован в `Integer`: больше памяти и промахи кеша.
|
||||
>
|
||||
> **`arrayOfNulls` и `Array(n)` для непримитивных типов требуют разного подхода:**
|
||||
> ```kotlin
|
||||
> val a = arrayOfNulls<String>(3) // Array<String?>
|
||||
> val b = Array(3) { "" } // Array<String> — инициализатор обязателен
|
||||
> println("${a.size} ${b.size}")
|
||||
> ```
|
||||
> Создать `Array<String>` без инициализатора нельзя - компилятор не позволит оставить non-null элементы пустыми.
|
||||
|
||||
Дополнительно: [Коллекции](../../Коллекции/Коллекции.md)
|
||||
Далее: [Функции](Функции.md)
|
||||
@@ -0,0 +1,254 @@
|
||||
---
|
||||
title: Приведение типов
|
||||
description: Проверка is, приведение as и as?, числовые преобразования и стирание типов
|
||||
tags:
|
||||
- kotlin
|
||||
- types
|
||||
---
|
||||
|
||||
## Проверка типа - is
|
||||
```kotlin
|
||||
fun describe(x: Any): String = when {
|
||||
x is String -> "строка длины ${x.length}"
|
||||
x is Int -> "число ${x + 1}"
|
||||
x !is Boolean -> "не логическое"
|
||||
else -> "логическое"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(describe("ab")) // строка длины 2
|
||||
println(describe(5)) // число 6
|
||||
println(describe(1.0)) // не логическое
|
||||
}
|
||||
```
|
||||
После успешной проверки компилятор сам приводит тип - это [умное приведение](../Null-безопасность/Умные%20приведения.md), явный каст не нужен.
|
||||
|
||||
## Небезопасное приведение - as
|
||||
```kotlin
|
||||
fun main() {
|
||||
val x: Any = "text"
|
||||
val s = x as String // ok
|
||||
println(s.length) // 4
|
||||
|
||||
val n: Any = 42
|
||||
// val bad = n as String // ClassCastException
|
||||
}
|
||||
```
|
||||
`as` используют, когда тип известен точно, а компилятор об этом знать не может.
|
||||
|
||||
## Безопасное приведение - as?
|
||||
Возвращает `null` вместо исключения:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val x: Any = "text"
|
||||
println(x as? Int) // null
|
||||
println(x as? String) // text
|
||||
println((x as? Int) ?: 0) // 0 — частая связка с elvis
|
||||
}
|
||||
```
|
||||
Результат всегда nullable, даже при приведении к non-null типу.
|
||||
|
||||
Сравнение способов:
|
||||
|
||||
| Запись | При несовпадении типа |
|
||||
|---|---|
|
||||
| `x is T` | `false` |
|
||||
| `x as T` | `ClassCastException` |
|
||||
| `x as? T` | `null` |
|
||||
|
||||
## Приведение к nullable
|
||||
```kotlin
|
||||
fun main() {
|
||||
val a: Any? = null
|
||||
println(a as? String) // null
|
||||
// println(a as String) // NPE: null cannot be cast to non-null type
|
||||
println(a as String?) // null — приведение к nullable разрешено
|
||||
}
|
||||
```
|
||||
|
||||
## Числовые преобразования
|
||||
Для чисел `as` не применяется - только явные функции:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val i = 42
|
||||
println(i.toLong()) // 42
|
||||
println(i.toDouble()) // 42.0
|
||||
println(i.toByte()) // 42
|
||||
println(i.toChar()) // *
|
||||
println(3.99.toInt()) // 3 — обрезание, не округление
|
||||
println("42".toInt()) // 42
|
||||
}
|
||||
```
|
||||
Неявных преобразований между числовыми типами нет:
|
||||
```kotlin
|
||||
fun main() {
|
||||
val i: Int = 42
|
||||
// val l: Long = i // ошибка компиляции
|
||||
val l: Long = i.toLong() // так
|
||||
}
|
||||
```
|
||||
|
||||
Потеря данных происходит молча:
|
||||
```kotlin
|
||||
fun main() {
|
||||
println(300.toByte()) // 44 — старшие биты отброшены
|
||||
println(1e20.toInt()) // 2147483647 — обрезано
|
||||
println(Double.NaN.toInt()) // 0
|
||||
}
|
||||
```
|
||||
Подробнее: [Числа и переполнение](Числа%20и%20переполнение.md)
|
||||
|
||||
## Стирание типов и дженерики
|
||||
Параметры типа стираются при компиляции, поэтому проверить их в рантайме нельзя:
|
||||
```kotlin
|
||||
fun check(x: Any) {
|
||||
if (x is List<*>) { // можно: звёздочка вместо типа
|
||||
println("список из ${x.size}")
|
||||
}
|
||||
// if (x is List<String>) // ошибка: cannot check for instance
|
||||
// // of erased type
|
||||
}
|
||||
|
||||
fun main() { check(listOf(1, 2)) } // список из 2
|
||||
```
|
||||
`List<*>` - это star-проекция: «список чего-то неизвестного». Элементы из него читаются как `Any?`.
|
||||
|
||||
Приведение к конкретному дженерику компилируется с предупреждением и небезопасно:
|
||||
```kotlin
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun <T> castList(x: Any): List<T> = x as List<T> // unchecked cast
|
||||
|
||||
fun main() {
|
||||
val nums: List<Int> = castList(listOf("a", "b")) // не упадёт здесь!
|
||||
// println(nums.first() + 1) // ClassCastException только при обращении
|
||||
println(nums.size) // 2 — работает, типы не проверяются
|
||||
}
|
||||
```
|
||||
Это отложенное падение: ошибка проявится далеко от места приведения.
|
||||
|
||||
Обойти стирание можно через `reified` в inline-функциях:
|
||||
```kotlin
|
||||
inline fun <reified T> Any.safeCast(): T? = this as? T
|
||||
|
||||
fun main() {
|
||||
println("text".safeCast<String>()) // text
|
||||
println("text".safeCast<Int>()) // null
|
||||
}
|
||||
```
|
||||
Подробнее: [Reified и inline](../../Дженерики/Reified%20и%20inline.md)
|
||||
|
||||
## Приведение в иерархии
|
||||
```kotlin
|
||||
open class Animal(val name: String)
|
||||
class Dog(name: String) : Animal(name) {
|
||||
fun bark() = "Гав"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val a: Animal = Dog("Рекс") // вверх по иерархии — неявно
|
||||
println(a.name) // Рекс
|
||||
|
||||
val d = a as Dog // вниз — только явно
|
||||
println(d.bark()) // Гав
|
||||
|
||||
val other: Animal = Animal("кто-то")
|
||||
println((other as? Dog)?.bark()) // null — безопасно
|
||||
}
|
||||
```
|
||||
Приведение вверх (к супертипу) всегда безопасно и неявно. Приведение вниз требует `as` или `as?`.
|
||||
|
||||
## Коллекции и вариантность
|
||||
```kotlin
|
||||
fun main() {
|
||||
val strings: List<String> = listOf("a")
|
||||
val anys: List<Any> = strings // ok — List ковариантен по out T
|
||||
println(anys.size) // 1
|
||||
|
||||
val mStrings: MutableList<String> = mutableListOf("a")
|
||||
// val mAnys: MutableList<Any> = mStrings // ошибка — MutableList инвариантен
|
||||
}
|
||||
```
|
||||
`List<out T>` разрешает такое присваивание, `MutableList<T>` - нет, иначе можно было бы положить `Int` в список строк. Подробнее: [Вариантность](../../Дженерики/Вариантность.md)
|
||||
|
||||
## Получение типа в рантайме
|
||||
```kotlin
|
||||
fun main() {
|
||||
val x: Any = "text"
|
||||
println(x::class) // class kotlin.String
|
||||
println(x::class.simpleName) // String
|
||||
println(x::class.java) // class java.lang.String — Java-класс
|
||||
println(x is CharSequence) // true — проверка по интерфейсу
|
||||
}
|
||||
```
|
||||
Подробнее: [Рефлексия](../../Рефлексия/Рефлексия.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `x instanceof String` | `x is String` |
|
||||
> | `(String) x` | `x as String` |
|
||||
> | нет безопасного каста | `x as? String` |
|
||||
> | `(int) doubleValue` | `doubleValue.toInt()` |
|
||||
> | `long l = intValue;` неявно | `intValue.toLong()` явно |
|
||||
> | `x.getClass()` | `x::class` или `x::class.java` |
|
||||
> | после `instanceof` нужен каст | умное приведение |
|
||||
> | массивы ковариантны | массивы инвариантны |
|
||||
>
|
||||
> Главные приобретения: `as?` вместо пары `instanceof` + каст, и запрет неявных числовых преобразований, из-за которых в Java `long total = intA * intB` тихо переполняется.
|
||||
>
|
||||
> Ковариантность массивов в Java - известная дыра в типизации:
|
||||
> ```java
|
||||
> Object[] objs = new String[1];
|
||||
> objs[0] = 42; // компилируется, падает в рантайме
|
||||
> ```
|
||||
> В Kotlin `Array<T>` инвариантен, и такой код не соберётся.
|
||||
>
|
||||
> Сравнить: [Приведение ссылочных типов в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Приведение%20ссылочных%20типов.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`as` на null даёт NPE, а не ClassCastException:**
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val a: Any? = null
|
||||
> // println(a as String) // NullPointerException
|
||||
> println(a as String?) // null — ok
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`as?` маскирует обе причины.** `null` в результате означает либо неверный тип, либо что значение изначально было `null` - различить невозможно:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val a: Any? = null
|
||||
> val b: Any = 42
|
||||
> println("${a as? String} ${b as? String}") // null null
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Unchecked cast падает не там, где написан:**
|
||||
> ```kotlin
|
||||
> @Suppress("UNCHECKED_CAST")
|
||||
> fun main() {
|
||||
> val list = listOf("a") as List<Int> // здесь не падает
|
||||
> println(list.size) // 1 — и здесь тоже
|
||||
> // println(list[0] + 1) // ClassCastException только тут
|
||||
> }
|
||||
> ```
|
||||
> Поэтому `@Suppress("UNCHECKED_CAST")` - всегда повод задуматься.
|
||||
>
|
||||
> **`toInt()` на `Double` обрезает дробную часть, а не округляет.** `3.99.toInt()` даёт `3`. Для округления - `roundToInt()`.
|
||||
>
|
||||
> **`toByte()` и `toShort()` молча теряют старшие биты:** `300.toByte()` возвращает `44`.
|
||||
>
|
||||
> **`is` не проверяет параметры дженерика.** `x is List<String>` не скомпилируется, а `x is List<*>` ничего не говорит об элементах. Проверять придётся вручную:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val x: Any = listOf(1, "a")
|
||||
> val strings = (x as? List<*>)?.filterIsInstance<String>()
|
||||
> println(strings) // [a] — только элементы нужного типа
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **`x::class` и `x::class.java` - разные вещи.** Первое - `KClass` Kotlin, второе - `Class` Java. Для `Int` они могут отличаться (`int` против `Integer`).
|
||||
|
||||
Дополнительно: [Умные приведения](../Null-безопасность/Умные%20приведения.md)
|
||||
Далее: [Any, Unit, Nothing](../Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md)
|
||||
@@ -0,0 +1,222 @@
|
||||
---
|
||||
title: Строки
|
||||
description: String как неизменяемый тип, полезные методы, StringBuilder и buildString
|
||||
tags:
|
||||
- kotlin
|
||||
- stdlib
|
||||
---
|
||||
|
||||
`String` - неизменяемый тип. Любая «модификация» создаёт новую строку.
|
||||
|
||||
```kotlin
|
||||
val s = "Kotlin"
|
||||
println(s.length) // 6
|
||||
println(s[0]) // K — доступ по индексу, как у массива
|
||||
println(s.uppercase()) // KOTLIN
|
||||
println(s) // Kotlin — исходная строка не изменилась
|
||||
```
|
||||
|
||||
На JVM это тот же `java.lang.String`, со всем его поведением: пул строк, интернирование, представление в LATIN-1 или UTF-16.
|
||||
|
||||
## Сравнение
|
||||
```kotlin
|
||||
val a = "Kotlin"
|
||||
val b = StringBuilder("Kotlin").toString()
|
||||
println(a == b) // true — сравнение содержимого
|
||||
println(a === b) // false — разные объекты
|
||||
println(a.equals("KOTLIN", ignoreCase = true)) // true
|
||||
```
|
||||
`==` вызывает `equals()`, поэтому работает правильно всегда. Это главное практическое отличие от Java, где `==` на строках сравнивает ссылки.
|
||||
|
||||
Для сортировки и порядка - `compareTo`:
|
||||
```kotlin
|
||||
println("abc" < "abd") // true
|
||||
println("a".compareTo("b")) // -1
|
||||
println(listOf("b", "a", "c").sorted()) // [a, b, c]
|
||||
```
|
||||
|
||||
## Итерация
|
||||
Строка - последовательность `Char`:
|
||||
```kotlin
|
||||
for (c in "abc") print("$c.") // a.b.c.
|
||||
println("abc".toList()) // [a, b, c]
|
||||
println("hello".count { it == 'l' }) // 2
|
||||
println("hello".filter { it != 'l' }) // heo
|
||||
println("abc".map { it.uppercaseChar() }) // [A, B, C]
|
||||
println("abc".reversed()) // cba
|
||||
```
|
||||
Работают все операции над коллекциями: `filter`, `map`, `any`, `count`. Только `filter` и `reversed` возвращают `String`, а `map` - `List`.
|
||||
|
||||
## Поиск и проверки
|
||||
```kotlin
|
||||
val s = "Hello, Kotlin"
|
||||
println(s.contains("Kotlin")) // true
|
||||
println("Kotlin" in s) // true — то же самое
|
||||
println(s.startsWith("Hello")) // true
|
||||
println(s.endsWith("lin")) // true
|
||||
println(s.indexOf("o")) // 4 — первое вхождение, -1 если нет
|
||||
println(s.lastIndexOf("o")) // 8
|
||||
println(s.isEmpty()) // false — длина 0
|
||||
println(" ".isBlank()) // true — пусто или только пробелы
|
||||
println(s.isNotEmpty()) // true
|
||||
```
|
||||
Для nullable-строк есть безопасные версии, их можно звать без `?.`:
|
||||
```kotlin
|
||||
val n: String? = null
|
||||
println(n.isNullOrEmpty()) // true
|
||||
println(n.isNullOrBlank()) // true
|
||||
println(n.orEmpty()) // "" — пустая строка вместо null
|
||||
```
|
||||
|
||||
## Извлечение частей
|
||||
```kotlin
|
||||
val s = "Hello, Kotlin"
|
||||
println(s.substring(7)) // Kotlin
|
||||
println(s.substring(0, 5)) // Hello — [0, 5)
|
||||
println(s.take(5)) // Hello — первые 5
|
||||
println(s.takeLast(6)) // Kotlin
|
||||
println(s.drop(7)) // Kotlin — отбросить первые 7
|
||||
println(s.dropLast(7)) // Hello
|
||||
println(s.first()) // H
|
||||
println(s.last()) // n
|
||||
println(s.getOrNull(100)) // null — вместо исключения
|
||||
```
|
||||
`take` и `drop` безопасны: если запросить больше длины, вернётся вся строка, а не исключение. `substring` за границами падает.
|
||||
|
||||
```kotlin
|
||||
println("abc".take(100)) // abc
|
||||
// println("abc".substring(100)) // StringIndexOutOfBoundsException
|
||||
```
|
||||
|
||||
## Изменение
|
||||
Функции `capitalize()` и `decapitalize()` устарели и удалены. Замена пишется так:
|
||||
```kotlin
|
||||
fun String.capitalize2() =
|
||||
replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
|
||||
|
||||
fun main() { println("hello".capitalize2()) } // Hello
|
||||
```
|
||||
|
||||
Остальные методы возвращают новую строку:
|
||||
```kotlin
|
||||
fun String.capitalize2() =
|
||||
replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
|
||||
|
||||
val s = " Hello, Kotlin "
|
||||
println(s.trim()) // "Hello, Kotlin" — с обеих сторон
|
||||
println(s.trimStart()) // "Hello, Kotlin "
|
||||
println(s.trimEnd()) // " Hello, Kotlin"
|
||||
println("Hello".replace("l", "L")) // HeLLo — все вхождения
|
||||
println("Hello".replaceFirst("l", "L")) // HeLlo
|
||||
println("Hello".lowercase()) // hello
|
||||
println("hello".uppercase()) // HELLO
|
||||
println("hello".capitalize2()) // Hello
|
||||
println("ab".repeat(3)) // ababab
|
||||
println("5".padStart(3, '0')) // 005
|
||||
println("5".padEnd(3)) // "5 "
|
||||
```
|
||||
|
||||
## Разбиение и сборка
|
||||
```kotlin
|
||||
println("a,b,c".split(",")) // [a, b, c]
|
||||
println("a1b2c".split("1", "2")) // [a, b, c] — несколько разделителей
|
||||
println("a,b,c".split(",", limit = 2)) // [a, b,c]
|
||||
println("a\nb".lines()) // [a, b] — по переводам строк
|
||||
println(listOf("a","b").joinToString("-")) // a-b
|
||||
println(listOf(1,2,3).joinToString(
|
||||
separator = ", ", prefix = "[", postfix = "]")) // [1, 2, 3]
|
||||
println("hello".chunked(2)) // [he, ll, o]
|
||||
```
|
||||
`joinToString` - основной способ собрать строку из коллекции, у него есть и `transform`:
|
||||
```kotlin
|
||||
println(listOf(1, 2).joinToString { "#$it" }) // #1, #2
|
||||
```
|
||||
|
||||
## Преобразование в число
|
||||
```kotlin
|
||||
println("42".toInt()) // 42
|
||||
println("42".toIntOrNull()) // 42
|
||||
println("abc".toIntOrNull()) // null — не бросает исключение
|
||||
println("3.14".toDouble()) // 3.14
|
||||
println("ff".toInt(16)) // 255 — с основанием
|
||||
// println("abc".toInt()) // NumberFormatException
|
||||
```
|
||||
В коде, который разбирает внешние данные, используйте только версии `*OrNull`.
|
||||
|
||||
## StringBuilder и buildString
|
||||
Для сборки строки в цикле конкатенация создаёт мусор - на каждой итерации новый объект. `StringBuilder` меняет один буфер:
|
||||
```kotlin
|
||||
val sb = StringBuilder()
|
||||
for (i in 1..3) sb.append(i).append(",")
|
||||
println(sb.toString()) // 1,2,3,
|
||||
```
|
||||
Идиоматичнее - `buildString`, он сам создаёт билдер и возвращает результат:
|
||||
```kotlin
|
||||
val s = buildString {
|
||||
append("Привет")
|
||||
append(", ")
|
||||
append("Kotlin")
|
||||
appendLine("!")
|
||||
append("Строка 2")
|
||||
}
|
||||
println(s)
|
||||
```
|
||||
Полезные методы билдера: `append`, `appendLine`, `insert(i, x)`, `deleteAt(i)`, `setLength(n)`, `reverse()`, `clear()`.
|
||||
|
||||
## Многострочные строки
|
||||
```kotlin
|
||||
val json = """
|
||||
{"name": "Kotlin"}
|
||||
""".trimIndent()
|
||||
```
|
||||
Подробнее: [Строковые шаблоны и вывод](../Базовый%20синтаксис/Строковые%20шаблоны%20и%20вывод.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `s.equals(t)` | `s == t` |
|
||||
> | `s == t` (ссылки) | `s === t` |
|
||||
> | `s.charAt(0)` | `s[0]` |
|
||||
> | `s.isEmpty()` | `s.isEmpty()`, плюс `isBlank()`, `isNullOrBlank()` |
|
||||
> | `s.toUpperCase()` | `s.uppercase()` |
|
||||
> | `new StringBuilder()` + `toString()` | `buildString { }` |
|
||||
> | `String.join(",", list)` | `list.joinToString(",")` |
|
||||
> | `Integer.parseInt(s)` | `s.toInt()` / `s.toIntOrNull()` |
|
||||
> | `s.split(",")` → `String[]` | → `List<String>` |
|
||||
> | `s.substring(100)` падает | `s.take(100)` безопасен |
|
||||
>
|
||||
> Главное приобретение - `*OrNull`-функции и безопасные `take`/`drop` вместо исключений на каждой опечатке во входных данных.
|
||||
>
|
||||
> `uppercase()` вместо `toUpperCase()` - не просто переименование: старая версия Java зависела от системной локали, из-за чего `"i".toUpperCase()` в турецкой локали давало `İ`. Kotlin по умолчанию использует `Locale.ROOT`.
|
||||
>
|
||||
> Сравнить: [Строки в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Строки.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`length` считает кодовые единицы UTF-16, а не символы.** Эмодзи и редкие иероглифы занимают две:
|
||||
> ```kotlin
|
||||
> println("😀".length) // 2, а не 1
|
||||
> println("😀".count()) // 2 — то же самое
|
||||
> println("😀".codePointCount(0, "😀".length)) // 1 — настоящее число символов
|
||||
> ```
|
||||
> Разрезать такую строку по индексу - значит получить битые символы.
|
||||
>
|
||||
> **`split` с пустым разделителем и пустой строкой дают неожиданный результат:**
|
||||
> ```kotlin
|
||||
> println("".split(",")) // [] — один пустой элемент, размер 1
|
||||
> println("".split(",").size) // 1
|
||||
> println("a,,b".split(",")) // [a, , b] — пустой элемент сохраняется
|
||||
> ```
|
||||
>
|
||||
> **`trimIndent` не обрезает пробелы внутри строк** - только общий отступ.
|
||||
>
|
||||
> **`replace` принимает строку, а не regex** - в отличие от Java:
|
||||
> ```kotlin
|
||||
> println("a.b".replace(".", "-")) // a-b — точка как символ
|
||||
> println("a.b".replace(Regex("."), "-")) // --- — regex
|
||||
> ```
|
||||
> В Java `String.replaceAll` работает с regex, и это частый источник путаницы при переходе.
|
||||
>
|
||||
> **Строка в `when` сравнивается по значению, но полнота не проверяется** - для `String` всегда нужен `else`.
|
||||
|
||||
Дополнительно: [Char](Char.md)
|
||||
Далее: [Char](Char.md)
|
||||
@@ -0,0 +1,280 @@
|
||||
---
|
||||
title: Функции
|
||||
description: Объявление функций, значения по умолчанию, именованные аргументы, vararg и локальные функции
|
||||
tags:
|
||||
- kotlin
|
||||
- syntax
|
||||
---
|
||||
|
||||
Функция объявляется словом `fun`. В Kotlin она может жить на верхнем уровне файла, внутри класса или внутри другой функции.
|
||||
|
||||
```kotlin
|
||||
fun greet(name: String): String {
|
||||
return "Привет, $name"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(greet("Kotlin")) // Привет, Kotlin
|
||||
}
|
||||
```
|
||||
Порядок: `fun` → имя → параметры → `:` тип результата → тело.
|
||||
|
||||
## Выражение вместо тела
|
||||
Если функция - одно выражение, фигурные скобки и `return` убираются:
|
||||
```kotlin
|
||||
fun square(x: Int): Int = x * x
|
||||
fun greet(name: String) = "Привет, $name" // тип выведен: String
|
||||
fun max(a: Int, b: Int) = if (a > b) a else b
|
||||
```
|
||||
Это основная форма в идиоматичном Kotlin. Тип результата можно опустить - компилятор выведет его. Но для публичного API его лучше писать явно: это контракт, и его случайное изменение не должно быть незаметным.
|
||||
|
||||
## Unit - отсутствие результата
|
||||
```kotlin
|
||||
fun log(message: String): Unit {
|
||||
println(message)
|
||||
}
|
||||
|
||||
fun log2(message: String) { // : Unit можно опустить
|
||||
println(message)
|
||||
}
|
||||
```
|
||||
`Unit` - настоящий тип с единственным значением, а не пустота. Подробнее: [Any, Unit, Nothing](../Базовые%20типы%20ООП/Any,%20Unit,%20Nothing.md)
|
||||
|
||||
## Параметры
|
||||
Параметры всегда **только для чтения** - это `val`, присвоить им нельзя:
|
||||
```kotlin
|
||||
fun bad(x: Int) {
|
||||
// x = 5 // ошибка компиляции
|
||||
val y = x + 1
|
||||
println(y)
|
||||
}
|
||||
```
|
||||
Тип параметра обязателен - вывода типов здесь нет.
|
||||
|
||||
## Значения по умолчанию
|
||||
```kotlin
|
||||
fun connect(host: String, port: Int = 8080, secure: Boolean = false) {
|
||||
println("$host:$port secure=$secure")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
connect("localhost") // localhost:8080 secure=false
|
||||
connect("localhost", 443) // localhost:443 secure=false
|
||||
connect("localhost", 443, true) // localhost:443 secure=true
|
||||
}
|
||||
```
|
||||
Это заменяет перегрузки: вместо трёх версий метода - одна функция. Значением по умолчанию может быть любое выражение, включая вызов функции и другие параметры:
|
||||
```kotlin
|
||||
fun repeat2(text: String, times: Int = text.length) = text.repeat(times)
|
||||
|
||||
fun main() {
|
||||
println(repeat2("ab")) // abab — times = 2
|
||||
println(repeat2("ab", 3)) // ababab
|
||||
}
|
||||
```
|
||||
Выражение вычисляется при каждом вызове, в котором аргумент опущен.
|
||||
|
||||
## Именованные аргументы
|
||||
Аргумент можно передать по имени параметра:
|
||||
```kotlin
|
||||
fun connect(host: String, port: Int = 80, secure: Boolean = false, timeout: Int = 30) {
|
||||
println("$host:$port s=$secure t=$timeout")
|
||||
}
|
||||
|
||||
fun main() {
|
||||
connect("localhost", secure = true) // пропустили port
|
||||
connect(host = "localhost", timeout = 60)
|
||||
connect(port = 443, host = "localhost") // порядок не важен
|
||||
}
|
||||
```
|
||||
Две причины использовать:
|
||||
1. **Пропустить** часть параметров со значениями по умолчанию
|
||||
2. **Читаемость** на вызовах с непонятными литералами
|
||||
|
||||
```kotlin
|
||||
// что здесь true?
|
||||
// createUser("Иван", true, false, true)
|
||||
// так понятно:
|
||||
// createUser("Иван", admin = true, banned = false, verified = true)
|
||||
```
|
||||
|
||||
Правило: позиционные аргументы идут до именованных. После первого именованного все следующие тоже должны быть именованными - кроме `vararg` со spread.
|
||||
|
||||
## vararg
|
||||
Переменное число аргументов:
|
||||
```kotlin
|
||||
fun sum(vararg nums: Int): Int {
|
||||
var total = 0
|
||||
for (n in nums) total += n // nums имеет тип IntArray
|
||||
return total
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(sum(1, 2, 3)) // 6
|
||||
println(sum()) // 0
|
||||
val arr = intArrayOf(1, 2, 3)
|
||||
println(sum(*arr)) // 6 — spread-оператор обязателен
|
||||
}
|
||||
```
|
||||
Правила:
|
||||
- `vararg` может быть **только один** в функции
|
||||
- он не обязан быть последним, но тогда остальные передаются по имени
|
||||
- внутри функции это массив (`Array<T>` или специализированный)
|
||||
- для передачи готового массива нужен `*` (spread)
|
||||
|
||||
```kotlin
|
||||
fun tag(vararg classes: String, id: String) = "$id: ${classes.joinToString()}"
|
||||
|
||||
fun main() {
|
||||
println(tag("a", "b", id = "x")) // x: a, b
|
||||
}
|
||||
```
|
||||
|
||||
## Локальные функции
|
||||
Функция внутри функции - видит переменные внешней:
|
||||
```kotlin
|
||||
fun process(items: List<Int>): List<String> {
|
||||
val prefix = ">> " // захватывается
|
||||
|
||||
fun format(x: Int): String = "$prefix$x" // локальная функция
|
||||
|
||||
return items.map { format(it) }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(process(listOf(1, 2))) // [>> 1, >> 2]
|
||||
}
|
||||
```
|
||||
Нужны, когда логика используется только внутри одной функции и не стоит того, чтобы выносить её в класс или в приватную функцию файла.
|
||||
|
||||
## Перегрузка
|
||||
Функции с одним именем и разными параметрами:
|
||||
```kotlin
|
||||
fun show(x: Int) = "число $x"
|
||||
fun show(x: String) = "строка $x"
|
||||
fun show(x: Int, y: Int) = "пара $x,$y"
|
||||
|
||||
fun main() {
|
||||
println(show(1)) // число 1
|
||||
println(show("a")) // строка a
|
||||
println(show(1, 2)) // пара 1,2
|
||||
}
|
||||
```
|
||||
Сигнатуру образуют имя и типы параметров. Тип результата в неё **не входит** - две функции, отличающиеся только им, не скомпилируются.
|
||||
|
||||
Чаще всего перегрузка не нужна: её заменяют значения по умолчанию.
|
||||
|
||||
## Передача аргументов
|
||||
Аргументы передаются **по значению**, как в Java:
|
||||
- примитив - копируется значение
|
||||
- объект - копируется ссылка
|
||||
|
||||
Значит, переприсвоить параметр внутри функции нельзя (он `val`), но изменяемый объект изменить можно:
|
||||
```kotlin
|
||||
fun addItem(list: MutableList<Int>) {
|
||||
list.add(99) // объект изменён — виден снаружи
|
||||
}
|
||||
|
||||
fun main() {
|
||||
val items = mutableListOf(1)
|
||||
addItem(items)
|
||||
println(items) // [1, 99]
|
||||
}
|
||||
```
|
||||
|
||||
## Однострочные функции-расширения
|
||||
Функции можно добавлять к существующим типам:
|
||||
```kotlin
|
||||
fun String.shout() = uppercase() + "!"
|
||||
|
||||
fun main() {
|
||||
println("привет".shout()) // ПРИВЕТ!
|
||||
}
|
||||
```
|
||||
Подробнее: [Функции-расширения](../../Функциональный%20Kotlin/Функции-расширения.md)
|
||||
|
||||
## Функции как значения
|
||||
Функция может быть параметром и результатом:
|
||||
```kotlin
|
||||
fun apply2(x: Int, f: (Int) -> Int) = f(x)
|
||||
|
||||
fun main() {
|
||||
println(apply2(3) { it * 2 }) // 6
|
||||
println(apply2(3, ::square)) // 9 — ссылка на функцию
|
||||
}
|
||||
|
||||
fun square(x: Int) = x * x
|
||||
```
|
||||
Подробнее: [Лямбды и функциональные типы](Лямбды%20и%20функциональные%20типы.md), [Функции высшего порядка](../../Функциональный%20Kotlin/Функции%20высшего%20порядка.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | метод только внутри класса | функция на верхнем уровне |
|
||||
> | `int f(int x) { return x; }` | `fun f(x: Int) = x` |
|
||||
> | `void` | `Unit` (можно опустить) |
|
||||
> | нет значений по умолчанию | `fun f(x: Int = 0)` |
|
||||
> | нет именованных аргументов | `f(x = 1)` |
|
||||
> | `int... nums` | `vararg nums: Int` |
|
||||
> | `f(arr)` для varargs | `f(*arr)` |
|
||||
> | параметр можно переприсвоить | параметр - `val` |
|
||||
> | перегрузки для опциональных параметров | одна функция со значениями по умолчанию |
|
||||
>
|
||||
> Значения по умолчанию + именованные аргументы убирают главный источник шума в Java API - «телескопические» перегрузки:
|
||||
> ```java
|
||||
> void connect(String host) { connect(host, 8080); }
|
||||
> void connect(String host, int port) { connect(host, port, false); }
|
||||
> void connect(String host, int port, boolean secure) { ... }
|
||||
> ```
|
||||
> В Kotlin это одна строка. Для вызова из Java есть `@JvmOverloads`, который генерирует такие перегрузки автоматически - см. [Kotlin из Java](../../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md).
|
||||
>
|
||||
> Сравнить: [Функции в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Изменяемое значение по умолчанию вычисляется на каждом вызове** - в отличие от Python, где оно создаётся один раз. Это как раз правильное поведение:
|
||||
> ```kotlin
|
||||
> fun add(item: Int, list: MutableList<Int> = mutableListOf()): List<Int> {
|
||||
> list.add(item)
|
||||
> return list
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> println(add(1)) // [1]
|
||||
> println(add(2)) // [2] — новый список, а не [1, 2]
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Значения по умолчанию не видны из Java.** Без `@JvmOverloads` Java-код обязан передавать все аргументы.
|
||||
>
|
||||
> **Переопределённый метод не может изменить значения по умолчанию:**
|
||||
> ```kotlin
|
||||
> open class A { open fun f(x: Int = 1) = x }
|
||||
> class B : A() {
|
||||
> override fun f(x: Int) = x * 2 // значение по умолчанию наследуется,
|
||||
> } // переобъявить его нельзя
|
||||
> ```
|
||||
>
|
||||
> **`vararg` без spread принимает массив как один элемент:**
|
||||
> ```kotlin
|
||||
> fun show(vararg items: Any) = items.size
|
||||
>
|
||||
> fun main() {
|
||||
> val arr = arrayOf(1, 2, 3)
|
||||
> println(show(arr)) // 1 — массив целиком как один аргумент
|
||||
> println(show(*arr)) // 3 — распакован
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Перегрузка + значения по умолчанию = неоднозначность.** Компилятор выберет наиболее специфичную перегрузку, а не ту, что со значениями по умолчанию:
|
||||
> ```kotlin
|
||||
> fun f(x: Int, y: Int = 0) = "два"
|
||||
> fun f(x: Int) = "один"
|
||||
>
|
||||
> fun main() {
|
||||
> println(f(1)) // "один" — перегрузка без default побеждает
|
||||
> }
|
||||
> ```
|
||||
> Такого сочетания лучше избегать.
|
||||
|
||||
Дополнительно: [Лямбды и функциональные типы](Лямбды%20и%20функциональные%20типы.md)
|
||||
Далее: [Лямбды и функциональные типы](Лямбды%20и%20функциональные%20типы.md)
|
||||
@@ -0,0 +1,231 @@
|
||||
---
|
||||
title: Числа и переполнение
|
||||
description: Переполнение целых, точность Double, BigDecimal для денег и математические функции
|
||||
tags:
|
||||
- kotlin
|
||||
- stdlib
|
||||
---
|
||||
|
||||
## Переполнение целых
|
||||
Kotlin, как и Java, **не проверяет переполнение**. При выходе за границы значение «заворачивается»:
|
||||
```kotlin
|
||||
println(Int.MAX_VALUE) // 2147483647
|
||||
println(Int.MAX_VALUE + 1) // -2147483648 — тихо
|
||||
println(Int.MIN_VALUE - 1) // 2147483647
|
||||
```
|
||||
Это не исключение и даже не предупреждение - просто неверный результат.
|
||||
|
||||
Границы типов:
|
||||
```kotlin
|
||||
println("${Byte.MIN_VALUE}..${Byte.MAX_VALUE}") // -128..127
|
||||
println("${Int.MIN_VALUE}..${Int.MAX_VALUE}") // -2147483648..2147483647
|
||||
println("${Long.MIN_VALUE}..${Long.MAX_VALUE}") // -9223372036854775808..9223372036854775807
|
||||
```
|
||||
|
||||
### Где это кусает
|
||||
Самый частый случай - умножение `Int`, результат которого не влезает:
|
||||
```kotlin
|
||||
val ms = 1000 * 60 * 60 * 24 * 30 // -1702967296 — переполнение!
|
||||
val msOk = 1000L * 60 * 60 * 24 * 30 // 2592000000 — первый операнд Long
|
||||
println("$ms $msOk")
|
||||
```
|
||||
Правило: если промежуточный результат может превысить 2 млрд, делайте `Long` **первый** операнд. Приписать `L` к последнему множителю недостаточно - умножение идёт слева направо.
|
||||
|
||||
### Проверяемая арифметика
|
||||
Явные функции, бросающие исключение при переполнении:
|
||||
```kotlin
|
||||
println(Math.addExact(1, 2)) // 3
|
||||
// println(Math.addExact(Int.MAX_VALUE, 1)) // ArithmeticException
|
||||
// println(Math.multiplyExact(100000, 100000)) // ArithmeticException
|
||||
```
|
||||
Доступны `addExact`, `subtractExact`, `multiplyExact`, `incrementExact`, `toIntExact`. Это функции JDK, в stdlib Kotlin аналогов нет.
|
||||
|
||||
## Деление
|
||||
```kotlin
|
||||
println(7 / 2) // 3 — целочисленное, дробная часть отброшена
|
||||
println(-7 / 2) // -3 — округление к нулю, не к меньшему
|
||||
println(7 % 2) // 1
|
||||
println(-7 % 2) // -1 — знак от делимого
|
||||
println(7.0 / 2) // 3.5
|
||||
```
|
||||
Деление на целочисленный ноль - исключение, на вещественный - нет:
|
||||
```kotlin
|
||||
// println(1 / 0) // ArithmeticException: / by zero
|
||||
println(1.0 / 0) // Infinity
|
||||
println(-1.0 / 0) // -Infinity
|
||||
println(0.0 / 0) // NaN
|
||||
```
|
||||
|
||||
### Остаток для отрицательных
|
||||
`%` в Kotlin даёт остаток со знаком делимого, что ломает индексацию по кругу:
|
||||
```kotlin
|
||||
println(-1 % 3) // -1 — не 2!
|
||||
println((-1).mod(3)) // 2 — математический модуль
|
||||
println((-1).rem(3)) // -1 — то же, что %
|
||||
```
|
||||
Для циклических индексов нужен `mod`, а не `%`.
|
||||
|
||||
## Вещественные числа
|
||||
`Float` и `Double` следуют IEEE 754 и **не представляют десятичные дроби точно**:
|
||||
```kotlin
|
||||
println(0.1 + 0.2) // 0.30000000000000004
|
||||
println(0.1 + 0.2 == 0.3) // false
|
||||
println(1.0 - 0.9) // 0.09999999999999998
|
||||
```
|
||||
|
||||
Сравнивать нужно с допуском:
|
||||
```kotlin
|
||||
fun almostEqual(a: Double, b: Double, eps: Double = 1e-9) =
|
||||
kotlin.math.abs(a - b) < eps
|
||||
|
||||
println(almostEqual(0.1 + 0.2, 0.3)) // true
|
||||
```
|
||||
|
||||
### NaN и бесконечности
|
||||
```kotlin
|
||||
val nan = Double.NaN
|
||||
println(nan == nan) // false! NaN не равен ничему, включая себя
|
||||
println(nan.isNaN()) // true — единственный верный способ проверки
|
||||
println(Double.POSITIVE_INFINITY) // Infinity
|
||||
println(1.0.isFinite()) // true
|
||||
println(0.0 == -0.0) // true
|
||||
```
|
||||
|
||||
`NaN` ведёт себя по-разному в операторах и в коллекциях - это редкая, но болезненная ловушка:
|
||||
```kotlin
|
||||
println(Double.NaN == Double.NaN) // false — оператор
|
||||
println(Double.NaN.equals(Double.NaN)) // true — метод equals
|
||||
println(listOf(Double.NaN).contains(Double.NaN)) // true — через equals
|
||||
```
|
||||
|
||||
## Деньги - BigDecimal
|
||||
Для денег `Double` использовать нельзя: ошибки округления накапливаются.
|
||||
```kotlin
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
|
||||
fun main() {
|
||||
val a = BigDecimal("0.1") // только из строки!
|
||||
val b = BigDecimal("0.2")
|
||||
println(a + b) // 0.3 — точно
|
||||
println(a.add(b) == BigDecimal("0.3")) // true
|
||||
|
||||
val price = BigDecimal("100.555")
|
||||
println(price.setScale(2, RoundingMode.HALF_UP)) // 100.56
|
||||
|
||||
// операторы перегружены, работают привычно
|
||||
println(BigDecimal("10") * BigDecimal("3")) // 30
|
||||
println(BigDecimal("10").divide(BigDecimal("4"))) // 2.5
|
||||
}
|
||||
```
|
||||
Создавать **только из строки**: `BigDecimal(0.1)` подхватит уже испорченное значение `Double`:
|
||||
```kotlin
|
||||
import java.math.BigDecimal
|
||||
|
||||
fun main() {
|
||||
println(BigDecimal(0.1)) // 0.1000000000000000055511151231257827...
|
||||
println(BigDecimal("0.1")) // 0.1
|
||||
}
|
||||
```
|
||||
|
||||
`BigDecimal.equals` сравнивает и масштаб, поэтому `2.0` и `2.00` не равны:
|
||||
```kotlin
|
||||
import java.math.BigDecimal
|
||||
|
||||
fun main() {
|
||||
println(BigDecimal("2.0") == BigDecimal("2.00")) // false
|
||||
println(BigDecimal("2.0").compareTo(BigDecimal("2.00")) == 0) // true
|
||||
}
|
||||
```
|
||||
Для сравнения значений - `compareTo`, не `==`.
|
||||
|
||||
## Математические функции
|
||||
```kotlin
|
||||
import kotlin.math.*
|
||||
|
||||
fun main() {
|
||||
println(abs(-5)) // 5
|
||||
println(max(3, 7)) // 7
|
||||
println(min(3, 7)) // 3
|
||||
println(sqrt(16.0)) // 4.0
|
||||
println(2.0.pow(10)) // 1024.0
|
||||
println(PI) // 3.141592653589793
|
||||
println(round(2.5)) // 2.0 — не 3.0! округление к чётному
|
||||
println(round(3.5)) // 4.0
|
||||
println(floor(2.9)) // 2.0
|
||||
println(ceil(2.1)) // 3.0
|
||||
println(truncate(2.9)) // 2.0
|
||||
println(2.567.roundToInt()) // 3
|
||||
println(hypot(3.0, 4.0)) // 5.0
|
||||
}
|
||||
```
|
||||
Пакет `kotlin.math` нужно импортировать - автоматически он не подключается.
|
||||
|
||||
Обратите внимание на `round`: при ровной половине он округляет **к ближайшему чётному** (IEEE 754 round-half-even), поэтому `round(2.5)` даёт `2.0`, а `round(3.5)` - `4.0`. Если нужно привычное «половина вверх», берите `roundToInt()`: `2.5.roundToInt()` даёт `3`.
|
||||
|
||||
### Ограничение диапазона
|
||||
```kotlin
|
||||
println(15.coerceIn(0, 10)) // 10
|
||||
println((-5).coerceIn(0, 10)) // 0
|
||||
println(5.coerceAtLeast(0)) // 5
|
||||
println(15.coerceAtMost(10)) // 10
|
||||
```
|
||||
Удобнее, чем `max(0, min(10, x))`.
|
||||
|
||||
## Преобразования и потеря данных
|
||||
```kotlin
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun main() {
|
||||
println(300.toByte()) // 44 — старшие биты отброшены
|
||||
println(3.99.toInt()) // 3 — отбрасывание, не округление
|
||||
println((-3.99).toInt()) // -3
|
||||
println(3.99.roundToInt()) // 4 — настоящее округление
|
||||
println(Double.NaN.toInt()) // 0 — тихо!
|
||||
println(1e20.toInt()) // 2147483647 — обрезано до MAX_VALUE
|
||||
}
|
||||
```
|
||||
`toInt()` на `Double` никогда не падает - он тихо даёт неверный результат. Для контроля используйте `roundToInt()` и проверку `isFinite()`.
|
||||
|
||||
## Представление и биты
|
||||
```kotlin
|
||||
println(255.toString(2)) // 11111111 — в двоичной
|
||||
println(255.toString(16)) // ff
|
||||
println("ff".toInt(16)) // 255
|
||||
println(5.countOneBits()) // 2 — число единичных битов
|
||||
println(Integer.toBinaryString(-1)) // 32 единицы
|
||||
```
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | `Math.abs(x)` | `kotlin.math.abs(x)` |
|
||||
> | `Math.pow(2, 10)` | `2.0.pow(10)` |
|
||||
> | `Integer.MAX_VALUE` | `Int.MAX_VALUE` |
|
||||
> | `Integer.parseInt(s)` | `s.toInt()` |
|
||||
> | `Integer.toBinaryString(n)` | `n.toString(2)` |
|
||||
> | `Math.floorMod(-1, 3)` | `(-1).mod(3)` |
|
||||
> | неявное `int` → `long` | только явный `toLong()` |
|
||||
> | `Math.addExact` | то же (из JDK) |
|
||||
>
|
||||
> Поведение переполнения, деления и IEEE 754 полностью унаследовано от JVM - здесь Kotlin ничего не меняет и не защищает. Выигрыш только в читаемости: `2.0.pow(10)` вместо `Math.pow(2, 10)`, `coerceIn` вместо вложенных `min`/`max`.
|
||||
>
|
||||
> Запрет неявных преобразований - реальная защита: в Java `long total = intA * intB;` переполняется до присваивания, и компилятор молчит. В Kotlin такой код не соберётся без явного `toLong()`.
|
||||
>
|
||||
> Сравнить: [Типы данных в Java](../../../Программирование%20на%20Java/Синтаксис%20и%20типы/Базовый%20синтаксис/Типы%20данных.md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **`1000 * 60 * 60 * 24 * 30` переполняется** - самая частая реальная ошибка с числами. Все операнды `Int`, результат считается в `Int`, и только потом присваивается в `Long`.
|
||||
>
|
||||
> **`Double` для денег даёт расхождения в копейках**, которые накапливаются на больших объёмах. Только `BigDecimal` из строки или целые копейки в `Long`.
|
||||
>
|
||||
> **`NaN == NaN` ложно, а `listOf(NaN).contains(NaN)` истинно.** Сортировка и `distinct()` с `NaN` ведут себя неинтуитивно.
|
||||
>
|
||||
> **`toInt()` на `Double` обрезает, а `NaN` превращает в 0** - без единого предупреждения.
|
||||
>
|
||||
> **`%` для отрицательных чисел даёт отрицательный остаток.** Код `arr[(i - 1) % n]` упадёт на `i = 0`; нужен `arr[(i - 1).mod(n)]`.
|
||||
>
|
||||
> **`BigDecimal("2.0") != BigDecimal("2.00")`** - `equals` учитывает масштаб. Сравнивайте через `compareTo`.
|
||||
|
||||
Дополнительно: [Типы данных](../Базовый%20синтаксис/Типы%20данных.md)
|
||||
Далее: [Массивы](Массивы.md)
|
||||
Reference in New Issue
Block a user