🎯 Kotlin

Dateiarbeit in Kotlin: Eine ausführliche Anleitung

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты пишешь сочинение в тетради. Пока ты пишешь — информация есть, но как только ты закрываешь тетрадь и убираешь её в рюкзак, текст исчезает из твоей головы? Нет, он остаётся на бумаге. Завтра ты откроешь тетрадь снова и продолжишь писать с того же места.

Точно так же работает и компьютерная программа. Когда программа запущена — все её данные живут в оперативной памяти (как в голове). Но стоит программе закрыться — память очищается. Чтобы сохранить информацию навсегда, нужно записать её в файл — как в тетрадь. А чтобы потом прочитать — открыть файл и взять данные оттуда.

Работа с файлами — это умение программы:

  • Сохранять данные (записывать в файл)
  • Загружать данные (читать из файла)
  • Изменять данные (дописывать или перезаписывать)

Зачем это нужно в реальной жизни? Давай посмотрим на примеры:

  • Игры: твой прогресс, уровень, количество монеток сохраняются в файл. Когда ты выходишь из игры, а потом заходишь снова — всё на месте.
  • Текстовые редакторы: ты пишешь реферат, нажимаешь «Сохранить» — и файл .docx или .txt появляется на диске.
  • Социальные сети: твои фотографии, сообщения, настройки профиля — всё это хранится в файлах на серверах.
  • Базы данных: даже они в конечном счёте используют файлы для хранения информации.
💡 Важно понять: без работы с файлами любая программа — это «рыба без воды». Она может что-то считать, но не может ничего сохранить. Файлы делают программы полезными в реальном мире.

Теория: как это работает изнутри

Чтобы понять, как Kotlin работает с файлами, нужно разобраться с несколькими ключевыми понятиями. Не пугайся — всё очень логично.

Потоки данных (Streams)

Представь, что файл — это водопроводная труба. Чтобы получить воду (данные), нужно открыть кран. В программировании этот «кран» называется потоком (stream). Есть два типа потоков:

  • Входной поток (Input Stream) — для чтения данных из файла в программу. Вода течёт из трубы в твой стакан.
  • Выходной поток (Output Stream) — для записи данных из программы в файл. Вода течёт из твоего стакана обратно в трубу.

Текстовые и бинарные файлы

Файлы бывают двух видов:

  • Текстовые (.txt, .csv, .json, .xml) — хранят текст, который можно прочитать человеком. Каждый символ — это буква, цифра или знак.
  • Бинарные (.jpg, .mp3, .exe, .pdf) — хранят данные в виде нулей и единиц. Человек их не прочитает, зато компьютер обрабатывает быстро.

В этой статье мы сосредоточимся на текстовых файлах — они самые простые и понятные для новичков.

Путь к файлу (Path)

Чтобы компьютер нашёл файл, нужно указать его адрес — путь. Путь бывает:

  • Абсолютный: полный адрес от корня диска. Пример: C:UsersИмяDocumentsfile.txt (Windows) или /home/имя/documents/file.txt (Linux/Mac).
  • Относительный: адрес относительно текущей папки, где запущена программа. Пример: data/file.txt — значит, в папке data рядом с программой.
⚠️ Совет новичку: всегда используй относительные пути, если не уверен. Так программа будет работать на любом компьютере, а не только на твоём.

Режимы работы с файлами

Когда мы открываем файл, нужно сказать компьютеру, что мы собираемся с ним делать:

  • Чтение (Read) — только смотрим, ничего не меняем.
  • Запись (Write) — пишем заново, старые данные стираются.
  • Дозапись (Append) — дописываем в конец файла, старые данные остаются.

Кодировка

Компьютер хранит текст в виде чисел. Кодировка — это таблица соответствия между буквами и числами. Самая популярная — UTF-8. Она поддерживает все языки мира, включая русский. Всегда используй UTF-8, чтобы не было «кракозябр» вместо букв.

Синтаксис в Kotlin: все формы и варианты

Kotlin — современный язык, и он предлагает несколько способов работы с файлами. Мы рассмотрим самые удобные и популярные.

1. Класс File из стандартной библиотеки

В Kotlin есть встроенный класс java.io.File. Он умеет всё: читать, писать, проверять, удалять. Создать объект файла очень просто:

// Создаём объект File, который указывает на файл
val file = File("example.txt")
// Пока файл не создан на диске — это просто "ссылка" на него

2. Основные методы для чтения

Вот таблица самых полезных методов для чтения текстовых файлов:

Метод Что делает Возвращает
readText() Читает весь файл целиком одной строкой String
readLines() Читает файл и разбивает на строки List<String>
forEachLine { ... } Читает файл построчно, вызывая функцию для каждой строки Unit
useLines { ... } Читает строки как последовательность (лениво), сам закрывает файл Unit
bufferedReader() Создаёт буферизированный читатель для больших файлов BufferedReader
inputStream() Создаёт поток для чтения байтов (для бинарных файлов) InputStream

3. Основные методы для записи

Метод Что делает Особенности
writeText(text) Записывает текст в файл (перезаписывает) Старое содержимое удаляется
appendText(text) Дописывает текст в конец файла Старое содержимое сохраняется
printWriter() Создаёт писателя для форматированного вывода Нужно закрывать вручную или использовать use
bufferedWriter() Создаёт буферизированного писателя Эффективно для больших объёмов

4. Важное правило: закрывай файлы!

Когда ты открываешь файл, операционная система выделяет ресурсы. Если их не вернуть (не закрыть файл), может произойти утечка ресурсов. В Kotlin есть элегантное решение — функция use:

// use автоматически закроет файл, даже если произойдёт ошибка
File("example.txt").bufferedReader().use { reader ->
    val text = reader.readText()
    println(text)
} // файл закрыт!
✅ Золотое правило: если открыл файл — закрой его. Лучший способ — использовать use { }.

Примеры кода — от простого к сложному

Теперь перейдём к практике. Все примеры рабочие — можешь копировать и запускать.

Пример 1: Создание файла и запись текста

import java.io.File

fun main() {
    // 1. Создаём объект File
    val file = File("my_first_file.txt")
    
    // 2. Записываем текст в файл
    // writeText() создаёт файл, если его нет, и записывает строку
    file.writeText("Привет, мир! Это мой первый файл на Kotlin.")
    
    // 3. Проверяем, что файл создан
    println("Файл создан: ${file.exists()}")
    println("Размер файла: ${file.length()} байт")
}

Объяснение:

  • import java.io.File — подключаем класс File из пакета java.io
  • File("my_first_file.txt") — создаём объект, указывающий на файл в текущей папке
  • writeText(...) — записывает строку в файл. Если файла нет — создаёт. Если есть — перезаписывает.
  • file.exists() — проверяет, существует ли файл на диске
  • file.length() — возвращает размер файла в байтах

Пример 2: Чтение всего файла одной строкой

import java.io.File

fun main() {
    val file = File("my_first_file.txt")
    
    // 1. Проверяем, существует ли файл
    if (file.exists()) {
        // 2. Читаем всё содержимое в одну строку
        val content = file.readText()
        println("Содержимое файла:")
        println(content)
    } else {
        println("Файл не найден!")
    }
}

Объяснение:

  • file.exists() — проверка существования файла (всегда проверяй перед чтением!)
  • file.readText() — читает весь файл и возвращает строку. Удобно для маленьких файлов.

Пример 3: Чтение файла построчно

import java.io.File

fun main() {
    val file = File("poem.txt")
    
    // Сначала создадим файл с несколькими строками
    file.writeText("""Первая строка стихотворения
Вторая строка
Третья строка
А это четвёртая""")
    
    println("Читаем файл построчно:")
    
    // 1. readLines() возвращает список строк
    val lines: List<String> = file.readLines()
    
    // 2. Проходим по всем строкам с индексом
    for ((index, line) in lines.withIndex()) {
        println("Строка ${index + 1}: $line")
    }
}

Объяснение:

  • readLines() — читает файл и возвращает список строк. Каждый элемент списка — одна строка файла.
  • withIndex() — удобная функция Kotlin, которая даёт доступ к индексу элемента.
  • Обрати внимание на тройные кавычки """...""" — они позволяют писать многострочный текст без специальных символов.

Пример 4: Дозапись в конец файла (Append)

import java.io.File

fun main() {
    val file = File("diary.txt")
    
    // 1. Записываем первую запись
    file.writeText("День 1: Сегодня я начал изучать Kotlin!
")
    
    // 2. Дописываем вторую запись в конец
    file.appendText("День 2: Я научился работать с файлами!
")
    
    // 3. Дописываем третью запись
    file.appendText("День 3: Пишу свою первую программу!
")
    
    // 4. Читаем и выводим всё содержимое
    println("Мой дневник:")
    println(file.readText())
}

Объяснение:

  • writeText() — перезаписывает файл (старое содержимое удаляется)
  • appendText() — дописывает в конец файла (старое содержимое сохраняется)
  • Символ — это перенос строки. Без него всё было бы в одну строку.

Пример 5: Обработка больших файлов с forEachLine

import java.io.File

fun main() {
    val file = File("big_file.txt")
    
    // Создадим большой файл для примера
    file.writeText("")
    for (i in 1..1000) {
        file.appendText("Строка номер $i
")
    }
    
    println("Обрабатываем файл построчно (без загрузки всего в память):")
    
    var lineCount = 0
    var wordCount = 0
    
    // forEachLine читает по одной строке — экономит память
    file.forEachLine { line ->
        lineCount++
        // Считаем слова в строке (разделяем по пробелам)
        val words = line.split(" ")
        wordCount += words.size
    }
    
    println("Всего строк: $lineCount")
    println("Всего слов: $wordCount")
}

Объяснение:

  • forEachLine — читает файл построчно, не загружая весь файл в память. Идеально для больших файлов (сотни мегабайт).
  • Внутри фигурных скобок — лямбда-функция, которая вызывается для каждой строки.
  • split(" ") — разбивает строку на части по пробелам.

Пример 6: Использование use для безопасной работы

import java.io.File

fun main() {
    val file = File("safe_example.txt")
    
    // Записываем данные
    file.writeText("Безопасная работа с файлами
")
    
    // Читаем с использованием use — файл закроется автоматически
    file.bufferedReader().use { reader ->
        var line = reader.readLine()
        while (line != null) {
            println("Прочитано: $line")
            line = reader.readLine()
        }
    } // Здесь файл гарантированно закрыт
    
    println("Файл успешно прочитан и закрыт!")
}

Объяснение:

  • bufferedReader() — создаёт буферизированный читатель. Буфер ускоряет чтение.
  • .use { reader -> ... } — гарантирует, что файл будет закрыт, даже если внутри произойдёт ошибка.
  • readLine() — читает одну строку. Возвращает null, когда строки закончились.

Пример 7: Работа с CSV-файлом (табличные данные)

import java.io.File

fun main() {
    val file = File("students.csv")
    
    // Создаём CSV-файл (Comma Separated Values — значения, разделённые запятыми)
    file.writeText("Имя,Возраст,Оценка
")
    file.appendText("Анна,15,5
")
    file.appendText("Иван,16,4
")
    file.appendText("Мария,15,5
")
    
    println("Содержимое CSV-файла:")
    println(file.readText())
    
    // А теперь прочитаем и разберём данные
    println("
Разбор данных:")
    val lines = file.readLines()
    
    // Первая строка — заголовок, пропускаем её
    for (i in 1 until lines.size) {
        val parts = lines[i].split(",")
        if (parts.size == 3) {
            val name = parts[0]
            val age = parts[1]
            val grade = parts[2]
            println("Ученик: $name, возраст: $age, оценка: $grade")
        }
    }
}

Объяснение:

  • CSV — популярный формат для хранения табличных данных. Каждая строка — запись, значения разделены запятыми.
  • split(",") — разбивает строку на части по запятой.
  • until — создаёт диапазон от 1 до (размер - 1), пропуская заголовок.

Пример 8: Проверка и создание директорий

import java.io.File

fun main() {
    // Создаём путь к файлу во вложенной папке
    val directory = File("my_data/subfolder")
    val file = File(directory, "notes.txt")
    
    // 1. Проверяем, существует ли папка
    if (!directory.exists()) {
        // 2. Создаём все необходимые папки
        directory.mkdirs()
        println("Создана папка: ${directory.absolutePath}")
    }
    
    // 3. Теперь можно работать с файлом
    file.writeText("Файл в папке my_data/subfolder")
    
    // 4. Выводим информацию
    println("Файл создан: ${file.absolutePath}")
    println("Размер: ${file.length()} байт")
    println("Это файл? ${file.isFile}")
    println("Это папка? ${file.isDirectory}")
}

Объяснение:

  • File(directory, "notes.txt") — создаёт файл внутри указанной папки.
  • mkdirs() — создаёт все папки по пути, включая вложенные. Если папка уже есть — ничего не делает.
  • absolutePath — возвращает полный путь к файлу на диске.
  • isFile и isDirectory — проверяют, является ли объект файлом или папкой.

Сравнение с Python: чем похоже и чем отличается

Если ты уже знаком с Python, тебе будет легче. Давай сравним подходы:

Операция Kotlin Python
Открыть файл для чтения File("file.txt").bufferedReader() open("file.txt", "r")
Прочитать весь файл file.readText() file.read()
Прочитать строки file.readLines() file.readlines()
Записать текст file.writeText("text") file.write("text")
Дописать в конец file.appendText("text") file.write("text") (режим "a")
Безопасное закрытие .use { } with open(...) as f:
Проверка существования file.exists() os.path.exists("file.txt")

Главные отличия:

  • Kotlin использует объектно-ориентированный подход: File — это объект с методами. Всё лаконично и удобно.
  • Python использует функцию open(), которая возвращает файловый объект. Нужно явно указывать режим ("r", "w", "a").
  • Kotlin более строгий: нужно импортировать java.io.File. Python работает «из коробки».
  • Python требует явного закрытия файла или использования with. Kotlin делает это через use.
💡 Если ты знаешь Python — ты уже понимаешь 80% того, как работают файлы в Kotlin. Разница только в синтаксисе.

Типичные ошибки и подводные камни

Даже опытные программисты иногда допускают эти ошибки. Запомни их, чтобы не попасть в ловушку.

Ошибка 1: Забыли проверить существование файла

// ❌ Плохо: если файла нет — будет ошибка
val content = File("missing.txt").readText()

// ✅ Хорошо: проверяем существование
val file = File("missing.txt")
if (file.exists()) {
    val content = file.readText()
} else {
    println("Файл не найден!")
}

Ошибка 2: Не закрыли файл (утечка ресурсов)

// ❌ Плохо: файл остаётся открытым
val reader = File("file.txt").bufferedReader()
val text = reader.readText()
// reader не закрыт!

// ✅ Хорошо: use закрывает автоматически
val text = File("file.txt").bufferedReader().use { it.readText() }

Ошибка 3: Проблемы с кодировкой

// ❌ Плохо: может быть кракозябры с русскими буквами
File("file.txt").writeText("Привет")

// ✅ Хорошо: явно указываем UTF-8
File("file.txt").writeText("Привет", Charsets.UTF_8)
// Или при чтении:
val text = File("file.txt").readText(Charsets.UTF_8)

Ошибка 4: Игнорирование исключений

// ❌ Плохо: программа упадёт при ошибке
fun readFile(path: String): String {
    return File(path).readText() // Если файл занят другим процессом — будет крах
}

// ✅ Хорошо: обрабатываем исключения
fun readFileSafe(path: String): String? {
    return try {
        File(path).readText()
    } catch (e: Exception) {
        println("Ошибка при чтении файла: ${e.message}")
        null // Возвращаем null, чтобы программа не упала
    }
}

Ошибка 5: Неправильный путь к файлу

// ❌ Плохо: жёстко прописанный путь не будет работать на другом компьютере
val file = File("C:/Users/МойКомпьютер/Documents/file.txt")

// ✅ Хорошо: используем относительный путь
val file = File("data/file.txt")
// Или получаем путь относительно приложения
val file = File(System.getProperty("user.dir"), "data/file.txt")

Ошибка 6: Запись поверх существующих данных

// ❌ Плохо: writeText стирает старые данные
File("log.txt").writeText("Новая запись") // Старые логи потеряны!

// ✅ Хорошо: используем appendText для дописывания
File("log.txt").appendText("Новая запись
") // Старые логи сохранились
⚠️ Запомни: writeText() — это как стереть всё с доски и написать заново. appendText() — это как дописать в конец тетради.

Лучшие практики и советы профессионалов

Эти советы помогут тебе писать код, который не стыдно показать опытным разработчикам.

Совет 1: Всегда используй use для автоматического закрытия

Это самый важный совет. use гарантирует, что файл будет закрыт, даже если произойдёт исключение. Это делает код безопасным и чистым.

// Профессиональный стиль
fun readConfig(path: String): Map<String, String> {
    val config = mutableMapOf<String, String>()
    File(path).bufferedReader().use { reader ->
        reader.forEachLine { line ->
            val parts = line.split("=")
            if (parts.size == 2) {
                config[parts[0].trim()] = parts[1].trim()
            }
        }
    }
    return config
}

Совет 2: Используй расширения Kotlin для удобства

Kotlin позволяет добавлять свои методы к классу File. Это делает код ещё более читаемым.

// Создаём расширение для File
fun File.readTextSafe(): String? {
    return try {
        if (this.exists()) this.readText() else null
    } catch (e: Exception) {
        println("Ошибка: ${e.message}")
        null
    }
}

// Теперь можно использовать
fun main() {
    val text = File("config.txt").readTextSafe()
    println(text ?: "Файл не найден или ошибка чтения")
}

Совет 3: Работай с путями через Path (Java NIO)

Для сложных операций с путями используй java.nio.file.Path и Paths. Это даёт больше возможностей.

import java.nio.file.Paths
import java.nio.file.Files

fun main() {
    val path = Paths.get("data", "subfolder", "file.txt")
    
    // Создаём все папки по пути
    Files.createDirectories(path.parent)
    
    // Записываем и читаем
    Files.writeString(path, "Содержимое файла")
    val content = Files.readString(path)
    println(content)
}

Совет 4: Обрабатывай большие файлы с умом

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

// Для больших файлов (сотни МБ и ГБ)
fun processLargeFile(path: String) {
    File(path).useLines { lines ->
        lines.forEach { line ->
            // Обрабатываем каждую строку по отдельности
            processLine(line)
        }
    }
}

fun processLine(line: String) {
    // Какая-то обработка строки
    println("Обработано: ${line.take(50)}...")
}

Совет 5: Используй временные файлы для тестов

При тестировании не создавай файлы вручную. Используй временные файлы — они удаляются автоматически.

import java.io.File

fun main() {
    // Создаём временный файл
    val tempFile = File.createTempFile("test_", ".txt")
    tempFile.writeText("Тестовые данные")
    
    println("Временный файл: ${tempFile.absolutePath}")
    
    // Работаем с файлом
    println(tempFile.readText())
    
    // Удаляем временный файл
    tempFile.deleteOnExit() // Удалится при завершении программы
    // Или сразу:
    // tempFile.delete()
}

Совет 6: Логируй операции с файлами

В больших проектах полезно логировать, что происходит с файлами. Это помогает отлаживать ошибки.

fun safeWriteToFile(file: File, content: String) {
    println("Запись в файл: ${file.absolutePath}")
    println("Размер данных: ${content.length} символов")
    
    try {
        file.writeText(content)
        println("Успешно записано ${file.length()} байт")
    } catch (e: Exception) {
        println("ОШИБКА запи