В этом уроке мы сделаем маленькую модель товара: Product будет data class, функция рассчитает скидку, copy создаст измененную версию товара, а destructuring покажет, как удобно доставать поля. Это практический материал, а не сухой список возможностей Kotlin
К концу урока вы поймете, зачем нужен data class, чем val в конструкторе отличается от обычного параметра и почему copy часто лучше ручного пересоздания объекта
- Что получится в конце
- Функция в Kotlin
- Параметры по умолчанию
- Что дает data class
- copy: новая версия без ручного копирования
- Destructuring
- Домашка: товар с рейтингом
- Мини-практика: валидация скидки
- Функции-расширения
- Named arguments и читаемость
- Домашка: корзина товаров
- Когда data class не подходит
- Частые ошибки и порядок проверки
- Что может быть еще интересно по этой теме
- Что почитать дальше по Kotlin
Что получится в конце
Код:
data class Product(
val title: String,
val price: Int,
val category: String = "general"
)
fun discounted(product: Product, percent: Int): Product {
val discount = product.price * percent / 100
return product.copy(price = product.price - discount)
}
fun main() {
val keyboard = Product("Keyboard", 8000, "hardware")
val saleKeyboard = discounted(keyboard, 15)
println(keyboard)
println(saleKeyboard)
}
Ожидаемый вывод будет похож на:
Product(title=Keyboard, price=8000, category=hardware)
Product(title=Keyboard, price=6800, category=hardware)
Функция в Kotlin
Обычная функция:
fun discounted(product: Product, percent: Int): Product {
val discount = product.price * percent / 100
return product.copy(price = product.price - discount)
}
fun объявляет функцию. В скобках идут параметры. После скобок через : Product указан возвращаемый тип
Если функция короткая, можно использовать expression body:
fun finalPrice(price: Int, percent: Int): Int =
price - price * percent / 100
Такой стиль хорош для простых вычислений. Если внутри несколько шагов, обычное тело с {} читается лучше
Параметры по умолчанию
В Product поле category имеет значение по умолчанию:
val category: String = "general"
Теперь объект можно создать короче:
val book = Product("Kotlin Book", 2500)
Категория будет "general". Это удобнее, чем создавать несколько перегруженных конструкторов, как часто приходится делать в Java
Если параметров много, используйте именованные аргументы:
val mouse = Product(
title = "Mouse",
price = 3500,
category = "hardware"
)
Так меньше риска перепутать порядок
Что дает data class
data class говорит компилятору: этот тип в первую очередь хранит данные. Kotlin автоматически генерирует полезные методы: читаемый toString, сравнение по полям, hashCode, copy и component-функции для destructuring
Без data вывод объекта был бы менее полезным. С data class вы сразу видите:
Product(title=Keyboard, price=8000, category=hardware)
Это удобно для учебного вывода, логов и проверки результата
copy: новая версия без ручного копирования
Строка:
product.copy(price = product.price - discount)
создает новый Product, меняя только price. Остальные поля берутся из исходного объекта
Это важно: мы не меняем старый keyboard, а создаем новую версию saleKeyboard. Такой стиль хорошо сочетается с val и делает поток данных понятнее
Если объект содержит mutable-коллекцию, copy не делает глубокую копию всех вложенных объектов. Для первого урока используйте простые immutable-поля, чтобы не смешивать темы
Destructuring
Data class можно разобрать на переменные:
val (title, price, category) = saleKeyboard
println("$title costs $price in $category")
Порядок соответствует параметрам в primary constructor. Это удобно в маленьких примерах, но в большом коде не злоупотребляйте destructuring, если имена полей важны для понимания
Иногда явный доступ лучше:
println(saleKeyboard.title)
Читатель сразу видит, какое поле используется
Домашка: товар с рейтингом
Расширьте Product:
data class Product(
val title: String,
val price: Int,
val category: String = "general",
val rating: Double = 0.0
)
Сделайте функцию:
fun isRecommended(product: Product): Boolean
Правило: товар рекомендован, если рейтинг не ниже 4.5 и цена меньше 10000. Выведите понятную строку для двух товаров
Мини-практика: валидация скидки
Сейчас функция принимает любой percent. Добавьте защиту:
fun discounted(product: Product, percent: Int): Product {
val safePercent = percent.coerceIn(0, 90)
val discount = product.price * safePercent / 100
return product.copy(price = product.price - discount)
}
coerceIn(0, 90) ограничивает процент диапазоном. Это маленький пример того, как функция может защищать свою логику от странных входных данных
Проверьте percent = -10, percent = 15, percent = 200. Цена не должна становиться больше исходной или уходить в ноль из-за случайного значения
Функции-расширения
Kotlin позволяет добавить функцию к существующему типу без наследования:
fun Product.label(): String =
"$title: $price"
Теперь можно писать:
println(keyboard.label())
Extension function не добавляет поле внутрь объекта и не меняет исходный класс. Это удобный синтаксис для функции, где первый параметр выглядит как receiver
Не стоит превращать extension functions в склад любой логики. Если функция относится только к одному экрану или одному сервису, обычная функция может быть честнее. Но для форматирования, маленьких преобразований и удобного API extension functions очень полезны
Named arguments и читаемость
Если функция принимает несколько чисел одного типа, именованные аргументы сильно снижают риск ошибки:
fun priceWithDelivery(price: Int, delivery: Int, discount: Int): Int =
price + delivery - discount
val total = priceWithDelivery(
price = 8000,
delivery = 300,
discount = 500
)
Без имен легко перепутать delivery и discount. Компилятор не заметит смысловую ошибку, потому что оба значения имеют тип Int
Это один из тех случаев, где Kotlin дает не только короткость, но и читаемость. Пользуйтесь named arguments там, где порядок параметров неочевиден
Домашка: корзина товаров
Создайте:
data class CartItem(
val title: String,
val price: Int,
val quantity: Int = 1
)
Напишите функцию:
fun itemTotal(item: CartItem): Int
Она должна возвращать price * quantity. Затем создайте два товара, посчитайте сумму корзины и выведите строки:
Keyboard x1 = 8000
Mouse x2 = 7000
Total = 15000
Дополнительная задача: сделайте copy(quantity = 3) для одного товара и проверьте, что исходный объект не изменился
Когда data class не подходит
Data class хорош для данных, но не для любого класса. Если объект управляет соединением, файлом, экраном, жизненным циклом или сложным состоянием, data class может быть неправильным сигналом
Например, DatabaseConnection как data class выглядит странно: сравнение по полям и copy там не являются главным смыслом. А вот Product, User, Order, CartItem подходят хорошо
Частые ошибки и порядок проверки
Забыли val или var в data class constructor Для свойства нужно писать val title: String, а не просто title: String. Иначе параметр не станет полем класса
Используете data class для поведения, а не данных Data class хорош для моделей данных. Если тип содержит сложное состояние и много логики, обычный class может быть честнее
Думаете, что copy меняет исходный объект copy создает новый объект. Старый остается прежним, если его поля сами по себе immutable
Слишком увлеклись destructuring Если без имен полей смысл теряется, используйте product.title и product.price
Что может быть еще интересно по этой теме
Data class похож на record в Java? По роли да: компактный тип для данных. Но детали синтаксиса, copy и Kotlin-идиомы отличаются
Можно ли добавить методы в data class? Да. Data class может иметь функции в теле. Но если методов становится слишком много, проверьте, не смешали ли вы модель данных и сервисную логику
Почему поля лучше делать val? Immutable-модель проще читать и безопаснее передавать между функциями. Если изменение нужно, часто лучше создать новую версию через copy
Что дальше после data class? Коллекции. Списки data class-объектов хорошо показывают силу filter, map, groupBy и sumOf
Что почитать дальше по Kotlin
- Null safety в Kotlin: ?, !! и Elvis operator
- Коллекции Kotlin: list, map, filter и groupBy
- Coroutines в Kotlin: первый async-пример
- Kotlin и Java: в чем разница для новичка



