Запечатанные классы и интерфейсы
Запечатанные классы и интерфейсы обеспечивают контролируемое наследование в иерархиях классов. Все прямые подклассы запечатанного класса известны во время компиляции. Другие подклассы не могут появиться за пределами модуля и пакета, в которых определён запечатанный класс. То же правило применяется к запечатанным интерфейсам и их реализациям: после компиляции модуля с запечатанным интерфейсом нельзя создать новые реализации.
Если сочетать запечатанные классы и интерфейсы с выражением when, можно учесть поведение всех возможных подклассов и гарантировать, что новые подклассы не появятся и не повлияют на код неблагоприятным образом.
Запечатанные классы лучше всего подходят для следующих сценариев:
Требуется ограничить наследование классов: у вас есть заранее определённый конечный набор подклассов, расширяющих класс, и все они известны во время компиляции.
Требуется типобезопасный дизайн: безопасность и сопоставление с образцом имеют решающее значение для вашего проекта. Особенно это важно при управлении состоянием или обработке сложной условной логики. Пример см. в разделе Использование запечатанных классов с выражениями when.
Работа с закрытыми API: вы хотите создавать надёжные и удобные в сопровождении публичные API библиотек, которые гарантируют, что сторонние клиенты используют API предусмотренным образом.
Более подробные практические примеры приведены в разделе Сценарии использования.
Объявление запечатанного класса или интерфейса
Чтобы объявить запечатанный класс или интерфейс, используйте модификатор sealed:
// Create a sealed interface sealed interface Error // Create a sealed class that implements sealed interface Error sealed class IOError(): Error // Define subclasses that extend sealed class 'IOError' class FileReadError(val file: File): IOError() class DatabaseError(val source: DataSource): IOError() // Create a singleton object implementing the 'Error' sealed interface object RuntimeError : Error
Этот пример может представлять API библиотеки, содержащий классы ошибок, чтобы пользователи библиотеки могли обрабатывать ошибки, которые она может генерировать. Если иерархия таких классов ошибок включает интерфейсы или абстрактные классы, доступные в публичном API, ничто не мешает другим разработчикам реализовывать их или расширять в клиентском коде. Поскольку библиотека не знает об ошибках, объявленных за её пределами, она не может обрабатывать их так же, как собственные классы. Однако благодаря запечатанной иерархии классов ошибок авторы библиотеки могут быть уверены, что знают все возможные типы ошибок и что впоследствии другие типы ошибок не появятся.
Иерархия в примере выглядит так:
Конструкторы
Сам запечатанный класс всегда является абстрактным классом, поэтому его нельзя инстанцировать напрямую. Однако он может содержать конструкторы или наследовать их. Эти конструкторы предназначены не для создания экземпляров самого запечатанного класса, а для его подклассов. Рассмотрим следующий пример с запечатанным классом Error и несколькими его подклассами, экземпляры которых мы создаём:
sealed class Error(val message: String) {
class NetworkError : Error("Network failure")
class DatabaseError : Error("Database cannot be reached")
class UnknownError : Error("An unknown error has occurred")
}
fun main() {
val errors = listOf(Error.NetworkError(), Error.DatabaseError(), Error.UnknownError())
errors.forEach { println(it.message) }
}
// Network failure
// Database cannot be reached
// An unknown error has occurred
В запечатанных классах можно использовать классы enum, чтобы представлять состояния константами перечисления и предоставлять дополнительные сведения. Каждая константа перечисления существует только в виде единственного экземпляра, тогда как у подклассов запечатанного класса может быть несколько экземпляров. В примере sealed class Error вместе с несколькими подклассами использует enum для обозначения степени серьёзности ошибки. Конструктор каждого подкласса инициализирует severity и может изменять его состояние:
enum class ErrorSeverity { MINOR, MAJOR, CRITICAL }
sealed class Error(val severity: ErrorSeverity) {
class FileReadError(val file: File): Error(ErrorSeverity.MAJOR)
class DatabaseError(val source: DataSource): Error(ErrorSeverity.CRITICAL)
object RuntimeError : Error(ErrorSeverity.CRITICAL)
// Additional error types can be added here
}
Конструкторы запечатанных классов могут иметь одну из двух областей видимости: protected (по умолчанию) или private:
sealed class IOError {
// A sealed class constructor has protected visibility by default. It's visible inside this class and its subclasses
constructor() { /*...*/ }
// Private constructor, visible inside this class only.
// Using a private constructor in a sealed class allows for even stricter control over instantiation, enabling specific initialization procedures within the class.
private constructor(description: String): this() { /*...*/ }
// This will raise an error because public and internal constructors are not allowed in sealed classes
// public constructor(code: Int): this() {}
}
Наследование
Прямые подклассы запечатанных классов и интерфейсов должны быть объявлены в одном пакете. Они могут быть классами верхнего уровня или быть вложенными в любое количество других именованных классов, интерфейсов или объектов. Подклассы могут иметь любую область видимости, если она соответствует обычным правилам наследования в Kotlin, в том числе правилам переопределения свойств.
У подклассов запечатанных классов должно быть полное имя. Они не могут быть локальными или анонимными объектами.
Эти ограничения не распространяются на косвенные подклассы. Если прямой подкласс запечатанного класса не помечен как запечатанный, его можно расширять любым способом, допускаемым его модификаторами:
// Sealed interface 'Error' has implementations only in the same package and module sealed interface Error // Sealed class 'IOError' extends 'Error' and is extendable only within the same package sealed class IOError(): Error // Open class 'CustomError' extends 'Error' and can be extended anywhere it's visible open class CustomError(): Error
Наследование в мультиплатформенных проектах
В мультиплатформенных проектах действует ещё одно ограничение наследования: прямые подклассы запечатанных классов должны находиться в одном наборе исходного кода. Оно применяется к запечатанным классам без модификаторов expect и actual.
Если запечатанный класс объявлен как expect в общем наборе исходного кода и имеет реализации actual в наборах исходного кода для платформ, то и версии expect, и actual могут иметь подклассы в своих наборах исходного кода. Кроме того, при использовании иерархической структуры можно создавать подклассы в любом наборе исходного кода между объявлениями expect и actual.
Подробнее об иерархической структуре мультиплатформенных проектов.
Использование запечатанных классов с выражением when
Главное преимущество запечатанных классов проявляется при их использовании в выражении when. Выражение when с запечатанным классом позволяет компилятору Kotlin исчерпывающе проверить, что учтены все возможные случаи. В таких ситуациях добавлять предложение else не нужно:
// Sealed class and its subclasses
sealed class Error {
class FileReadError(val file: String): Error()
class DatabaseError(val source: String): Error()
object RuntimeError : Error()
}
//sampleStart
// Function to log errors
fun log(e: Error) = when(e) {
is Error.FileReadError -> println("Error while reading file ${e.file}")
is Error.DatabaseError -> println("Error while reading from database ${e.source}")
Error.RuntimeError -> println("Runtime error")
// No `else` clause is required because all the cases are covered
}
//sampleEnd
// List all errors
fun main() {
val errors = listOf(
Error.FileReadError("example.txt"),
Error.DatabaseError("usersDatabase"),
Error.RuntimeError
)
errors.forEach { log(it) }
}
При использовании запечатанных классов с выражениями when можно также добавлять условия-защиты, чтобы включать дополнительные проверки в одну ветвь. Подробнее см. в разделе Условия-защиты в выражениях when.
Сценарии использования
Рассмотрим несколько практических сценариев, в которых запечатанные классы и интерфейсы могут быть особенно полезны.
Управление состоянием в приложениях с пользовательским интерфейсом
Запечатанные классы можно использовать для представления различных состояний пользовательского интерфейса в приложении. Такой подход обеспечивает структурированную и безопасную обработку изменений интерфейса. В этом примере показано, как управлять различными состояниями пользовательского интерфейса:
sealed class UIState {
data object Loading : UIState()
data class Success(val data: String) : UIState()
data class Error(val exception: Exception) : UIState()
}
fun updateUI(state: UIState) {
when (state) {
is UIState.Loading -> showLoadingIndicator()
is UIState.Success -> showData(state.data)
is UIState.Error -> showError(state.exception)
}
}
Обработка способов оплаты
В практических бизнес-приложениях часто требуется эффективно обрабатывать различные способы оплаты. Для реализации такой бизнес-логики можно использовать запечатанные классы с выражениями when. Представление различных способов оплаты в виде подклассов запечатанного класса создаёт понятную и удобную для сопровождения структуру обработки транзакций:
sealed class Payment {
data class CreditCard(val number: String, val expiryDate: String) : Payment()
data class PayPal(val email: String) : Payment()
data object Cash : Payment()
}
fun processPayment(payment: Payment) {
when (payment) {
is Payment.CreditCard -> processCreditCardPayment(payment.number, payment.expiryDate)
is Payment.PayPal -> processPayPalPayment(payment.email)
is Payment.Cash -> processCashPayment()
}
}
Payment — это запечатанный класс, представляющий различные способы оплаты в системе электронной коммерции: CreditCard, PayPal и Cash. Каждый подкласс может иметь собственные свойства, например number и expiryDate для CreditCard и email для PayPal.
Функция processPayment() показывает, как обрабатывать различные способы оплаты. Такой подход гарантирует, что будут учтены все возможные типы оплаты, а система останется гибкой и позволит добавлять новые способы оплаты в будущем.
Обработка запросов и ответов API
Запечатанные классы и интерфейсы можно использовать для реализации системы аутентификации пользователей, обрабатывающей запросы и ответы API. Система аутентификации пользователей поддерживает вход и выход из системы. Запечатанный интерфейс ApiRequest определяет конкретные типы запросов: LoginRequest для входа и LogoutRequest для выхода из системы. Запечатанный класс ApiResponse инкапсулирует различные сценарии ответа: UserSuccess с данными пользователя, UserNotFound для отсутствующих пользователей и Error для любых сбоев. Функция handleRequest обрабатывает эти запросы типобезопасным способом с помощью выражения when, а getUserById имитирует получение данных пользователя:
// Import necessary modules
import io.ktor.server.application.*
import io.ktor.server.resources.*
import kotlinx.serialization.*
// Define the sealed interface for API requests using Ktor resources
@Resource("api")
sealed interface ApiRequest
@Serializable
@Resource("login")
data class LoginRequest(val username: String, val password: String) : ApiRequest
@Serializable
@Resource("logout")
object LogoutRequest : ApiRequest
// Define the ApiResponse sealed class with detailed response types
sealed class ApiResponse {
data class UserSuccess(val user: UserData) : ApiResponse()
data object UserNotFound : ApiResponse()
data class Error(val message: String) : ApiResponse()
}
// User data class to be used in the success response
data class UserData(val userId: String, val name: String, val email: String)
// Function to validate user credentials (for demonstration purposes)
fun isValidUser(username: String, password: String): Boolean {
// Some validation logic (this is just a placeholder)
return username == "validUser" && password == "validPass"
}
// Function to handle API requests with detailed responses
fun handleRequest(request: ApiRequest): ApiResponse {
return when (request) {
is LoginRequest -> {
if (isValidUser(request.username, request.password)) {
ApiResponse.UserSuccess(UserData("userId", "userName", "userEmail"))
} else {
ApiResponse.Error("Invalid username or password")
}
}
is LogoutRequest -> {
// Assuming logout operation always succeeds for this example
ApiResponse.UserSuccess(UserData("userId", "userName", "userEmail")) // For demonstration
}
}
}
// Function to simulate a getUserById call
fun getUserById(userId: String): ApiResponse {
return if (userId == "validUserId") {
ApiResponse.UserSuccess(UserData("validUserId", "John Doe", "john@example.com"))
} else {
ApiResponse.UserNotFound
}
// Error handling would also result in an Error response.
}
// Main function to demonstrate the usage
fun main() {
val loginResponse = handleRequest(LoginRequest("user", "pass"))
println(loginResponse)
val logoutResponse = handleRequest(LogoutRequest)
println(logoutResponse)
val userResponse = getUserById("validUserId")
println(userResponse)
val userNotFoundResponse = getUserById("invalidId")
println(userNotFoundResponse)
}
Что дальше
Подробнее о наследовании в Kotlin.
© 2010–2026 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/sealed-classes.html