Новый sealed-подтип молча уходит не в ту ветку when — найдите и исправьте
PaymentResult — sealed-интерфейс. Неделю назад добавили третий вариант, Pending. Сборка осталась зелёной, но платежи в ожидании теперь показываются пользователю как неудачные.
Ограничения: PaymentResult должен остаться sealed, у Pending должно быть своё сообщение, а любой будущий вариант иерархии обязан ломать сборку, а не проскакивать незамеченным.
sealed interface PaymentResult {
data class Approved(val txId: String) : PaymentResult
data class Declined(val reason: String) : PaymentResult
data class Pending(val etaMinutes: Int) : PaymentResult
}
fun message(result: PaymentResult): String = when (result) {
is PaymentResult.Approved -> "Paid, receipt " + result.txId
else -> "Payment failed"
}
Найдите и исправьте ошибку.
Ветка else уничтожает исчерпывающность: пока она есть, компилятор перестаёт проверять when, и Pending молча попадает в else уже во время выполнения. Уберите else и обработайте каждый подтип — тогда любой будущий вариант сломает сборку.
- ✗Добавлять
elseвwhenпо sealed-типу, лишь бы замолчал компилятор - ✗Считать, что новый подтип не проскочит, раз код всё ещё компилируется
- ✗Думать, что
elseи полный набор веток взаимозаменяемы
- →Где ещё в кодовой базе спрячется эта ошибка после добавления нового подтипа?
- →Когда ветка
elseпо sealed-типу всё-таки оправдана?
Почему платёж в ожидании стал неудачным
Ветка else — это обещание компилятору, что все остальные варианты обрабатываются одинаково. Пока она есть, when полон по построению, и проверка на полноту не выполняется вовсе. Поэтому добавление Pending в иерархию не вызвало ни ошибки, ни предупреждения: новый подтип просто провалился в else и получил сообщение о неудаче.
Именно это и отнимает главную выгоду sealed: компилятор знает полный набор прямых наследников, и when без else обязан покрыть их все.
Исправление
fun message(result: PaymentResult): String = when (result) {
is PaymentResult.Approved -> "Paid, receipt " + result.txId
is PaymentResult.Declined -> "Payment failed: " + result.reason
is PaymentResult.Pending -> "Pending, about " + result.etaMinutes + " min"
}
Ветки else больше нет — и это осознанное решение. Когда завтра в иерархию добавят Chargeback, этот when перестанет компилироваться, и его придётся дополнить. Ошибка превращается из молчаливой в невозможную.
Когда else всё же уместен
Когда вариантов много, а различать осмысленно нужно один-два, и вы сознательно согласны, чтобы новые варианты вели себя как «прочие». Для доменного результата вроде платежа это почти всегда неверный выбор.