--- 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() 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): 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(private val value: T) { inner class Wrapper { // T доступен fun get(): T = value fun describe() = "обёртка над ${value}" } class Helper4(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() > > 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)