Avatar ·

Как получить индекс элемента в цикле forEach в Kotlin?

Привет! Я новичок в Kotlin, перехожу с Java. В Java я привык использовать обычный цикл for с индексом: ```java for (int i = 0; i < list.size(); i++) { System.out.println("Index: " + i + ", Value: " + list.get(i)); } ``` Но в Kotlin я вижу, что все рекомендуют использовать forEach: ```kotlin list.forEach { item -> println(item) } ``` Проблема в том, что мне часто нужно знать не только значение элемента, но и его индекс (позицию) в списке. Например, я делаю список задач в Android и хочу показывать порядковый номер каждой задачи. Или я хочу проверить, является ли элемент последним в списке. Я пробовал сделать так: ```kotlin var index = 0 list.forEach { item -> println("$index: $item") index++ } ``` Но это выглядит костыльно и ненадежно. Если внутри цикла будет break или return, индекс собьется. Еще я слышал, что в Kotlin есть функция withIndex(), но не понимаю, как её правильно использовать. Подскажите, как правильно и красиво получить индекс элемента в forEach? Есть ли стандартные функции для этого? Мне важно, чтобы код был идиоматичным для Kotlin, а не просто переписанным с Java. Теги: android, kotlin, for-loop, collections <<>>

Использование withIndex() — самый правильный способ

В Kotlin есть встроенная функция withIndex(), которая возвращает коллекцию объектов IndexedValue. Каждый такой объект содержит два поля: index (Int) и value (тип элемента). Это идиоматичный и безопасный способ получить индекс внутри forEach.

val list = listOf("Яблоко", "Банан", "Вишня")

list.withIndex().forEach { (index, item) ->
    println("Индекс $index: $item")
}
// Вывод:
// Индекс 0: Яблоко
// Индекс 1: Банан
// Индекс 2: Вишня

Обратите внимание на деструктуризацию в лямбде: (index, item) — это удобно и читаемо. Вы также можете использовать более явный синтаксис:

list.withIndex().forEach { indexedValue ->
    println("Индекс ${indexedValue.index}: ${indexedValue.value}")
}

Альтернатива: forEachIndexed

Есть ещё более короткий вариант — функция-расширение forEachIndexed. Она принимает лямбду с двумя параметрами: индекс и значение.

list.forEachIndexed { index, item ->
    println("Индекс $index: $item")
}

Это, пожалуй, самый лаконичный способ. Но если вам нужно не просто выполнить действие, а получить новую коллекцию (например, отфильтровать или преобразовать), лучше использовать withIndex() в связке с другими функциями.

Примеры для реальных задач

  • Проверка, является ли элемент последним:
list.withIndex().forEach { (index, item) ->
    val isLast = index == list.lastIndex
    println("$item ${if (isLast) "(последний)" else ""}")
}
  • Создание списка с номерами (для RecyclerView):
data class Task(val title: String, val number: Int)

val tasks = listOf("Купить хлеб", "Помыть посуду", "Сделать ДЗ")
val numberedTasks = tasks.withIndex().map { (index, title) ->
    Task(title = title, number = index + 1)
}
// Result: [Task(title=Купить хлеб, number=1), Task(title=Помыть посуду, number=2), ...]
  • Фильтрация по индексу (например, каждый второй элемент):
val evenIndexedItems = list.withIndex()
    .filter { (index, _) -> index % 2 == 0 }
    .map { (_, item) -> item }
// Result: [Яблоко, Вишня]

Почему ваш ручной счётчик — плохая идея?

Ваш вариант с var index = 0 и index++ работает, но нарушает принципы функционального программирования, к которым стремится Kotlin. Проблемы:

  • Мутабельная переменная (var) — это источник ошибок в многопоточном коде.
  • Если внутри цикла использовать return (из лямбды с меткой) или break (через run), счётчик собьётся.
  • Код менее читаем: нужно помнить, что index объявлен снаружи.

Используйте withIndex() или forEachIndexed — они чище, безопаснее и короче.

Как быть с break/continue?

В обычном forEach нельзя использовать break или continue. Если вам нужен досрочный выход по индексу, используйте обычный цикл for:

for (i in list.indices) {
    if (i == 1) continue // пропустить индекс 1
    if (i == 3) break    // остановиться на индексе 3
    println("$i: ${list[i]}")
}

Но если вы всё же хотите остаться в функциональном стиле, используйте takeWhile или first — это более идиоматично, чем break.

<<>>

Использование mapIndexed для преобразования

Если ваша цель — не просто выполнить действие с элементом, а создать новую коллекцию на основе исходной с учётом индекса, mapIndexed будет удобнее, чем withIndex() + map.

val list = listOf("A", "B", "C")
val indexedList = list.mapIndexed { index, item ->
    "[$index] $item"
}
// Результат: ["[0] A", "[1] B", "[2] C"]

То же самое для фильтрации — используйте filterIndexed:

val oddIndexed = list.filterIndexed { index, _ -> index % 2 == 1 }
// Результат: ["B"]

Эти функции работают по тому же принципу, что и forEachIndexed, но возвращают новую коллекцию, а не Unit. Это делает код более декларативным и цепочным.

Когда какой способ выбрать?

  • Просто вывести индекс → forEachIndexed (коротко и ясно).
  • Преобразовать коллекцию → mapIndexed.
  • Отфильтровать с учётом индекса → filterIndexed.
  • Нужен объект с индексом для передачи в другую функцию → withIndex() (возвращает Iterable).

Совет: не используйте withIndex() только ради forEach — для этого есть forEachIndexed. Но если вам нужно несколько операций (фильтрация + маппинг + forEach), withIndex() позволяет сделать это в одной цепочке без потери информации об индексе.