在Scala中使用重载构造函数定义自己的异常

37

在Java中,异常至少有以下四个构造函数:

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

如果你想定义自己的扩展,只需声明一个派生异常并实现每个所需的构造函数调用相应的超级构造函数即可。
在Scala中,如何实现相同的功能?
到目前为止,我看到了这篇文章和这个SO答案,但我怀疑一定有更简单的方法来实现这样一个常见的事情。
6个回答

59

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

异常应该扩展RuntimeException吗? - opensas
很好的答案,而且,如果你将它声明为一个 case class,你就可以摆脱 "new" 了... - opensas
1
@opensas object MyException { def apply(message: String = null, cause: Throwable = null) = new MyException(message, cause)} 已经足够了。 - senia
2
不幸的是,RuntimeException(cause)Runtime(null, cause) 的行为不同,因为前者设置了 detailMessage = cause.toString(),而后者则将 detailMessage = null。因此,上述代码的行为将不会与在Java中可能调用适当的超类构造函数完全相同。 - Cory Klein

12

嗯,这是我目前找到的最好的东西。

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)
}

你可以使用"new MissingConfigurationException"或 companion 对象中的 apply 方法来实现这一点。

无论如何,我仍然感到惊讶,居然没有更简单的方法来实现它。


6
你可以使用 Throwable.initCause
class MyException (message: String, cause: Throwable) 
  extends RuntimeException(message) {
    if (cause != null)
      initCause(cause)

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

5
对我来说,似乎有三种不同的需求彼此之间存在动态张力:
1. RuntimeException扩展器的便利性;即创建RuntimeException后代所需编写的最小代码。 2. 客户端感知易用性;即在调用现场编写的最小代码。 3. 客户端避免将可怕的Java null泄漏到他们的代码中的偏好。
如果一个人不关心第三点,则这个答案的peer(this answer)似乎相当简洁。
然而,如果一个人重视第三点,同时尽可能接近第一点和第二点,下面的解决方案可以有效地将Java null泄漏封装到您的Scala API中。
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)
}

如果您想消除在实际使用MyRuntimeException时必须使用new的情况,请添加此伴生对象(它只是将所有apply调用转发到现有的“主”类构造函数):

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运算符,以便于未来进行重构。如果这种重构特别强调工厂模式,那么这样做尤其有帮助。我的最终结果虽然更冗长,但对客户使用来说应该非常好。

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的生态系统,只需要跨越一小步。这个想法是定义一个“根”RuntimeException,从中可以创建一个新的特定后代异常的生态系统。对我来说,重要的是使使用catchmatch在特定类型的错误上更容易利用。

例如,我定义了一个validate方法,它在允许创建一个case类之前验证一组条件。每个失败的条件都会生成一个RuntimeException实例。然后方法返回RuntimeInstance列表。这使客户端能够决定他们如何处理响应;抛出持有异常列表、扫描列表寻找特定异常并将其抛出或者只是将整个异常传递给调用链,而不触发昂贵的JVMthrow命令。

这个特定的问题空间有三个RuntimeException的不同后代,一个抽象类(FailedPrecondition)和两个具体类(FailedPreconditionMustBeNonEmptyListFailedPreconditionsException)。

第一个FailedPreconditionRuntimeException的直接后代,非常类似于MyRuntimeException,并且是抽象的(为了防止直接实例化)。 FailedPrecondition有一个“伴生对象特质”FailedPreconditionObject,它充当实例化工厂(抑制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的直接子类,它包装了一个FailedPreconditionList,并动态地管理异常消息的发出。
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))

然后将所有内容整合起来并使其更加整洁,我将FailedPreconditionFailedPreconditionMustBeNonEmptyList都放在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中实现RuntimeException或者扩展到更一般的"异常生态系统"都很难找到既具体又全面的资料,而在Java中我已经习惯了这种模式。
我很想听到反馈(除了变体,比如“哇!对我来说太冗长了。”)。我也希望有任何额外的优化或减少冗余的方法,但不会失去我已经为此模式的客户所创造的价值和简洁性。

0
这是一种类似于@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)) 

0

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"。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接