Функции и data class в Kotlin

В этом уроке мы сделаем маленькую модель товара: Product будет data class, функция рассчитает скидку, copy создаст измененную версию товара, а destructuring покажет, как удобно доставать поля. Это практический материал, а не сухой список возможностей Kotlin

К концу урока вы поймете, зачем нужен data class, чем val в конструкторе отличается от обычного параметра и почему copy часто лучше ручного пересоздания объекта

Что получится в конце

Код:

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

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x