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

Ответ

В Kotlin можно ограничить Generic параметр интерфейсом с помощью where или :. Пример:

interface MyInterface {
    fun doSomething()
}

class MyClass<T> where T : MyInterface {
    fun process(item: T) {
        item.doSomething()
    }
}

// Или альтернативный синтаксис:
class AnotherClass<T : MyInterface>(val item: T)

Ключевые моменты:

  1. Ограничение гарантирует, что тип T реализует MyInterface
  2. Можно использовать несколько ограничений через where T : A, T : B
  3. Внутри класса доступны все методы интерфейса для типа T

Пример использования:

class Implementation : MyInterface {
    override fun doSomething() = println("Working")
}

val obj = MyClass<Implementation>()

Ответ 18+ 🔞

А, ну ты про дженерики в Kotlin! Давай разберём эту штуку, там всё не так страшно, как кажется на первый взгляд.

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

Смотри, допустим, у тебя есть интерфейс — набор правил, контракт.

interface MyInterface {
    fun doSomething()
}

Просто говорит: «Кто меня реализует, тот обязан иметь метод doSomething()». Ёпта, логично же.

А теперь ты хочешь создать класс, который будет принимать в работу только таких умельцев. Как сказать компилятору: «Слушай, пускай только тех, кто контракт подписал»?

Есть два основных способа, оба рабочие, выбирай какой больше нравится.

Способ первый — через where. Выглядит солидно, как будто ты условия ставишь.

class MyClass<T> where T : MyInterface {
    fun process(item: T) {
        item.doSomething() // Всё, тут уже можно спать спокойно. Компилятор знает, что у `item` этот метод точно есть.
    }
}

Читается это как: «Класс MyClass работает с типом T, но только при условии, где T является наследником MyInterface». В рот меня чих-пых, звучит-то как! Прямо терпения ноль ебать ждать, когда какой-нибудь String попробует сюда пролезть — компилятор его нахуй пошлёт сразу.

Способ второй — короче, через двоеточие в объявлении класса. Для тех, кто не любит много букв.

class AnotherClass<T : MyInterface>(val item: T)

Тут всё то же самое, просто запись компактнее. «Класс AnotherClass для типа T, который должен быть MyInterface». И всё, да похуй на остальное.

А главная фишка в чём? Можно навесить овердохуища ограничений! Допустим, ты хочешь, чтобы твой тип T был не просто умным, а очень умным: и интерфейс A реализовывал, и от класса B наследовался.

class SuperClass<T> where T : MyInterface, T : SomeOtherClass {
    // ...
}

Вот это уже мощно. Ты говоришь системе: «Пускать только избранных, которые прошли оба круга ада». Сам от себя охуел, какая гибкость.

Ну и пример использования, чтобы совсем уж ясно стало:

class Implementation : MyInterface {
    override fun doSomething() = println("Working")
}

val obj = MyClass<Implementation>() // Всё ок, Implementation подписал контракт.
obj.process(Implementation()) // Напечатает "Working".

А если попробуешь MyClass<String>(), то получишь по шапке от компилятора — ни хуя себе, String-то наш метод doSomething() не реализует! Вот и весь сказ, чувак. Ограничиваешь — и спишь спокойно, никто тебе в коде сюрпризов гадких не подкинет.