Наследование в Kotlin

Все классы в Kotlin имеют общий суперкласс Any, который является суперклассом по умолчанию для класса без объявленных супертипов:

class Example // Неявно наследуется от Any 

У Any есть три метода: equals(), hashCode() и toString(). Таким образом, они определены для всех классов Kotlin.

По умолчанию классы Kotlin являются окончательными: они не могут быть унаследованы. Чтобы сделать класс наследуемым, отметьте его ключевым словом open.

open class Base //Класс открыт для наследования 

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

open class Base(p: Int)

class Derived(p: Int) : Base(p)

Если производный класс имеет первичный конструктор, базовый класс может (и должен) быть инициализирован прямо здесь, используя параметры первичного конструктора.

Если производный класс не имеет основного конструктора, то каждый вторичный конструктор должен инициализировать базовый тип с помощью ключевого слова super или делегировать другому конструктору, который это делает. Обратите внимание, что в этом случае разные вторичные конструкторы могут вызывать разные конструкторы базового типа:

class MyView : View {
    constructor(ctx: Context) : super(ctx)

    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}

Переопределение методов

Kotlin требует явных модификаторов для переопределяемых членов (их называют открытыми (open)) и для переопределений:

open class Shape {
    open fun draw() { /*...*/ }
    fun fill() { /*...*/ }
}
 
class Circle() : Shape() {
    override fun draw() { /*...*/ }
}

Модификатор override требуется для Circle.draw(). Если бы его не было, компилятор пожаловался бы. Если для функции нет модификатора open, например Shape.fill(), объявление метода с такой же сигнатурой в подклассе является недопустимым, как с override, так и без него. Модификатор open не действует при добавлении к членам последнего класса (т. е. класса без модификатора open).

Член, помеченный как override, сам по себе открыт (open), т.е. его можно переопределить в подклассах. Если вы хотите запретить переопределение, используйте final:

open class Rectangle() : Shape() {
    final override fun draw() { /*...*/ }
}

Переопределение свойств

Переопределение свойств работает аналогично переопределению методов; свойства, объявленные в суперклассе, которые затем повторно объявляются в производном классе, должны иметь префикс override и иметь совместимый тип. Каждое объявленное свойство может быть переопределено свойством с инициализатором или свойством с методом get.

open class Shape {
    open val vertexCount: Int = 0
}

class Rectangle : Shape() {
    override val vertexCount = 4
}

Вы также можете переопределить свойство val с помощью свойства var, но не наоборот. Это разрешено, потому что свойство val по существу объявляет метод get, а переопределение его как var дополнительно объявляет метод set в производном классе.

Обратите внимание, что вы можете использовать ключевое слово override как часть объявления свойства в основном конструкторе.

interface Shape {
    val vertexCount: Int
}

class Rectangle(override val vertexCount: Int = 4) : Shape // Всегда имеет 4 вершины

class Polygon : Shape {
    override var vertexCount: Int = 0  // Позже может быть установлено любое число
}

Порядок инициализации производного класса

Во время создания нового экземпляра производного класса инициализация базового класса выполняется как первый шаг (которому предшествует только оценка аргументов для конструктора базового класса) и, таким образом, происходит до запуска логики инициализации производного класса.

open class Base(val name: String) {

    init { println("Initializing Base") }

    open val size: Int = 
        name.length.also { println("Initializing size in Base: $it") }
}

class Derived(
    name: String,
    val lastName: String,
) : Base(name.capitalize().also { println("Argument for Base: $it") }) {

    init { println("Initializing Derived") }

    override val size: Int =
        (super.size + lastName.length).also { println("Initializing size in Derived: $it") }
}

Вывод:

Constructing Derived("hello", "world")
Argument for Base: Hello
Initializing Base
Initializing size in Base: 5
Initializing Derived
Initializing size in Derived: 10

Это означает, что к моменту выполнения конструктора базового класса свойства, объявленные или переопределенные в производном классе, еще не инициализированы. Если какое-либо из этих свойств используется в логике инициализации базового класса (прямо или косвенно, через другую переопределенную реализацию открытого члена), это может привести к неправильному поведению или сбою во время выполнения. Поэтому при разработке базового класса следует избегать использования открытых членов в конструкторах, инициализаторах свойств и блоках инициализации.

Вызов реализации суперкласса

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

open class Rectangle {
    open fun draw() { println("Drawing a rectangle") }
    val borderColor: String get() = "black"
}

class FilledRectangle : Rectangle() {
    override fun draw() {
        super.draw()
        println("Filling the rectangle")
    }

    val fillColor: String get() = super.borderColor
}

Внутри внутреннего класса доступ к суперклассу внешнего класса осуществляется с помощью ключевого слова super с именем внешнего класса: super@Outer:

class FilledRectangle: Rectangle() {
    override fun draw() { 
        val filler = Filler()
        filler.drawAndFill()
    }

    inner class Filler {
        fun fill() { println("Filling") }
        fun drawAndFill() {
            super@FilledRectangle.draw() // Вызывает реализацию draw() в Rectangle
            fill()
            // Использует реализацию функции get() borderColor в Rectangle
            println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}")
        }
    }
}

Вывод:

Drawing a rectangle
Filling
Drawn a filled rectangle with color black

Превалирующие правила

В Kotlin наследование реализаций регулируется следующим правилом: если класс наследует несколько реализаций одного и того же члена от своих непосредственных суперклассов, он должен переопределить этот член и предоставить свою собственную реализацию (возможно, используя одну из унаследованных). Чтобы обозначить супертип, из которого взята унаследованная реализация, мы используем super квалификацированный по имени супертипа в угловых скобках, например super<Base>:

open class Rectangle {
    open fun draw() { /* ... */ }
}

interface Polygon {
    fun draw() { /* ... */ } // члены интерфейса 'open' по умолчанию
}

class Square() : Rectangle(), Polygon {
    // Компилятор требует переопределения функции draw():
    override fun draw() {
        super<Rectangle>.draw() // вызов Rectangle.draw()
        super<Polygon>.draw() // вызов Polygon.draw()
    }
}

Унаследовать как от Rectangle, так и от Polygon - это нормально, но у обоих есть свои реализации draw(), поэтому мы должны переопределить draw() в Square и предоставить собственную реализацию, которая устраняет двусмысленность.

Абстрактные классы

Класс и некоторые его члены могут быть объявлены абстрактными. Абстрактный член не имеет реализации в своем классе. Обратите внимание, что нам не нужно аннотировать абстрактный класс или функцию с помощью open - это само собой разумеется.

Мы можем заменить неабстрактный открытый член абстрактным

open class Polygon {
    open fun draw() {}
}

abstract class Rectangle : Polygon() {
    abstract override fun draw()
}

Сопутствующие объекты

Если вам нужно написать функцию, которая может быть вызвана без экземпляра класса, но нуждается в доступе к внутренним компонентам класса (например, к фабричному методу), вы можете записать ее как член объявления объекта внутри этого класса.

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


Читайте также:


Комментарии

Популярные сообщения из этого блога

Строки в Kotlin

Возврат и прыжки в Kotlin