假设我有这样的类层次结构:
abstract class Expr
case class Var(name: String) extends Expr
case class ExpList(listExp: List[Expr]) extends Expr
最好像这样定义ExpList
的构造函数:
case class ExpList(listExp: Expr*) extends Expr
我想知道,每个定义在模式匹配方面有哪些缺点/好处?
答案 0 :(得分:17)
让我们回答这里涉及的不同问题。我的确推荐这种语法:
case class ExpList(listExp: Expr*) extends Expr
但答案取决于您的编码示例。那么让我们看看如何在模式匹配中使用varargs,何时使用List
以及WrappedArray的问题。
一句小话:Expr
和ExpList
(有或没有'r'?)之间的不一致是有问题的,当打字并试图记住哪一个 - 坚持一个约定,Exp
是足够清楚并经常使用。
让我们首先考虑这个声明:
abstract class Expr
case class ExpList(listExp: Expr*) extends Expr
case class Var(name: String) extends Expr
这个代码示例:
val v = Var("a")
val result = for (i <- Seq(ExpList(v), ExpList(v, v), ExpList(v, v, v))) yield (i match {
case ExpList(a) => "Length 1 (%s)" format a
case ExpList(a, b, c, d @ _*) => "Length >= 3 (%s, %s, %s, %s...)" format (a, b, c, d)
case ExpList(args @ _*) => "Any length: " + args
})
result foreach println
产生
Length 1 (Var(a))
Any length: WrappedArray(Var(a), Var(a))
Length >= 3 (Var(a), Var(a), Var(a), WrappedArray()...)
我在这里使用的是:
ExpList(a, b)
匹配带有两个子节点的ExpList; ExpList(a)
将ExpList与一个子项匹配。
_*
是匹配类型为A
的值的序列的模式,其可以任意长(包括0)。
我还使用模式绑定器identifier @ pattern
,它允许绑定一个对象,同时还用另一个模式进一步解构它;它们适用于任何模式,而不仅仅是_*
。
使用identifier @ _*
时,identifier
必须键入Seq[A]
。
所有这些结构也适用于Seq
;但是如果我们在声明中使用Seq
,就像这样:
case class ExpList(listExp: Seq[Expr]) extends Expr
相同的case子句从(例如)case ExpList(a, b, c, d @ _*) =>
更改为case ExpList(Seq(a, b, c, d @ _*)) =>
。所以语法更混乱。
从语法上讲,Expr*
唯一“更难”的是写下面的函数,它从表达式列表构造一个ExpList:
def f(x: Seq[Expr]) = ExpList(x: _*)
请注意_*
在此使用(再次)。
List
类 List
很方便,如xs match { case head :: tail => ... case Nil => }
中所示。但是,通常使用折叠可以更紧凑地表达此代码,如果您不使用此样式编写代码,则无需使用List
。特别是在界面中,通常只需要您的代码需要它。
我们上面讨论的是不变性。案例类的实例应该是不可变的。现在,当使用Expr*
时,case类的参数实际上是类型collection.Seq[Expr]
,并且此类型包含可变实例 - 实际上,ExprList将接收子类WrappedArray
的实例,这是可变的。请注意,collection.Seq
是collection.mutable.Seq
和collection.immutable.Seq
的超类,而后者默认为Seq
别名。
如果没有向下转播它,就不能改变这样的价值,但有人可能会这样做(我不知道是什么原因)。
如果要阻止客户端执行此操作,则需要将接收的值转换为不可变序列 - 但在使用case class ExpList(listExp: Expr*) extends Expr
声明ExpList时无法执行此操作。
您需要使用其他构造函数。
要在其他代码中进行转换,由于toSeq
返回原始序列,因此必须使用列表内容作为可变参数调用Seq
的构造函数。因此,您使用我上面显示的语法Seq(listExpr: _*)
。
目前这并不重要,因为Seq
的默认实现是List
,但这可能会在未来发生变化(也许更快,谁知道?)。
无法声明同一方法的两个重载,一个采用T*
,一个采用Seq[T]
,因为在输出类中它们将变为相同。可以使用一个小技巧来使m看起来不同并且有两个构造函数:
case class ExpList(listExp: Seq[Expr]) extends Expr
object ExpList {
def apply(listExp: Expr*)(implicit d: DummyImplicit) = new ExpList(Seq(listExp: _*))
}
这里我也将数组转换为不可变序列,如上所述。遗憾的是,模式匹配已完成,如上例所示,案例类接受Seq[Expr]
而不是Expr*
。
答案 1 :(得分:12)
你可以拥有两个构造函数:
case class ExpList(listExp: List[Expr]) extends Expr
object ExpList {
def apply(listExp: Expr*) = new ExpList(listExp.toList)
}
//now you can do
ExpList(List(Var("foo"), Var("bar")))
//or
ExpList(Var("foo"), Var("bar"))
可变参数转换为mutable.WrappedArray
,因此为了与不可变的案例类的约定保持一致,您应该使用列表作为实际值。
答案 2 :(得分:1)
正如对Dan的解决方案的评论:如果你在函数中有这个,那么由于Scala中的错误不起作用https://issues.scala-lang.org/browse/SI-3772。你会得到类似的东西:
scala> :paste
// Entering paste mode (ctrl-D to finish)
def g(){
class Expr {}
case class ExpList(listExp: List[Expr]) extends Expr
object ExpList {
def apply(listExp: Expr*) = new ExpList(listExp.toList)
}
}
// Exiting paste mode, now interpreting.
<console>:10: error: ExpList is already defined as (compiler-generated) case cla
ss companion object ExpList
object ExpList {
^
目前,解决方法只是将对象放在第一位。
scala> :paste
// Entering paste mode (ctrl-D to finish)
def g(){
class Expr {}
object ExpList {
def apply(listExp: Expr*) = new ExpList(listExp.toList)
}
case class ExpList(listExp: List[Expr]) extends Expr
}
// Exiting paste mode, now interpreting.
g: ()Unit
我希望这会阻止人们像我一样绊倒这个错误。