В чем разница между запечатанным классом и принципом наследования в Kotlin?

#java #kotlin #enums #sealed-class

#java #kotlin #перечисления #запечатанный класс

Вопрос:

Я новичок в Kotlin. Я читаю книгу, и запечатанный класс отображается там как «расширение» Enum. Я не вижу сходства между ними. На мой взгляд, запечатанный класс больше связан с наследованием, потому что каждый класс может наследовать от него и добавлять к нему функции и свойства, например:

 sealed class messageType
class MessageSuccess (var msg: String) : MwssageType()
class MessageFailure (var msg: String, var e: Exeception) : MwssageType()
  

Я не вижу здесь значений, подобных тем, которые мы имеем в Enum, только излом наследования.
Может кто-нибудь объяснить мне, что такое представить между Enum и Sealed, которые я не могу найти?
Может быть, его сила заключается в использовании его с выражением when?

Комментарии:

1. Из документа Закрытые классы в некотором смысле являются расширением классов enum: набор значений для типа enum также ограничен, но каждая константа enum существует только как один экземпляр, тогда как подкласс закрытого класса может иметь несколько экземпляров, которые могут содержать состояние , которое имеет смысл для вас?

2. @Eklavya, я прочитал ссылку, которую вы прикрепили. Я не вижу сходства. Можете ли вы, пожалуйста, помочь мне понять?

3. Закрытый класс, в котором каждый подтип является object синглтоном, в точности эквивалентен перечислению.

4. @LouisWasserman, но мы можем создать столько экземпляров подтипа, сколько пожелаем. Это цитата из книги, которую я читаю: И в отличие от класса enum, вы можете создавать несколько экземпляров каждого типа.

5. @mohsen, это такой сложный язык. Иногда мне хочется плакать.. Мне не хватает Java

Ответ №1:

Я думаю, что документация подразумевает расширение, на самом деле это не расширение перечислений, а инструмент, подобный перечислениям, с большей мощностью, поскольку он может удерживать состояние. давайте посмотрим на ваш пример с перечислениями.

 sealed class SealedMessageType
class MessageSuccess (val msg: String) : SealedMessageType()
class MessageFailure (val e: Exeception) : SealedMessageType()

enum class EnumMessageType {
    Success,
    Failure
}
  

и теперь, если вы используете перечисления, у вас есть:

 val enumMessageType: EnumMessageType = callNetwork()

    when(enumMessageType) {
        EnumMessageType.Success -> { TODO() }
        EnumMessageType.Failure -> { TODO() }
    }
  

здесь, когда вы используете перечисления, вы не можете получить данные вашего результата из перечислений, и вам нужно получить сообщение или ошибку с какой-либо другой переменной. единственное, что вы можете получить, это тип результата без его состояния. но с запечатанными классами:

 val sealedMessageType: SealedMessageType = callNetwork()

    when(sealedMessageType) {
        is MessageSuccess -> { println(sealedMessageType.msg) }
        is MessageFailure -> { throw sealedMessageType.e }
    }
  

среда IDE может интеллектуальным образом преобразовать ваш результат, и вы можете получить состояние вашего результата (в случае успеха — сообщение, в случае сбоя — исключение). это то, что документ подразумевает под расширением.

но в целом вы правы, запечатанный класс связан с наследованием. на самом деле запечатанный класс — это не что иное, как абстрактный класс, который имеет частный конструктор и не может быть создан. давайте посмотрим на декопилированный java-код:

 @Metadata(
   mv = {1, 4, 0},
   bv = {1, 0, 3},
   k = 1,
   d1 = {"u0000u0014nu0002u0018u0002nu0002u0010u0000nu0000nu0002u0018u0002nu0002u0018u0002nu0000b6u0018u00002u00020u0001Bu0007bu0002¢u0006u0002u0010u0002u0082u0001u0002u0003u0004¨u0006u0005"},
   d2 = {"Lcom/example/customview/SealedMessageType;", "", "()V", "Lcom/example/customview/MessageSuccess;", "Lcom/example/customview/MessageFailure;", "app"}
)
public abstract class SealedMessageType {
   private SealedMessageType() {
   }

   // $FF: synthetic method
   public SealedMessageType(DefaultConstructorMarker $constructor_marker) {
      this();
   }
}
  
 @Metadata(
   mv = {1, 4, 0},
   bv = {1, 0, 3},
   k = 1,
   d1 = {"u0000u0012nu0002u0018u0002nu0002u0018u0002nu0000nu0002u0010u000enu0002bu0004u0018u00002u00020u0001Bru0012u0006u0010u0002u001au00020u0003¢u0006u0002u0010u0004Ru0011u0010u0002u001au00020u0003¢u0006bnu0000u001au0004bu0005u0010u0006¨u0006u0007"},
   d2 = {"Lcom/example/customview/MessageSuccess;", "Lcom/example/customview/SealedMessageType;", "msg", "", "(Ljava/lang/String;)V", "getMsg", "()Ljava/lang/String;", "app"}
)
public final class MessageSuccess extends SealedMessageType {
   @NotNull
   private final String msg;

   @NotNull
   public final String getMsg() {
      return this.msg;
   }

   public MessageSuccess(@NotNull String msg) {
      Intrinsics.checkNotNullParameter(msg, "msg");
      super((DefaultConstructorMarker)null);
      this.msg = msg;
   }
}
  

здесь вы можете видеть, что SealedMessageType на самом деле это абстрактный класс. единственное различие между абстрактным классом и закрытым классом заключается в том, что компилятор генерирует некоторые метаданные для закрытых классов и может предупреждать вас об отсутствующих ветвях при использовании when ключевого слова, что невозможно сделать с помощью абстрактных классов. в приведенном выше коде вы можете видеть, что SealedMessageType метаданные класса содержат оба MessageSuccess и MessageFailure как дочерние классы, а MessageSuccess метаданные также содержатся SealedMessageType как родительские. таких метаданных нет, если вы используете абстрактные классы.

если вы используете этот простой трюк, компилятор выдает вам ошибку, если вы пропустите какие-либо ветви, и IDE может помочь вам реализовать отсутствующие ветви с помощью Alt Enter . хитрость заключается в определении исчерпывающей функции расширения:

 
fun main() {
    when(callNetwork()) { // error: when' expression must be exhaustive, add necessary 'is MessageSuccess', 'is MessageFailure' branches or 'else' branch instead

    }.exhaustive()
}

fun Any?.exhaustive() = this



fun callNetwork(): SealedMessageType {
    TODO()
}
  

Комментарии:

1. @moshen, спасибо за подробное объяснение! Но: 1. Где вы видите метаданные MessageFailute? 2. об исчерпывающем , если «случаи» (не нашли лучшего названия для значений), скажем, выполняют только println(«Что-то»), так какой объект будет запускать исчерпывающий метод?

2. 1. посмотрите на декомпилированный код java, над определением SealedMessageType класса вы можете увидеть метаданные 2. exhaustive метод должен использоваться в when инструкции, как вы можете видеть в примере. это потому, что выдает ошибку, когда вы используете when ключевое слово в качестве выражения, если вы используете его как оператор, например, мы используем know , он не выдает никаких ошибок. этот трюк заставляет when ключевое слово возвращать что-то, что делает его выражением для получения дополнительной информации обратитесь к kotlinlang.org/docs/reference/control-flow.html

3. я так старался понять, что вы написали в комментарии, но безуспешно. Может быть, это потому, что я не могу понять разницу между выражением и оператором, что бы я ни делал. В любом случае спасибо. Я одобряю ваш ответ

4. выражение возвращает некоторое значение, а оператор — нет. например, в java if это оператор, но в kotlin он может использоваться либо как оператор, либо как выражение. он может возвращать некоторое значение. ****************** val max = if (a > b) a else b вы можете видеть, что if здесь используется как выражение, потому что оно возвращает значение. то же самое происходит для when

5. @moshen, хорошо, я понимаю, спасибо. Но, допустим, все значения (случаи) в when, все, что они делают, это: println(что-то). Какой объект будет возвращен, поэтому мы можем добавить *.exclusive() . вам нужен любой объект, но единственное, что нужно, это println(что-то). кто будет «запускать» исчерпывающий метод?

Ответ №2:

В терминах функционального программирования запечатанный класс используется для создания категории,