Scala类在其随播对象中使用apply
和unapply
方法很常见。
unapply
的行为是明确的:如果其参数是或者可以转换为类的有效实例,则返回Some
。否则,请返回None
。
举一个具体的例子,让我们设想一个Url
案例类:
object Url {
def apply(str: String): Url = ???
def unapply(str: String): Option[Url] = ???
}
case class Url(protocol: String, host: String, path: String)
如果str
是有效网址,则unapply
将返回Some[Url]
,否则为None
。
apply
对我来说有点不太清楚:它如何应对str
不是有效的网址?
来自Java世界,我的第一直觉是抛出IllegalArgumentException
,这将允许我们将伴侣对象实现为:
object Url {
def apply(str: String): Url = ... // some function that parses a URI and throws if it fails.
def unapply(str: String): Option[Url] = Try(apply(str)).toOption
}
我理解这在功能世界中并不算是非常好的做法(例如,在this answer中解释过)。
另一种方法是让apply
返回Option[Url]
,在这种情况下,它会成为unapply
的简单克隆,最好是未实现的。
这是正确的结论吗?这类潜在失败的apply
方法是否应该实现?在这种情况下,投掷是否正常?我还没有看到第三种选择吗?
答案 0 :(得分:7)
这有点主观,但我认为你不应该这样做。
假设您允许apply
失败,即抛出异常或返回空选项。然后执行val url = Url(someString)
可能会失败,尽管看起来非常像构造函数。这就是整个问题:伴随对象的apply
方法应该可靠地为您构造新实例,并且您无法从任意字符串中可靠地构造Url
实例。所以不要这样做。
unapply
通常应该用于获取有效的Url
对象,并返回另一个可以再次创建Url
的表示形式。作为一个例子,查看为case类生成的unapply
方法,它只返回一个包含构造它的参数的元组。因此签名实际上应该是def unapply(url: Url): String
。
所以我的结论是都不应该用于构建Url
。我认为让方法def parse(str: String): Option[Url]
明确你正在做什么(解析字符串)并且它可能会失败是最惯用的。然后,您可以Url.parse(someString).map(url => ...)
使用Url
实例。
答案 1 :(得分:3)
在None
的情况下,提取器的用户通常负责决定如何处理失败,并且模式匹配语法使得这非常方便。这是一个稍微简单的例子:
import scala.util.Try
object IntString {
def unapply(s: String) = Try(s.toInt).toOption
}
现在我们可以写下列任何内容:
def intStringEither(s: String): Either[String, Int] = s match {
case IntString(i) => Right(i)
case invalid => Left(invalid)
}
或者:
def intStringOption(s: String): Option[Int] = s match {
case IntString(i) => Some(i)
case _ => None
} // ...or equivalently just `= IntString.unapply(s)`.
或者:
def intStringCustomException(s: String): Int = s match {
case IntString(i) => i
case invalid => throw MyCustomParseFailure(invalid)
}
这种灵活性是关于提取器的好处之一,如果你在unapply
中抛出(非致命)异常,那么就会使这种灵活性短路。