← Назад к вопросам

Как наследоваться от sealed class

1.7 Middle🔥 171 комментариев
#Kotlin основы#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI21 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Как наследоваться от sealed class в Kotlin

Что такое sealed class

Sealed class — это класс, который ограничивает иерархию наследования. Все прямые подклассы sealed class должны быть объявлены в одном файле (в Kotlin 1.0) или в одном пакете (в Kotlin 1.1+).

Это особенно полезно для моделирования ограниченных иерархий типов, когда ты знаешь заранее, какие подтипы возможны.

Правила наследования от sealed class

1. Прямые наследники должны быть в одном файле/пакете

Это основное ограничение sealed class:

// файл: sealed.kt
sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

// Всё в одном файле!

2. Подклассы могут быть:

a) Data class:

sealed class Result {
    data class Success(val data: String) : Result()
    data class Error(val message: String) : Result()
}

val result: Result = Result.Success("Hello")
when (result) {
    is Result.Success -> println(result.data)  // Автоматический smart cast
    is Result.Error -> println(result.message)
}

b) Regular class:

sealed class Shape {
    class Circle(val radius: Float) : Shape()
    class Rectangle(val width: Float, val height: Float) : Shape()
}

c) Object singleton:

sealed class Status {
    object Loading : Status()
    object Success : Status()
    data class Error(val message: String) : Status()
}

d) Даже другой sealed class:

sealed class Expression {
    sealed class BinaryOp : Expression() {
        data class Add(val left: Expression, val right: Expression) : BinaryOp()
        data class Subtract(val left: Expression, val right: Expression) : BinaryOp()
    }
    
    data class Literal(val value: Int) : Expression()
}

Практические примеры

Пример 1: Моделирование Result/Either

Это классический пример использования sealed class:

sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

// Использование
suspend fun fetchUser(): Result<User> {
    return try {
        val user = api.getUser()
        Result.Success(user)
    } catch (e: Exception) {
        Result.Error(e)
    }
}

// Pattern matching
when (val result = fetchUser()) {
    is Result.Success -> {
        // Автоматический smart cast: result.data имеет тип User
        println("User: ${result.data.name}")
    }
    is Result.Error -> {
        // result.exception доступно
        logger.error(result.exception)
    }
    Result.Loading -> {
        // Loading не имеет поля данных
        showProgressBar()
    }
}

Пример 2: Моделирование UI состояния в MVVM

sealed class UiState<out T> {
    object Idle : UiState<Nothing>()
    object Loading : UiState<Nothing>()
    data class Content<T>(val data: T) : UiState<T>()
    data class Error(val error: Throwable) : UiState<Nothing>()
}

class UserViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState<User>>(UiState.Idle)
    val uiState: StateFlow<UiState<User>> = _uiState.asStateFlow()
    
    fun loadUser(userId: String) {
        viewModelScope.launch {
            _uiState.value = UiState.Loading
            _uiState.value = try {
                UiState.Content(userRepository.getUser(userId))
            } catch (e: Exception) {
                UiState.Error(e)
            }
        }
    }
}

// В UI (Compose)
@Composable
fun UserScreen(viewModel: UserViewModel) {
    val uiState by viewModel.uiState.collectAsState()
    
    when (val state = uiState) {
        UiState.Idle -> Text("Start loading")
        UiState.Loading -> CircularProgressIndicator()
        is UiState.Content -> UserCard(state.data)  // Smart cast
        is UiState.Error -> ErrorMessage(state.error.message)
    }
}

Пример 3: Моделирование событий

sealed class AnalyticsEvent {
    data class ScreenViewed(val screenName: String) : AnalyticsEvent()
    data class ButtonClicked(val buttonId: String, val timestamp: Long) : AnalyticsEvent()
    data class ErrorOccurred(val errorCode: Int, val message: String) : AnalyticsEvent()
}

class AnalyticsTracker {
    fun logEvent(event: AnalyticsEvent) {
        when (event) {
            is AnalyticsEvent.ScreenViewed -> {
                // event.screenName доступно
                sendToFirebase("screen_view", mapOf("name" to event.screenName))
            }
            is AnalyticsEvent.ButtonClicked -> {
                sendToFirebase("button_click", mapOf("id" to event.buttonId))
            }
            is AnalyticsEvent.ErrorOccurred -> {
                sendToFirebase("error", mapOf(
                    "code" to event.errorCode,
                    "message" to event.message
                ))
            }
        }
    }
}

Ограничения sealed class

До Kotlin 1.1

  • Прямые наследники должны быть в одном файле
  • Нельзя иметь наследников в других файлах пакета

В Kotlin 1.1+

  • Прямые наследники должны быть в одном пакете
  • Но могут быть в разных файлах одного пакета

Всегда

  • Косвенные наследники (наследники наследников) могут быть в разных пакетах
  • sealed class не может быть private или internal в пределах класса

Сравнение с альтернативами

sealed class vs enum

// enum — каждое значение singleton
enum class Status {
    IDLE, LOADING, SUCCESS, ERROR
}

// sealed class — каждый подкласс может иметь свои данные
sealed class Status {
    object Idle : Status()
    object Loading : Status()
    data class Success(val data: String) : Status()
    data class Error(val message: String) : Status()
}

sealed class vs open class

// open class — кто угодно может наследоваться
open class Animal { }
class Dog : Animal() { }  // ✓ разрешено
class Cat : Animal() { }  // ✓ разрешено

// sealed class — только известные подклассы
sealed class Animal { }
class Dog : Animal() { }  // ✓ разрешено (если в одном файле)
class Cat : Animal() { }  // ✓ разрешено (если в одном файле)

Best Practices

  1. Используй sealed class для ограниченных иерархий — когда ты знаешь все возможные подтипы
  2. Combine с when expression — полнота проверки гарантирована компилятором
  3. Используй для Result/Either типов — очень естественно для обработки ошибок
  4. Используй для UI состояния — чистая и type-safe модель состояния
  5. Не переусложняй — если иерархия может быть расширяемой, используй open class