285 lines
13 KiB
Markdown
285 lines
13 KiB
Markdown
---
|
|
title: Вложенные и внутренние классы
|
|
description: Вложенный класс по умолчанию, inner для ссылки на внешний объект и утечки памяти
|
|
tags:
|
|
- kotlin
|
|
- oop
|
|
---
|
|
|
|
Класс внутри класса по умолчанию **не** имеет ссылки на внешний объект. Чтобы её получить, нужен `inner`.
|
|
|
|
```kotlin
|
|
class Outer5 {
|
|
private val secret = "скрыто"
|
|
|
|
class Nested2 {
|
|
fun describe() = "вложенный, внешний объект недоступен"
|
|
// fun bad() = secret // ошибка: нет доступа
|
|
}
|
|
|
|
inner class Inner5 {
|
|
fun describe() = "внутренний, вижу: $secret"
|
|
}
|
|
}
|
|
|
|
fun main() {
|
|
println(Outer5.Nested2().describe()) // внешний объект не нужен
|
|
println(Outer5().Inner5().describe()) // нужен экземпляр Outer5
|
|
}
|
|
```
|
|
|
|
| | вложенный (по умолчанию) | `inner` |
|
|
|---|---|---|
|
|
| ссылка на внешний объект | нет | есть |
|
|
| доступ к членам внешнего класса | только к членам `companion object` | ко всем, включая `private` |
|
|
| как создать | `Outer.Nested()` | `outer.Inner()` |
|
|
| аналог в Java | `static class` | обычный вложенный класс |
|
|
| риск утечки памяти | нет | есть |
|
|
|
|
Обратите внимание на создание: `Outer5().Inner5()` - сначала внешний объект, затем внутренний на нём.
|
|
|
|
## Зачем вложенные классы
|
|
Группировка по смыслу: класс нужен только в контексте внешнего и не имеет смысла отдельно.
|
|
```kotlin
|
|
class HttpClient2 {
|
|
class Request2(val url: String, val method: String = "GET")
|
|
class Response(val code: Int, val body: String)
|
|
|
|
fun send(request: Request2): Response =
|
|
Response(200, "ответ на ${request.method} ${request.url}")
|
|
}
|
|
|
|
fun main() {
|
|
val client = HttpClient2()
|
|
val response = client.send(HttpClient2.Request2("http://x.ru"))
|
|
println("${response.code}: ${response.body}") // 200: ответ на GET http://x.ru
|
|
}
|
|
```
|
|
Имя `HttpClient2.Request2` само документирует принадлежность, и пакет не засоряется.
|
|
|
|
Частый случай - варианты sealed-иерархии:
|
|
```kotlin
|
|
sealed interface State3 {
|
|
data object Loading3 : State3
|
|
data class Ready(val data: String) : State3
|
|
data class Error3(val message: String) : State3
|
|
}
|
|
|
|
fun render(s: State3) = when (s) {
|
|
State3.Loading3 -> "загрузка"
|
|
is State3.Ready -> "готово: ${s.data}"
|
|
is State3.Error3 -> "ошибка: ${s.message}"
|
|
}
|
|
|
|
fun main() {
|
|
println(render(State3.Ready("данные"))) // готово: данные
|
|
println(render(State3.Loading3)) // загрузка
|
|
}
|
|
```
|
|
Подробнее: [Sealed class и interface](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Sealed%20class%20и%20interface.md)
|
|
|
|
## Зачем inner
|
|
Когда внутренний объект логически часть конкретного экземпляра и должен видеть его состояние.
|
|
```kotlin
|
|
class Table(private val name: String) {
|
|
private val rows = mutableListOf<String>()
|
|
|
|
inner class Row(private val data: String) {
|
|
fun insert(): String {
|
|
rows.add(data) // меняем состояние внешнего
|
|
return "в таблицу $name добавлено '$data', всего ${rows.size}"
|
|
}
|
|
}
|
|
|
|
fun newRow(data: String) = Row(data)
|
|
fun count() = rows.size
|
|
}
|
|
|
|
fun main() {
|
|
val t = Table("users")
|
|
println(t.newRow("Иван").insert()) // в таблицу users добавлено 'Иван', всего 1
|
|
println(t.newRow("Пётр").insert()) // в таблицу users добавлено 'Пётр', всего 2
|
|
println(t.count()) // 2
|
|
}
|
|
```
|
|
|
|
## this в inner-классе
|
|
При совпадении имён нужна квалификация метками:
|
|
```kotlin
|
|
class Outer6(val name: String = "внешний") {
|
|
inner class Inner6(val name: String = "внутренний") {
|
|
fun describe() = "${this.name} / ${this@Outer6.name}"
|
|
}
|
|
}
|
|
|
|
fun main() {
|
|
println(Outer6().Inner6().describe()) // внутренний / внешний
|
|
}
|
|
```
|
|
`this@Outer6` - ссылка на внешний объект. Без метки `this` относится к ближайшему классу.
|
|
|
|
То же для `super`:
|
|
```kotlin
|
|
open class Base36 { open fun f() = "база" }
|
|
|
|
class Outer7 : Base36() {
|
|
override fun f() = "внешний"
|
|
|
|
inner class Inner7 {
|
|
fun both() = "${f()} / ${super@Outer7.f()}"
|
|
}
|
|
}
|
|
|
|
fun main() { println(Outer7().Inner7().both()) } // внешний / база
|
|
```
|
|
|
|
## Локальные классы
|
|
Класс можно объявить внутри функции:
|
|
```kotlin
|
|
fun process(items: List<Int>): String {
|
|
val prefix = ">> "
|
|
|
|
class Formatter(val value: Int) {
|
|
fun format() = "$prefix$value" // захватывает prefix
|
|
}
|
|
|
|
return items.map { Formatter(it).format() }.joinToString()
|
|
}
|
|
|
|
fun main() {
|
|
println(process(listOf(1, 2))) // >> 1, >> 2
|
|
}
|
|
```
|
|
Локальный класс видит переменные функции, включая изменяемые. Нужен редко: обычно хватает [объекта-выражения](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md) или лямбды.
|
|
|
|
## Вложенный интерфейс и object
|
|
```kotlin
|
|
class Service17 {
|
|
fun interface Listener { // интерфейс всегда "вложенный"
|
|
fun onEvent(name: String): String
|
|
}
|
|
|
|
object Defaults { // object не может быть inner
|
|
const val TIMEOUT = 30
|
|
}
|
|
|
|
companion object {
|
|
fun create() = Service17()
|
|
}
|
|
|
|
fun notify2(l: Listener) = l.onEvent("старт")
|
|
}
|
|
|
|
fun main() {
|
|
val s = Service17.create()
|
|
println(s.notify2 { "получено: $it" }) // получено: старт
|
|
println(Service17.Defaults.TIMEOUT) // 30
|
|
}
|
|
```
|
|
Интерфейсы, `object`, `enum` и `data class` не могут быть `inner` - у них нет смысла быть привязанными к экземпляру.
|
|
|
|
## Вложенность и дженерики
|
|
`inner`-класс видит параметры типа внешнего:
|
|
```kotlin
|
|
class Box3<T>(private val value: T) {
|
|
inner class Wrapper { // T доступен
|
|
fun get(): T = value
|
|
fun describe() = "обёртка над ${value}"
|
|
}
|
|
|
|
class Helper4<U>(val other: U) // вложенный: свой параметр типа
|
|
}
|
|
|
|
fun main() {
|
|
val b = Box3("данные")
|
|
println(b.Wrapper().get()) // данные
|
|
println(b.Wrapper().describe()) // обёртка над данные
|
|
println(Box3.Helper4(42).other) // 42
|
|
}
|
|
```
|
|
Вложенный класс параметров внешнего не видит - ему нужны свои.
|
|
|
|
> [!info] Отличия от Java
|
|
> | Java | Kotlin |
|
|
> |---|---|
|
|
> | `static class Nested` | `class Nested` (по умолчанию) |
|
|
> | `class Inner` (без static) | `inner class Inner` |
|
|
> | `outer.new Inner()` | `outer.Inner()` |
|
|
> | `Outer.this.field` | `this@Outer.field` |
|
|
> | `Outer.super.method()` | `super@Outer.method()` |
|
|
> | анонимный класс | объект-выражение |
|
|
>
|
|
> **Значения по умолчанию перевёрнуты - и это важно.** В Java вложенный класс по умолчанию держит ссылку на внешний объект, и чтобы этого избежать, нужно написать `static`. Разработчики об этом забывают, из-за чего возникают утечки памяти: внутренний объект, переживший внешний, не даёт собрать его сборщику мусора.
|
|
>
|
|
> В Kotlin по умолчанию ссылки нет - её нужно запросить словом `inner`. Безопасный вариант стал умолчанием, а опасный требует явного решения. Это особенно заметно в Android, где утечки активити через нестатические внутренние классы и анонимные обработчики были массовой проблемой.
|
|
>
|
|
> **`Outer.this` против `this@Outer`.** Синтаксис меток в Kotlin единообразен: та же `@`-нотация используется для `return@forEach` и `this@apply` в scope-функциях.
|
|
>
|
|
> Сравнить: [Вложенные классы в Java](../../Программирование%20на%20Java/ООП/Вложенные%20классы.md)
|
|
|
|
> [!warning] Подводные камни
|
|
> **`inner` держит внешний объект в памяти - это утечка.** Если внутренний объект переживает внешний (попал в долгоживущую коллекцию, кеш или обработчик), внешний не будет собран:
|
|
> ```kotlin
|
|
> class Activity2(val heavyData: ByteArray = ByteArray(1024)) {
|
|
> inner class Handler3 {
|
|
> fun handle() = "обработка, внешний объект жив"
|
|
> }
|
|
> }
|
|
>
|
|
> val globalHandlers = mutableListOf<Activity2.Handler3>()
|
|
>
|
|
> fun main() {
|
|
> globalHandlers.add(Activity2().Handler3())
|
|
> println(globalHandlers.first().handle())
|
|
> println("Activity2 не может быть собран: он держится ссылкой из Handler3")
|
|
> }
|
|
> ```
|
|
> Правило: `inner` только когда нужен доступ к состоянию внешнего объекта и время жизни внутреннего не превышает внешнего.
|
|
>
|
|
> **Вложенный класс не видит членов экземпляра внешнего класса** - частая ошибка при переносе кода из Java, где поведение обратное:
|
|
> ```kotlin
|
|
> class Outer8 {
|
|
> private val value = 1
|
|
> class Nested3 {
|
|
> // fun bad() = value // ошибка
|
|
> fun ok() = "нужен inner или параметр"
|
|
> }
|
|
> }
|
|
>
|
|
> fun main() { println(Outer8.Nested3().ok()) }
|
|
> ```
|
|
>
|
|
> **Создание `inner`-класса требует экземпляра внешнего:**
|
|
> ```kotlin
|
|
> class Outer9 {
|
|
> inner class Inner8
|
|
> class Nested4
|
|
> }
|
|
>
|
|
> fun main() {
|
|
> println(Outer9.Nested4()::class.simpleName) // Nested4
|
|
> println(Outer9().Inner8()::class.simpleName) // Inner8
|
|
> // Outer9.Inner8() // ошибка
|
|
> }
|
|
> ```
|
|
>
|
|
> **`data class` не может быть `inner`:**
|
|
> ```kotlin
|
|
> class Outer10 {
|
|
> data class Point5(val x: Int) // вложенный — ок
|
|
> // inner data class Bad(val x: Int) // ошибка
|
|
> }
|
|
>
|
|
> fun main() { println(Outer10.Point5(1)) } // Point5(x=1)
|
|
> ```
|
|
> Сгенерированные `equals` и `copy` не учитывали бы ссылку на внешний объект, что дало бы противоречивое поведение.
|
|
>
|
|
> **Глубокая вложенность ухудшает читаемость.** `Outer.Middle.Inner.Deep` в сигнатурах - сигнал, что классы стоит вынести на верхний уровень или в отдельный файл.
|
|
>
|
|
> **Вложенный класс попадает в публичный API под составным именем.** Переименование внешнего класса меняет полное имя вложенного - для библиотеки это ломающее изменение.
|
|
>
|
|
> **Локальный класс в функции захватывает переменные** и продлевает их жизнь, как и лямбда. Если объект локального класса возвращается из функции, захваченные значения остаются в памяти.
|
|
|
|
Дополнительно: [Object и companion object](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/Object%20и%20companion%20object.md)
|
|
Далее: [Полиморфизм](Полиморфизм.md)
|