1. Оператор []: чтение и запись
Теперь перейдём к тому, ради чего всё затевалось: как получить элемент из массива и как его записать. В Kotlin оператор [] — это стандартный способ доступа к элементам массива: a[i] читает значение, а a[i] = ... записывает новое.
Начнём с самого маленького примера: прочитать, изменить, снова прочитать.
fun main() {
val steps = intArrayOf(3000, 5000, 4500)
println(steps[0]) // 3000
steps[0] = 7000
println(steps[0]) // 7000
}
Здесь важно заметить две вещи.
Во-первых, массивы изменяемы: их элементы можно менять, даже если переменная массива объявлена как val. val запрещает переприсвоить саму ссылку на массив, но не запрещает менять содержимое «коробки». Это нормальное поведение: «коробка та же, предметы внутри поменялись».
Во-вторых, запись steps[0] = 7000 работает только если индекс корректный. Если индекс неверный — программа упадёт во время выполнения.
2. Индексы и нумерация с нуля
Когда впервые видишь a[0], хочется спросить: «Почему не a[1]? Я же человек, а не робот». Ответ простой: так исторически сложилось почти во всех языках, и Kotlin здесь не исключение. Индекс — это смещение от начала массива: первый элемент находится на смещении 0. Это неудобно ровно первые два дня, а потом вы начинаете считать от нуля даже тетрадные страницы.
Главное правило на массива звучит так:
- первый элемент массива имеет индекс 0
- последний элемент массива имеет индекс size - 1
- допустимые индексы — от 0 до size - 1 включительно
Kotlin чётко придерживается этой модели: индексация массивов начинается с нуля, а доступ/изменение делаются через оператор [].
Давайте посмотрим на короткую иллюстрацию. Представим массив из трёх чисел:
значения: [ 50, 10, 99 ]
индексы: 0 1 2
size = 3
последний индекс = size - 1 = 2
Именно поэтому выражение a[3] для массива длины 3 — это попытка найти четвёртый элемент, которого нет.
3. size: сколько элементов и где граница
Если в массиве size элементов, то возникает соблазн думать так: «Раз размер 3, значит индексы 1..3». И вот тут Kotlin говорит вам «не-а»: size — это количество элементов, а не «максимальный индекс».
Если size = 3, то индексы: 0, 1, 2. Сравним в табличке:
массива |
Допустимые индексы | Последний индекс |
|---|---|---|
|
нет | — |
|
|
|
|
|
|
|
|
|
Кстати, обратите внимание на size = 0 (пустой массив): у него нет ни одного допустимого индекса. И поэтому любая попытка сделать a[0] приведёт к ошибке. Но сам такой массив имеет право на существование.
Мини-пример:
fun main() {
val a = intArrayOf(10, 20, 30)
println("size = ${a.size}") // size = 3
println("last index = ${a.size - 1}") // last index = 2
}
a.size - 1 — это выражение, которое вы будете писать очень часто. Сначала в программах, а потом и в реальном мире.
4. indices: правильный диапазон индексов
Если вы постоянно пишете 0 и size - 1, легко ошибиться на единицу. А ошибки «на 1» — это древнее проклятье программирования, которое питается вашими дедлайнами :)
Поэтому Kotlin даёт удобную штуку: a.indices.
indices — это готовый набор всех корректных индексов массива. Если массив длины 3, то indices примерно соответствует 0..2. Если массив пустой, indices будет «пустым диапазоном», и это тоже удобно: многие циклы просто ничего не сделают.
Самое приятное: indices можно использовать и в циклах, и в проверках.
Проверка индекса через in a.indices
Представьте, что пользователь вводит номер дня недели, а вы хотите записать шаги в массив. Пользователь — существо творческое: сегодня он вводит 3, завтра 999, послезавтра -5 (чтобы проверить, «а что будет»). Поэтому индекс нужно проверять.
fun main() {
val steps = intArrayOf(3000, 5000, 4500) // 3 дня для примера
val i = 2
if (i in steps.indices) {
println("steps[$i] = ${steps[i]}") // steps[2] = 4500
} else {
println("Некорректный индекс: $i")
}
}
Эта конструкция читается примерно так: «если i находится в допустимых индексах массива steps».
И да: in здесь — это именно проверка принадлежности: «лежит ли значение в диапазоне». Эта связка (in + indices) — один из самых базовых приёмов безопасной работы с массивами.
Обход массива через for (i in a.indices)
Сегодня мы не углубляемся в алгоритмы «сумма/максимум/поиск», но сам факт обхода по индексам нам полезен уже сейчас, потому что иногда нужен именно индекс, а не только значение.
fun main() {
val steps = intArrayOf(3000, 5000, 4500)
for (i in steps.indices) {
println("Day#$i: ${steps[i]}")
// Day#0: 3000
// Day#1: 5000
// Day#2: 4500
}
}
Почему это лучше, чем for (i in 0..steps.size - 1)? Потому что вы не пишете границы руками — меньше шансов ошибиться.
Что будет при выходе за границы
Есть вещи, которые Kotlin может проверить заранее (например, «нельзя сложить String и Int»). А есть вещи, которые зависят от данных во время выполнения. Индексы массива — как раз второй случай.
Если вы пишете:
val a = intArrayOf(1, 2, 3)
println(a[10])
Компилятор может не распознать вашу ошибку. Может, вы потом поменяете массив, может, индекс придёт из ввода. Поэтому код компилируется, но программа упадёт во время выполнения с исключением «выход за границы массива».
5. Трекер шагов и безопасное обновление
Чтобы примеры были реальнее, придумаем небольшую учебную программу, которую можно развивать дальше в рамках уровня про массивы. Пусть это будет опять консольный трекер шагов за неделю. Сейчас он максимально простой: у нас есть массив на 7 дней, мы спрашиваем у пользователя день (индекс) и новое значение шагов, и обновляем массив безопасно.
Ключевой момент: мы не пытаемся сделать «идеальный интерфейс», не используем команды и сложный парсинг — только то, что вы уже знаете.
fun main() {
val steps = IntArray(7) // по умолчанию все элементы = 0
println("Введите индекс дня (0..6):")
val day = readln().toIntOrNull()
println("Введите количество шагов:")
val value = readln().toIntOrNull()
if (day == null || value == null) {
println("Ошибка: нужно вводить целые числа.")
return
}
if (day in steps.indices) {
steps[day] = value
println("Ок: steps[$day] = ${steps[day]}")
} else {
println("Ошибка: день должен быть от 0 до 6.")
}
}
Обратите внимание на стиль «двойной защиты». Сначала мы проверяем, что ввод распарсился (toIntOrNull() дал не null). Затем мы проверяем, что индекс в границах. Это не паранойя, это реальная жизнь: пользователь может ввести что угодно, включая «котиков» и -100, а программа должна хотя бы не падать.
6. Массовые операции: fill() и copyOf()
Иногда с массивом нужно сделать «пакетную» операцию: быстро сбросить значения или, наоборот, сохранить копию (снимок) перед изменениями. Для этого в Kotlin есть простые, но очень полезные методы.
fill(value): быстро заполнить массив одним значением
Иногда нужно массово перезаписать массив: сбросить значения, поставить дефолт, заполнить «пустыми» данными. Делать это вручную циклом можно, но скучно: программисту положено лениться конструктивно.
Для этого есть метод fill(value): он проходит по массиву и записывает одно и то же значение во все элементы.
Представьте сценарий: мы ведём шаги за неделю, но хотим «начать заново» (например, новый челлендж). Тогда проще всего сделать сброс:
fun main() {
val steps = intArrayOf(1200, 5000, 4300, 0, 8000, 6500, 2000)
steps.fill(0)
for (x in steps) {
print("$x ")
}
// 0 0 0 0 0 0 0
}
Здесь важно не перепутать ожидания: fill(0) не меняет размер массива, он просто меняет значения элементов.
Если вы заполните IntArray(7) семёрками, то у вас будет семь элементов, и все равны 7, но никакой магии:
fun main() {
val steps = IntArray(7)
steps.fill(7000)
println(steps[0]) // 7000
println(steps[6]) // 7000
}
Это одна из тех операций, которые делают код короче и понятнее: «заполни массив значением X» читается лучше, чем цикл на 7 строк ради той же идеи.
copyOf(): копия массива vs «второе имя»
Сейчас будет одна из самых важных тем уровня — и одна из самых «подлых» для новичков.
Когда вы пишете:
val a = intArrayOf(1, 2, 3)
val b = a
вы не копируете массив. Вы создаёте вторую переменную, которая указывает на тот же самый массив. Это как два ярлыка на рабочем столе, ведущих в одну и ту же папку. Удалите файл через один ярлык — и через другой он тоже исчезнет.
А вот copyOf() создаёт новый массив, который изначально содержит те же значения, но существует отдельно.
Давайте посмотрим на разницу в коде. Сначала — просто ссылка без дублирования массива:
fun main() {
val original = intArrayOf(10, 20, 30)
val alias = original
alias[0] = 999
println("original[0] = ${original[0]}") // original[0] = 999
println("alias[0] = ${alias[0]}") // alias[0] = 999
}
Многие ожидают, что original останется (10, 20, 30), но нет: alias — это не копия, это тот же массив.
Теперь — правильная независимая копия:
fun main() {
val original = intArrayOf(10, 20, 30)
val copy = original.copyOf()
copy[0] = 999
println("original[0] = ${original[0]}") // original[0] = 10
println("copy[0] = ${copy[0]}") // copy[0] = 999
}
В трекере шагов это можно использовать как «снимок недели»: вы сохранили старые значения, а потом начали менять текущие.
Практика: снимок массива и сброс недели
Соединим две операции (copyOf() и fill()) в один небольшой, но практичный сценарий. Пусть программа делает так: у нас есть шаги за неделю, мы создаём копию как «архив», потом сбрасываем текущую неделю в нули. В реальной жизни это может быть «новая неделя начинается с чистого листа».
fun main() {
val steps = intArrayOf(1200, 5000, 4300, 0, 8000, 6500, 2000)
val archive = steps.copyOf()
steps.fill(0)
print("archive: ")
for (x in archive) print("$x ")
println()
// archive: 1200 5000 4300 0 8000 6500 2000
print("steps: ")
for (x in steps) print("$x ")
println()
// steps: 0 0 0 0 0 0 0
}
7. Типичные ошибки
Ошибка №1: думать, что последний индекс равен size.
Это классика: массив из 3 элементов, вы пишете a[3] и удивляетесь, почему программа падает. Правило простое: size — это количество элементов, а последний индекс — size - 1. Если вы не хотите держать это в голове постоянно, используйте a.indices для циклов и проверок, чтобы не вычислять границы вручную.
Ошибка №2: не проверять индекс, который пришёл из ввода (или из расчёта).
Пока индекс «вручную написан» и вы уверены, что он корректный, всё хорошо. Но как только индекс приходит от пользователя или вычисляется сложнее, он может стать отрицательным или слишком большим. Тогда обращение a[i] превращается в падение программы. Привычка if (i in a.indices) — это не «лишняя строка», а страховка от неуправляемого поведения.
Ошибка №3: считать, что val b = a создаёт копию массива.
Нет, это просто ещё одно имя для того же массива. Измените b[0] — изменится и a[0]. Если нужна независимая копия, используйте copyOf(). Это особенно важно, когда вы сохраняете «снимки» состояния: без copyOf() вы случайно испортите «архив», меняя «текущее».
Ошибка №4: пытаться «напечатать массив» через println(a) и ждать список элементов.
Для новичка это выглядит логично: «я же печатаю переменную». Но по умолчанию вы увидите не содержимое, а техническое представление объекта. Самый понятный для текущего уровня способ увидеть элементы — пройтись циклом и распечатать каждый элемент. Да, это чуть длиннее, зато вы точно понимаете, что происходит.
Ошибка №5: ожидать, что fill() изменит размер массива или добавит элементы.
fill(value) ничего не добавляет и не удаляет: он просто перезаписывает существующие элементы. Массив остаётся фиксированного размера — ровно таким, каким вы его создали в начале. Если хочется структуру «которая растёт», это будет отдельная тема позже, а сегодня держим в голове принцип: массив — это фиксированная коробка, и fill() лишь раскладывает в неё одинаковые предметы.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ