Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
---
|
||||
title: Полиморфизм
|
||||
description: Полиморфизм подтипов, перегрузка, параметрический полиморфизм и что разрешается статически
|
||||
tags:
|
||||
- kotlin
|
||||
- oop
|
||||
---
|
||||
|
||||
Полиморфизм - возможность работать с разными типами через один интерфейс. В Kotlin четыре формы:
|
||||
|
||||
| Форма | Механизм | Когда выбирается реализация |
|
||||
|---|---|---|
|
||||
| подтипов | `override` | в рантайме, по фактическому типу |
|
||||
| ad-hoc (перегрузка) | одно имя, разные параметры | при компиляции |
|
||||
| параметрический | дженерики | при компиляции (типы стираются) |
|
||||
| через `when` по sealed | сопоставление типов | в рантайме, но проверяется компилятором |
|
||||
|
||||
## Полиморфизм подтипов
|
||||
```kotlin
|
||||
interface Shape5 {
|
||||
fun area(): Double
|
||||
fun name(): String
|
||||
}
|
||||
|
||||
class Circle5(val r: Double) : Shape5 {
|
||||
override fun area() = Math.PI * r * r
|
||||
override fun name() = "круг"
|
||||
}
|
||||
|
||||
class Square4(val side: Double) : Shape5 {
|
||||
override fun area() = side * side
|
||||
override fun name() = "квадрат"
|
||||
}
|
||||
|
||||
fun describe(s: Shape5) = "${s.name()}: %.2f".format(java.util.Locale.US, s.area())
|
||||
|
||||
fun main() {
|
||||
val shapes: List<Shape5> = listOf(Circle5(1.0), Square4(2.0))
|
||||
shapes.forEach { println(describe(it)) }
|
||||
// круг: 3.14
|
||||
// квадрат: 4.00
|
||||
println(shapes.sumOf { it.area() } > 7) // true
|
||||
}
|
||||
```
|
||||
Функция `describe` написана один раз и работает с любым `Shape5`, включая те, которые появятся позже. Выбор реализации происходит в рантайме по фактическому типу объекта - это динамическая диспетчеризация.
|
||||
|
||||
## Перегрузка
|
||||
Одно имя, разные параметры. Выбор - при компиляции, по статическим типам аргументов:
|
||||
```kotlin
|
||||
fun format2(value: Int) = "целое: $value"
|
||||
fun format2(value: Double) = "дробное: $value"
|
||||
fun format2(value: String) = "строка: '$value'"
|
||||
fun format2(value: Any?) = "что-то: $value"
|
||||
|
||||
fun main() {
|
||||
println(format2(1)) // целое: 1
|
||||
println(format2(1.0)) // дробное: 1.0
|
||||
println(format2("текст")) // строка: 'текст'
|
||||
println(format2(listOf(1))) // что-то: [1]
|
||||
|
||||
val x: Any = 1 // статический тип Any!
|
||||
println(format2(x)) // что-то: 1 — не "целое: 1"
|
||||
}
|
||||
```
|
||||
Последняя строка показывает суть: компилятор видит тип переменной, а не объекта. Это основное отличие от полиморфизма подтипов.
|
||||
|
||||
В Kotlin перегрузка нужна реже - её заменяют значения по умолчанию:
|
||||
```kotlin
|
||||
fun connect(host: String, port: Int = 8080, secure: Boolean = false) =
|
||||
"$host:$port secure=$secure"
|
||||
|
||||
fun main() {
|
||||
println(connect("localhost")) // localhost:8080 secure=false
|
||||
println(connect("localhost", secure = true)) // localhost:8080 secure=true
|
||||
}
|
||||
```
|
||||
Подробнее: [Функции](../Синтаксис%20и%20типы/Работа%20с%20данными/Функции.md)
|
||||
|
||||
## Параметрический полиморфизм
|
||||
Один код для любого типа:
|
||||
```kotlin
|
||||
fun <T> firstOrDefault(items: List<T>, default: T): T =
|
||||
if (items.isEmpty()) default else items[0]
|
||||
|
||||
class Box4<T>(private val value: T) {
|
||||
fun get(): T = value
|
||||
fun <R> map(f: (T) -> R): Box4<R> = Box4(f(value))
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(firstOrDefault(listOf(1, 2), 0)) // 1
|
||||
println(firstOrDefault(emptyList(), "нет")) // нет
|
||||
println(Box4(5).map { it * 2 }.get()) // 10
|
||||
}
|
||||
```
|
||||
Ограничение параметра типа позволяет вызывать методы:
|
||||
```kotlin
|
||||
fun <T : Comparable<T>> maxOf2(a: T, b: T): T = if (a > b) a else b
|
||||
|
||||
fun main() {
|
||||
println(maxOf2(3, 7)) // 7
|
||||
println(maxOf2("abc", "abd")) // abd
|
||||
}
|
||||
```
|
||||
Подробнее: [Параметрический полиморфизм](../Дженерики/Параметрический%20полиморфизм.md)
|
||||
|
||||
## Полиморфизм через sealed и when
|
||||
```kotlin
|
||||
sealed interface Payment {
|
||||
data class Card(val number: String) : Payment
|
||||
data class Cash(val amount: Long) : Payment
|
||||
data object Invoice : Payment
|
||||
}
|
||||
|
||||
fun process2(p: Payment): String = when (p) {
|
||||
is Payment.Card -> "карта ****${p.number.takeLast(4)}"
|
||||
is Payment.Cash -> "наличные ${p.amount}"
|
||||
Payment.Invoice -> "счёт"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(process2(Payment.Card("1234567890123456"))) // карта ****3456
|
||||
println(process2(Payment.Cash(500))) // наличные 500
|
||||
println(process2(Payment.Invoice)) // счёт
|
||||
}
|
||||
```
|
||||
Это альтернатива полиморфизму подтипов с обратным распределением ответственности:
|
||||
|
||||
| | метод в классе | `when` по sealed |
|
||||
|---|---|---|
|
||||
| добавить новый тип | правок в одном месте | компилятор найдёт все `when` |
|
||||
| добавить новую операцию | правка во всех классах | один новый `when` |
|
||||
| логика операции | размазана по классам | собрана в одном месте |
|
||||
|
||||
Выбор зависит от того, что меняется чаще. Набор типов стабилен, а операций много - `sealed` + `when`. Операции стабильны, а типы добавляются - методы в классах.
|
||||
|
||||
Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
|
||||
|
||||
## Что разрешается статически
|
||||
Это главный источник ошибок ожиданий. Статически (по типу ссылки, а не объекта) выбираются:
|
||||
|
||||
**Функции-расширения:**
|
||||
```kotlin
|
||||
open class Base37
|
||||
class Derived33 : Base37()
|
||||
|
||||
fun Base37.describe() = "база"
|
||||
fun Derived33.describe() = "подкласс"
|
||||
|
||||
fun main() {
|
||||
val d: Base37 = Derived33()
|
||||
println(d.describe()) // база — по типу переменной!
|
||||
println(Derived33().describe()) // подкласс
|
||||
}
|
||||
```
|
||||
|
||||
**Перегруженные функции:**
|
||||
```kotlin
|
||||
open class Animal4
|
||||
class Dog3 : Animal4()
|
||||
|
||||
fun feed(a: Animal4) = "корм для животного"
|
||||
fun feed(d: Dog3) = "корм для собаки"
|
||||
|
||||
fun main() {
|
||||
val a: Animal4 = Dog3()
|
||||
println(feed(a)) // корм для животного
|
||||
println(feed(Dog3())) // корм для собаки
|
||||
}
|
||||
```
|
||||
|
||||
**Члены `companion object`** - они не переопределяются:
|
||||
```kotlin
|
||||
open class Base38 {
|
||||
companion object { fun create() = "из Base" }
|
||||
}
|
||||
|
||||
class Derived34 : Base38() {
|
||||
companion object { fun create() = "из Derived" }
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(Base38.create()) // из Base
|
||||
println(Derived34.create()) // из Derived — выбор по имени класса
|
||||
}
|
||||
```
|
||||
|
||||
Динамически выбираются только **члены класса**, помеченные `open`/`abstract` и переопределённые через `override`.
|
||||
|
||||
## Приведение и проверка типа
|
||||
```kotlin
|
||||
fun handle(x: Any): String = when (x) {
|
||||
is String -> "строка длины ${x.length}" // умное приведение
|
||||
is Int -> "число ${x + 1}"
|
||||
is List<*> -> "список из ${x.size}"
|
||||
else -> "неизвестно: ${x::class.simpleName}"
|
||||
}
|
||||
|
||||
fun main() {
|
||||
println(handle("abc")) // строка длины 3
|
||||
println(handle(5)) // число 6
|
||||
println(handle(listOf(1, 2))) // список из 2
|
||||
println(handle(1.0)) // неизвестно: Double
|
||||
}
|
||||
```
|
||||
Длинная цепочка `is` часто означает, что иерархия просит `sealed`-типа или полиморфного метода. Подробнее: [Приведение типов](../Синтаксис%20и%20типы/Работа%20с%20данными/Приведение%20типов.md)
|
||||
|
||||
## Вариантность
|
||||
Полиморфизм распространяется на дженерики через `out` и `in`:
|
||||
```kotlin
|
||||
open class Animal5(val name: String)
|
||||
class Dog4(name: String) : Animal5(name)
|
||||
|
||||
fun printAll(animals: List<Animal5>) = animals.joinToString { it.name }
|
||||
|
||||
fun main() {
|
||||
val dogs: List<Dog4> = listOf(Dog4("Рекс"), Dog4("Бобик"))
|
||||
println(printAll(dogs)) // Рекс, Бобик — List<Dog4> подходит как List<Animal5>
|
||||
}
|
||||
```
|
||||
Это работает, потому что `List<out E>` ковариантен. С `MutableList` так нельзя - он инвариантен. Подробнее: [Вариантность](../Дженерики/Вариантность.md)
|
||||
|
||||
> [!info] Отличия от Java
|
||||
> | Java | Kotlin |
|
||||
> |---|---|
|
||||
> | метод виртуален по умолчанию | нужен `open` |
|
||||
> | `@Override` опциональна | `override` обязательно |
|
||||
> | перегрузка для опциональных параметров | значения по умолчанию |
|
||||
> | `instanceof` + каст | `is` + умное приведение |
|
||||
> | `switch` с pattern matching (Java 21) | `when` с `is` |
|
||||
> | Visitor для операций над иерархией | `sealed` + `when` |
|
||||
> | статические методы скрываются | члены компаньона не наследуются |
|
||||
>
|
||||
> **Механика полиморфизма подтипов идентична** - это та же виртуальная диспетчеризация JVM. Отличается значение по умолчанию: в Kotlin метод финален, пока не помечен `open`.
|
||||
>
|
||||
> **Visitor почти не нужен.** В Java для добавления операций к закрытой иерархии без правки классов применяют паттерн Visitor: интерфейс с методом на каждый тип, метод `accept` в каждом классе. `sealed` + `when` даёт ту же проверку полноты компилятором, но без дополнительного кода:
|
||||
> ```kotlin
|
||||
> sealed interface Expr2 {
|
||||
> data class Num2(val value: Int) : Expr2
|
||||
> data class Add2(val l: Expr2, val r: Expr2) : Expr2
|
||||
> }
|
||||
>
|
||||
> fun eval(e: Expr2): Int = when (e) {
|
||||
> is Expr2.Num2 -> e.value
|
||||
> is Expr2.Add2 -> eval(e.l) + eval(e.r)
|
||||
> }
|
||||
>
|
||||
> fun show(e: Expr2): String = when (e) { // новая операция — один when
|
||||
> is Expr2.Num2 -> "${e.value}"
|
||||
> is Expr2.Add2 -> "(${show(e.l)} + ${show(e.r)})"
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val expr = Expr2.Add2(Expr2.Num2(1), Expr2.Num2(2))
|
||||
> println("${show(expr)} = ${eval(expr)}") // (1 + 2) = 3
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Умное приведение убирает двойную работу.** В Java после `instanceof` нужен явный каст (до версии 16, где появился pattern matching).
|
||||
>
|
||||
> Сравнить: [Полиморфизм в Java](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм.md), [Полиморфизм подтипов](../../Программирование%20на%20Java/ООП/Полиморфизм/Полиморфизм%20подтипов.md), [Ad-hoc полиморфизм](../../Программирование%20на%20Java/ООП/Полиморфизм/Ad-hoc%20(перегрузка).md)
|
||||
|
||||
> [!warning] Подводные камни
|
||||
> **Расширения не полиморфны** - выбор по статическому типу. Это самая частая ошибка:
|
||||
> ```kotlin
|
||||
> open class Base39
|
||||
> class Derived35 : Base39()
|
||||
>
|
||||
> fun Base39.name() = "база"
|
||||
> fun Derived35.name() = "подкласс"
|
||||
>
|
||||
> fun main() {
|
||||
> println(listOf<Base39>(Derived35()).first().name()) // база
|
||||
> }
|
||||
> ```
|
||||
> Нужен полиморфизм - нужен член класса.
|
||||
>
|
||||
> **Перегрузка выбирается по типу ссылки, а не объекта.** Код ведёт себя по-разному в зависимости от того, как объявлена переменная.
|
||||
>
|
||||
> **Смешение перегрузки и переопределения даёт неочевидный результат:**
|
||||
> ```kotlin
|
||||
> open class Base40 { open fun f(x: Any) = "any" }
|
||||
> class Derived36 : Base40() {
|
||||
> override fun f(x: Any) = "override"
|
||||
> fun f(x: Int) = "overload"
|
||||
> }
|
||||
>
|
||||
> fun main() {
|
||||
> val d = Derived36()
|
||||
> println(d.f(1)) // overload
|
||||
> println((d as Base40).f(1)) // override
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> **Вызов `open`-метода из конструктора попадает в подкласс до его инициализации.** Подробнее: [Инициализация](Инициализация.md)
|
||||
>
|
||||
> **Стирание типов ломает перегрузку по дженерикам:**
|
||||
> ```kotlin
|
||||
> // fun show(items: List<String>) = "строки"
|
||||
> // fun show(items: List<Int>) = "числа" // ошибка: та же JVM-сигнатура
|
||||
>
|
||||
> @JvmName("showStrings")
|
||||
> fun show2(items: List<String>) = "строки: ${items.size}"
|
||||
> fun show2(items: List<Int>) = "числа: ${items.size}"
|
||||
>
|
||||
> fun main() { println(show2(listOf("a"))) } // строки: 1
|
||||
> ```
|
||||
> Обойти можно через `@JvmName`, но обычно это признак, что нужен один дженерик-метод.
|
||||
>
|
||||
> **Длинная цепочка `is` вместо полиморфизма.** Если `when (x) { is A -> ...; is B -> ... }` повторяется в нескольких местах, логику стоит перенести в методы типов или закрыть иерархию через `sealed`, чтобы компилятор контролировал полноту.
|
||||
>
|
||||
> **Члены `companion object` не переопределяются.** Попытка «переопределить» фабрику в подклассе создаёт независимый компаньон; вызов `Derived.create()` работает только потому, что имя разрешается статически.
|
||||
>
|
||||
> **Массивы инвариантны, а `List` ковариантен** - в отличие от Java, где массивы ковариантны и могут бросить `ArrayStoreException`:
|
||||
> ```kotlin
|
||||
> fun main() {
|
||||
> val strings: List<String> = listOf("a")
|
||||
> val anys: List<Any> = strings // ok
|
||||
> println(anys.size) // 1
|
||||
> // val arr: Array<Any> = arrayOf("a") // ошибка компиляции
|
||||
> }
|
||||
> ```
|
||||
|
||||
Дополнительно: [Переопределение](Переопределение.md)
|
||||
Далее: [Система исключений](../Исключения/Система%20исключений.md)
|
||||
Reference in New Issue
Block a user