案例类,模式匹配和varargs

时间:2012-02-09 22:51:45

标签: scala pattern-matching

假设我有这样的类层次结构:

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

我想知道,每个定义在模式匹配方面有哪些缺点/好处?

3 个答案:

答案 0 :(得分:17)

让我们回答这里涉及的不同问题。我的确推荐这种语法:

case class ExpList(listExp: Expr*) extends Expr

但答案取决于您的编码示例。那么让我们看看如何在模式匹配中使用varargs,何时使用List以及WrappedArray的问题。 一句小话:ExprExpList(有或没有'r'?)之间的不一致是有问题的,当打字并试图记住哪一个 - 坚持一个约定,Exp是足够清楚并经常使用。

Varargs和模式匹配

让我们首先考虑这个声明:

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.Seqcollection.mutable.Seqcollection.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

我希望这会阻止人们像我一样绊倒这个错误。