Подтвердить что ты не робот

Определите свои собственные исключения с помощью перегруженных конструкторов в scala

В исключениях Java есть как минимум эти четыре конструктора:

Exception() 
Exception(String message) 
Exception(String message, Throwable cause) 
Exception(Throwable cause) 

Если вы хотите определить свои собственные расширения, вам просто нужно объявить исключения потомков и реализовать каждый желаемый конструктор, вызывающий супер супер конструктор

Как вы можете достичь того же в scala?

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

4b9b3361

Ответ 1

Значение по умолчанию для cause равно null. И для message это либо cause.toString(), либо null:

val e1 = new RuntimeException()

e.getCause
// res1: java.lang.Throwable = null

e.getMessage
//res2: java.lang.String = null

val cause = new RuntimeException("cause msg")
val e2 = new RuntimeException(cause)

e.getMessage()
//res3: String = java.lang.RuntimeException: cause msg

Таким образом, вы можете просто использовать значения по умолчанию:

class MyException(message: String = null, cause: Throwable = null) extends
  RuntimeException(MyException.defaultMessage(message, cause), cause)

object MyException {
  def defaultMessage(message: String, cause: Throwable) =
    if (message != null) message
    else if (cause != null) cause.toString()
    else null
}

// usage:
new MyException(cause = myCause)
// res0: MyException = MyException: java.lang.RuntimeException: myCause msg

Ответ 2

Хорошо, это лучшее, что я нашел до сих пор

class MissingConfigurationException private(ex: RuntimeException) extends RuntimeException(ex) {
  def this(message:String) = this(new RuntimeException(message))
  def this(message:String, throwable: Throwable) = this(new RuntimeException(message, throwable))
}

object MissingConfigurationException {
  def apply(message:String) = new MissingConfigurationException(message)
  def apply(message:String, throwable: Throwable) = new MissingConfigurationException(message, throwable)
}

таким образом вы можете использовать "новое MissingConfigurationException" или метод apply из объекта-компаньона

В любом случае, я все еще удивляюсь, что нет более простого способа его достижения

Ответ 3

Вы можете использовать Throwable.initCause.

class MyException (message: String, cause: Throwable) 
  extends RuntimeException(message) {
    if (cause != null)
      initCause(cause)

    def this(message: String) = this(message, null)  
}

Ответ 4

Для меня, кажется, существуют три разные потребности, которые имеют динамическое напряжение друг с другом:

  • Удобство расширителя RuntimeException; то есть минимальный код, который должен быть записан для создания потомка RuntimeException
  • Клиент воспринимал легкость использования; т.е. минимальный код, который должен быть записан на сайте вызова
  • Предпочтение клиента избегать утечки страшного Java null в свой код

Если вам неинтересно номер 3, то этот ответ (одноранговый для этого) выглядит довольно кратким.

Однако, если один из значений числа 3 пытается как можно ближе приблизиться к номерам 1 и 2, решение ниже эффективно инкапсулирует утечку Java null в ваш API Scala.

class MyRuntimeException (
  val optionMessage: Option[String],
  val optionCause: Option[Throwable],
  val isEnableSuppression: Boolean,
  val isWritableStackTrace: Boolean
) extends RuntimeException(
  optionMessage match {
    case Some(string) => string
    case None => null
  },
  optionCause match {
    case Some(throwable) => throwable
    case None => null
  },
  isEnableSuppression,
  isWritableStackTrace
) {
  def this() =
    this(None, None, false, false)
  def this(message: String) =
    this(Some(message), None, false, false)
  def this(cause: Throwable) =
    this(None, Some(cause), false, false)
  def this(message: String, cause: Throwable) =
    this(Some(message), Some(cause), false, false)
}

И если вы хотите исключить необходимость использования new, где фактически используется MyRuntimeException, добавьте этот объект-компаньон (который просто переадресует все вызовы приложения к существующему конструктору класса master):

object MyRuntimeException {
  def apply: MyRuntimeException =
    MyRuntimeException()
  def apply(message: String): MyRuntimeException =
    MyRuntimeException(optionMessage = Some(message))
  def apply(cause: Throwable): MyRuntimeException =
    MyRuntimeException(optionCause = Some(cause))
  def apply(message: String, cause: Throwable): MyRuntimeException =
    MyRuntimeException(optionMessage = Some(message), optionCause = Some(cause))
  def apply(
    optionMessage: Option[String] = None,
    optionCause: Option[Throwable] = None,
    isEnableSuppression: Boolean = false,
    isWritableStackTrace: Boolean = false
  ): MyRuntimeException =
    new MyRuntimeException(
      optionMessage,
      optionCause,
      isEnableSuppression,
      isWritableStackTrace
    )
}

Лично я предпочитаю фактически подавлять использование оператора new в максимально возможном количестве кода, чтобы облегчить возможные будущие рефакторинги. Это особенно полезно, если рефакторинг сильно зависит от шаблона Factory. Мой конечный результат, хотя и более подробный, должен быть очень приятным для клиентов.

object MyRuntimeException {
  def apply: MyRuntimeException =
    MyRuntimeException()
  def apply(message: String): MyRuntimeException =
    MyRuntimeException(optionMessage = Some(message))
  def apply(cause: Throwable): MyRuntimeException =
    MyRuntimeException(optionCause = Some(cause))
  def apply(message: String, cause: Throwable): MyRuntimeException =
    MyRuntimeException(optionMessage = Some(message), optionCause = Some(cause))
  def apply(
    optionMessage: Option[String] = None,
    optionCause: Option[Throwable] = None,
    isEnableSuppression: Boolean = false,
    isWritableStackTrace: Boolean = false
  ): MyRuntimeException =
    new MyRuntimeException(
      optionMessage,
      optionCause,
      isEnableSuppression,
      isWritableStackTrace
    )
}

class MyRuntimeException private[MyRuntimeException] (
  val optionMessage: Option[String],
  val optionCause: Option[Throwable],
  val isEnableSuppression: Boolean,
  val isWritableStackTrace: Boolean
) extends RuntimeException(
  optionMessage match {
    case Some(string) => string
    case None => null
  },
  optionCause match {
    case Some(throwable) => throwable
    case None => null
  },
  isEnableSuppression,
  isWritableStackTrace
)


Изучение более сложного шаблона RuntimeException:

Это лишь небольшой скачок от исходного вопроса к желанию создать экосистему специализированного RuntimeException для пакета или API. Идея состоит в том, чтобы определить "корень" RuntimeException, из которого может быть создана новая экосистема конкретных исключений потомков. Для меня было важно сделать использование catch и match намного проще для использования для определенных типов ошибок.

Например, у меня установлен метод validate, который проверяет набор условий до того, как разрешить создание класса case. Каждое неработающее условие генерирует экземпляр RuntimeException. А затем список RuntimeInstance возвращается методом. Это дает клиенту возможность решить, как они хотели бы реагировать; throw исключение списка исключений, сканирование списка для чего-то определенного и throw, или просто нажимать всю цепочку вызовов без привлечения очень дорогой команды JVM throw.

Это конкретное проблемное пространство имеет три разных потомка RuntimeException, один абстрактный (FailedPrecondition) и два конкретных (FailedPreconditionMustBeNonEmptyList и FailedPreconditionsException).

Первый, FailedPrecondition, является прямым потомком до RuntimeException, очень похож на MyRuntimeException, и является абстрактным (для предотвращения прямых экземпляров). FailedPrecondition имеет "признак сопутствующих объектов", FailedPreconditionObject, который действует как экземпляр Factory (подавляет оператор new).

trait FailedPreconditionObject[F <: FailedPrecondition] {
  def apply: F =
    apply()

  def apply(message: String): F =
    apply(optionMessage = Some(message))

  def apply(cause: Throwable): F =
    apply(optionCause = Some(cause))

  def apply(message: String, cause: Throwable): F =
    apply(optionMessage = Some(message), optionCause = Some(cause))

  def apply(
      optionMessage: Option[String] = None
    , optionCause: Option[Throwable] = None
    , isEnableSuppression: Boolean = false
    , isWritableStackTrace: Boolean = false
  ): F
}
abstract class FailedPrecondition (
  val optionMessage: Option[String],
  val optionCause: Option[Throwable],
  val isEnableSuppression: Boolean,
  val isWritableStackTrace: Boolean
) extends RuntimeException(
  optionMessage match {
    case Some(string) => string
    case None => null
  },
  optionCause match {
    case Some(throwable) => throwable
    case None => null
  },
  isEnableSuppression,
  isWritableStackTrace
)

Второй, FailedPreconditionMustBeNonEmptyList, является косвенным потомком RuntimeException и прямой конкретной реализацией FailedPrecondition. Он определяет как объект-компаньон, так и класс. Сопутствующий объект расширяет черту FailedPreconditionObject. И класс просто расширяет абстрактный класс FailedPrecondition и отмечает его final, чтобы предотвратить дальнейшие расширения.

object FailedPreconditionMustBeNonEmptyList extends FailedPreconditionObject[FailedPreconditionMustBeNonEmptyList] {
  def apply(
      optionMessage: Option[String] = None
    , optionCause: Option[Throwable] = None
    , isEnableSuppression: Boolean = false
    , isWritableStackTrace: Boolean = false
  ): FailedPreconditionMustBeNonEmptyList =
    new FailedPreconditionMustBeNonEmptyList(
        optionMessage
      , optionCause
      , isEnableSuppression
      , isWritableStackTrace
    )
}
final class FailedPreconditionMustBeNonEmptyList private[FailedPreconditionMustBeNonEmptyList] (
    optionMessage: Option[String]
  , optionCause: Option[Throwable]
  , isEnableSuppression: Boolean
  , isWritableStackTrace: Boolean
) extends
  FailedPrecondition(
      optionMessage
    , optionCause
    , isEnableSuppression
    , isWritableStackTrace
  )

Третий, FailedPreconditionsException, является прямым потомком до RuntimeException, который обертывает List из FailedPrecondition, а затем динамически управляет испусканием сообщения об исключении.

object FailedPreconditionsException {
  def apply(failedPrecondition: FailedPrecondition): FailedPreconditionsException =
    FailedPreconditionsException(List(failedPrecondition))
  def apply(failedPreconditions: List[FailedPrecondition]): FailedPreconditionsException =
    tryApply(failedPreconditions).get
  def tryApply(failedPrecondition: FailedPrecondition): Try[FailedPreconditionsException] =
    tryApply(List(failedPrecondition))
  def tryApply(failedPreconditions: List[FailedPrecondition]): Try[FailedPreconditionsException] =
    if (failedPreconditions.nonEmpty)
      Success(new FailedPreconditionsException(failedPreconditions))
    else
      Failure(FailedPreconditionMustBeNonEmptyList())
  private def composeMessage(failedPreconditions: List[FailedPrecondition]): String =
    if (failedPreconditions.size > 1)
      s"failed preconditions [${failedPreconditions.size}] have occurred - ${failedPreconditions.map(_.optionMessage.getOrElse("")).mkString("|")}"
    else
      s"failed precondition has occurred - ${failedPreconditions.head.optionMessage.getOrElse("")}"
}
final class FailedPreconditionsException private[FailedPreconditionsException] (
  val failedPreconditions: List[FailedPrecondition]
) extends RuntimeException(FailedPreconditionsException.composeMessage(failedPreconditions))

И затем, объединяя все это вместе и аккуратно, я помещаю как FailedPrecondition, так и FailedPreconditionMustBeNonEmptyList в объект FailedPreconditionsException. И вот как выглядит конечный результат:

object FailedPreconditionsException {
  trait FailedPreconditionObject[F <: FailedPrecondition] {
    def apply: F =
      apply()

    def apply(message: String): F =
      apply(optionMessage = Some(message))

    def apply(cause: Throwable): F =
      apply(optionCause = Some(cause))

    def apply(message: String, cause: Throwable): F =
      apply(optionMessage = Some(message), optionCause = Some(cause))

    def apply(
        optionMessage: Option[String] = None
      , optionCause: Option[Throwable] = None
      , isEnableSuppression: Boolean = false
      , isWritableStackTrace: Boolean = false
    ): F
  }
  abstract class FailedPrecondition (
      val optionMessage: Option[String]
    , val optionCause: Option[Throwable]
    , val isEnableSuppression: Boolean
    , val isWritableStackTrace: Boolean
  ) extends RuntimeException(
    optionMessage match {
      case Some(string) => string
      case None => null
    },
    optionCause match {
      case Some(throwable) => throwable
      case None => null
    },
    isEnableSuppression,
    isWritableStackTrace
  )

  object FailedPreconditionMustBeNonEmptyList extends FailedPreconditionObject[FailedPreconditionMustBeNonEmptyList] {
    def apply(
        optionMessage: Option[String] = None
      , optionCause: Option[Throwable] = None
      , isEnableSuppression: Boolean = false
      , isWritableStackTrace: Boolean = false
    ): FailedPreconditionMustBeNonEmptyList =
      new FailedPreconditionMustBeNonEmptyList(
          optionMessage
        , optionCause
        , isEnableSuppression
        , isWritableStackTrace
      )
  }
  final class FailedPreconditionMustBeNonEmptyList private[FailedPreconditionMustBeNonEmptyList] (
      optionMessage: Option[String]
    , optionCause: Option[Throwable]
    , isEnableSuppression: Boolean
    , isWritableStackTrace: Boolean
  ) extends
    FailedPrecondition(
        optionMessage
      , optionCause
      , isEnableSuppression
      , isWritableStackTrace
    )

  def apply(failedPrecondition: FailedPrecondition): FailedPreconditionsException =
    FailedPreconditionsException(List(failedPrecondition))

  def apply(failedPreconditions: List[FailedPrecondition]): FailedPreconditionsException =
    tryApply(failedPreconditions).get

  def tryApply(failedPrecondition: FailedPrecondition): Try[FailedPreconditionsException] =
    tryApply(List(failedPrecondition))

  def tryApply(failedPreconditions: List[FailedPrecondition]): Try[FailedPreconditionsException] =
    if (failedPreconditions.nonEmpty)
      Success(new FailedPreconditionsException(failedPreconditions))
    else
      Failure(FailedPreconditionMustBeNonEmptyList())
  private def composeMessage(failedPreconditions: List[FailedPrecondition]): String =
    if (failedPreconditions.size > 1)
      s"failed preconditions [${failedPreconditions.size}] have occurred - ${failedPreconditions.map(_.optionMessage.getOrElse("")).mkString("|")}"
    else
      s"failed precondition has occurred - ${failedPreconditions.head.optionMessage.getOrElse("")}"
}
final class FailedPreconditionsException private[FailedPreconditionsException] (
  val failedPreconditions: List[FailedPreconditionsException.FailedPrecondition]
) extends RuntimeException(FailedPreconditionsException.composeMessage(failedPreconditions))

И это будет выглядеть так, как бы клиент использовал вышеуказанный код для создания собственного вывода исключений, называемого FailedPreconditionMustBeNonEmptyString:

object FailedPreconditionMustBeNonEmptyString extends FailedPreconditionObject[FailedPreconditionMustBeNonEmptyString] {
  def apply(
      optionMessage: Option[String] = None
    , optionCause: Option[Throwable] = None
    , isEnableSuppression: Boolean = false
    , isWritableStackTrace: Boolean = false
  ): FailedPreconditionMustBeNonEmptyString =
    new FailedPreconditionMustBeNonEmptyString(
        optionMessage
      , optionCause
      , isEnableSuppression
      , isWritableStackTrace
    )
}
final class FailedPreconditionMustBeNonEmptyString private[FailedPreconditionMustBeNonEmptyString] (
    optionMessage: Option[String]
  , optionCause: Option[Throwable]
  , isEnableSuppression: Boolean
  , isWritableStackTrace: Boolean
) extends
  FailedPrecondition(
      optionMessage
    , optionCause
    , isEnableSuppression
    , isWritableStackTrace
  )

И тогда использование этого исключения выглядит следующим образом:

throw FailedPreconditionMustBeNonEmptyString()

Я вышел далеко за пределы исходного вопроса, потому что мне было трудно найти что-то близкое к тому, чтобы быть как конкретным, так и всеобъемлющим в Scala -ifying RuntimeException в конкретном или расширяться в более общую "экосистему исключений", с которой Я стал таким комфортным, когда на Java.

Мне бы хотелось услышать обратную связь (кроме вариаций, "Вау! Это слишком много для меня".) на моем решении. И мне бы хотелось, чтобы какие-либо дополнительные оптимизации или способы уменьшить многословие БЕЗ УДАЛЕНИЯ любого из значений или терпения, которые я создал для клиентов этого шаблона.

Ответ 5

Scala соответствие шаблонов в блоках try/catch работает на интерфейсах. Моим решением является использование интерфейса для имени исключения, а затем использование отдельных экземпляров класса.

trait MyException extends RuntimeException

class MyExceptionEmpty() extends RuntimeException with MyException

class MyExceptionStr(msg: String) extends RuntimeException(msg) with MyException

class MyExceptionEx(t: Throwable) extends RuntimeException(t) with MyException

object MyException {
  def apply(): MyException = new MyExceptionEmpty()
  def apply(msg: String): MyException = new MyExceptionStr(msg)
  def apply(t: Throwable): MyException = new MyExceptionEx(t)
}

class MyClass {
  try {
    throw MyException("oops")
  } catch {
    case e: MyException => println(e.getMessage)
    case _: Throwable => println("nope")
  }
}

При запуске MyClass будет выводиться "oops".

Ответ 6

Вот аналогичный подход к одному из @roman-borisov, но более типичный.

case class ShortException(message: String = "", cause: Option[Throwable] = None)
    extends Exception(message) {
  cause.foreach(initCause)
}

Затем вы можете создавать Исключения в Java-стиле:

throw ShortException() 
throw ShortException(message) 
throw ShortException(message, Some(cause)) 
throw ShortException(cause = Some(cause))