← Назад к вопросам

Как обязать Generic наследоваться от интерфейса

1.8 Middle🔥 151 комментариев
#Kotlin основы#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как обязать Generic наследоваться от интерфейса

Это делается через bounded type parameters (ограничены типы параметров). Синтаксис очень простой, но мощный.

1. Базовый синтаксис

Требование: Generic тип ДОЛЖЕН наследоваться от определённого интерфейса или класса.

// Синтаксис
fun <T : SomeInterface> function(param: T) { ... }

class MyClass<T : SomeInterface> { ... }

interface MyInterface<T : SomeInterface> { ... }

Пример:

// Generic T ДОЛЖЕН быть Comparable
fun <T : Comparable<T>> sort(list: List<T>) {
    // Можно использовать методы Comparable
    list.forEach { item ->
        item.compareTo(item)  // Этот метод есть! (гарантировано)
    }
}

// Использование
sort(listOf(1, 2, 3))              // OK (Int имеет Comparable)
sort(listOf("a", "b"))              // OK (String имеет Comparable)
sort(listOf(MyClass()))             // ❌ Ошибка! MyClass не implements Comparable

2. Практический пример: Filterable

interface Filterable {
    fun matches(query: String): Boolean
}

// T ДОЛЖЕН быть Filterable
fun <T : Filterable> filterItems(items: List<T>, query: String): List<T> {
    return items.filter { it.matches(query) }  // matches() гарантирован
}

// Классы для фильтрации
data class User(val name: String) : Filterable {
    override fun matches(query: String): Boolean {
        return name.contains(query, ignoreCase = true)
    }
}

data class Product(val title: String) : Filterable {
    override fun matches(query: String): Boolean {
        return title.contains(query, ignoreCase = true)
    }
}

// Использование
val users = listOf(
    User("John"),
    User("Jane"),
    User("Bob")
)
val filtered = filterItems(users, "john")  // OK
println(filtered)  // [User("John")]

3. Множественные ограничения (Upper Bounds)

Можно требовать несколько интерфейсов одновременно:

// T ДОЛЖЕН быть Comparable И Serializable
fun <T> compare(a: T, b: T) where T : Comparable<T>, T : Serializable {
    // Можно использовать методы обоих интерфейсов
    a.compareTo(b)  // Из Comparable
    (a as? Serializable)?.let { it }  // Из Serializable
}

// Альтернативный синтаксис (в Kotlin немного другой):
fun <T : Comparable<T>> compare(a: T, b: T) where T : Serializable {
    // То же самое
}

4. Пример: Repository с ограничениями

// Интерфейс для сущностей с ID
interface Entity {
    val id: Int
}

// Repository, который работает только с Entity
class Repository<T : Entity> {
    private val cache = mutableMapOf<Int, T>()
    
    fun save(item: T) {
        cache[item.id] = item  // item.id гарантирован!
    }
    
    fun getById(id: Int): T? {
        return cache[id]
    }
}

// Сущности
data class User(override val id: Int, val name: String) : Entity
data class Post(override val id: Int, val title: String) : Entity

// Использование
val userRepo = Repository<User>()
userRepo.save(User(1, "John"))  // OK

val postRepo = Repository<Post>()
postRepo.save(Post(1, "Hello"))  // OK

// ❌ Неправильно
data class InvalidClass(val data: String)
val badRepo = Repository<InvalidClass>()  // Ошибка компиляции!

5. Ограничение классом

Можно требовать наследование от класса (не только интерфейса):

open class BaseModel {
    abstract fun toJson(): String
}

// T ДОЛЖЕН наследоваться от BaseModel
fun <T : BaseModel> serialize(item: T): String {
    return item.toJson()  // Метод гарантирован
}

class UserModel : BaseModel() {
    override fun toJson(): String = "{name: 'John'}"
}

serialize(UserModel())  // OK

6. Reified Type Parameters (специальный случай)

В Kotlin есть особенность reified для работы с типами в runtime:

inline fun <reified T> fromJson(json: String): T {
    return Gson().fromJson(json, T::class.java)  // Можно использовать T::class
}

data class User(val name: String)

val user = fromJson<User>("{\"name\":\"John\"}")
println(user)  // User(name="John")

7. Пример: DAO с Type Bounds

interface Entity {
    val id: Long
    fun validate(): Boolean
}

// DAO только для Entity-типов
class BaseDao<T : Entity>(
    private val database: AppDatabase
) {
    fun insert(entity: T): Long {
        if (!entity.validate()) {
            throw IllegalArgumentException("Invalid entity")
        }
        // Вставляем в БД
        return database.insert(entity)
    }
    
    fun update(entity: T) {
        if (!entity.validate()) {
            throw IllegalArgumentException("Invalid entity")
        }
        database.update(entity)
    }
}

// Сущности
data class User(
    override val id: Long = 0,
    val name: String,
    val email: String
) : Entity {
    override fun validate(): Boolean {
        return name.isNotEmpty() && email.contains("@")
    }
}

data class Post(
    override val id: Long = 0,
    val title: String,
    val content: String
) : Entity {
    override fun validate(): Boolean {
        return title.isNotEmpty() && content.isNotEmpty()
    }
}

// Использование
val userDao = BaseDao<User>(database)
val newUser = User(name = "John", email = "john@example.com")
userDao.insert(newUser)  // OK

val postDao = BaseDao<Post>(database)
val newPost = Post(title = "Hello", content = "World")
postDao.insert(newPost)  // OK

8. Covariance и Bounds

// Covariant bound (producer)
class Producer<out T : Entity> {
    fun produce(): T { /* ... */ }
}

// Contravariant bound (consumer)
class Consumer<in T : Entity> {
    fun consume(item: T) { /* ... */ }
}

// Invariant (default)
class Container<T : Entity> {
    fun get(): T { /* ... */ }
    fun put(item: T) { /* ... */ }
}

9. Практическое применение: Adapter

interface ViewHolder {
    fun bind(data: Any)
}

// Adapter для любых данных, которые имеют ViewHolder
class GenericAdapter<T : ViewHolder>(
    private val items: List<T>
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        (holder as? T)?.bind(items[position])  // T имеет метод bind()
    }
    
    override fun getItemCount(): Int = items.size
    
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        // ...
        return MyViewHolder()
    }
}

class MyViewHolder : ViewHolder {
    override fun bind(data: Any) {
        // Привязываем данные
    }
}

10. Ошибки при использовании Type Bounds

❌ Забыли ограничение

fun <T> process(item: T) {
    item.validate()  // ❌ Ошибка! T может не иметь validate()
}

// ✅ Правильно
fun <T : Entity> process(item: T) {
    item.validate()  // OK!
}

❌ Передали неправильный тип

fun <T : Entity> save(item: T) { /* ... */ }

data class WrongClass(val data: String)
save(WrongClass("test"))  // ❌ Ошибка компиляции!

Итог

  • Синтаксис: <T : Interface> обязывает T иметь методы интерфейса
  • Множественные ограничения: where T : Interface1, T : Interface2
  • Проверяется на compile-time, так что ошибки ловятся рано
  • Позволяет безопасно использовать методы Generic типа
  • Широко используется в Repository, DAO, Adapter'ах
  • Улучшает type safety и делает код понятнее