Scala的case class和class有什么区别?

493

我在谷歌搜索中寻找“case class”和“class”之间的区别。大家都提到,当你想对类进行模式匹配时,请使用“case class”。否则请使用类,并提及一些额外的好处,如等式和哈希码重写。但这些是使用“case class”而不是“class”的唯一原因吗?

我猜这个特性在Scala中应该有一些非常重要的理由。有什么解释或有没有资源可以学习更多关于Scala case classes的知识?

17个回答

448

Case classes 可以被看作是简单且不可变的数据持有对象,应该只依赖于它们的构造函数参数。

这种函数式概念允许我们

  • 使用紧凑的初始化语法(Node(1, Leaf(2), None))
  • 使用模式匹配进行分解
  • 隐式定义相等比较

与继承结合使用,case classes 用于模拟代数数据类型 (algebraic datatypes)

如果一个对象在内部执行状态计算或展示其他形式的复杂行为,则应该是一个普通类。


12
在某种程度上,ADT类似于“参数化的枚举”,非常强大且类型安全。 - Dario
8
密封的case类用于模拟代数数据类型,否则子类数量不受限制。 - Thomas Jung
6
@Thomas:准确地说,派生自密封抽象类的case类模拟了闭合代数数据类型,而ADT在其他情况下是开放的。 - Dario
2
@Dario...而且类型是开放的,不是一个ADT。 :-) - Thomas Jung
2
注意,Scala不强制要求case类是不可变的:case class Foo(var int: Int)。[...] 简单且不可变的数据持有对象[...] - jub0bs
显示剩余6条评论

175

从技术上讲,类和样例类之间没有区别——即使编译器在使用样例类时进行了一些优化。然而,样例类用于消除特定模式的样板代码,这个模式是实现代数数据类型

这种类型的一个非常简单的例子是树。例如,二叉树可以像这样实现:

sealed abstract class Tree
case class Node(left: Tree, right: Tree) extends Tree
case class Leaf[A](value: A) extends Tree
case object EmptyLeaf extends Tree

这使我们能够做到以下事情:
// DSL-like assignment:
val treeA = Node(EmptyLeaf, Leaf(5))
val treeB = Node(Node(Leaf(2), Leaf(3)), Leaf(5))

// On Scala 2.8, modification through cloning:
val treeC = treeA.copy(left = treeB.left)

// Pretty printing:
println("Tree A: "+treeA)
println("Tree B: "+treeB)
println("Tree C: "+treeC)

// Comparison:
println("Tree A == Tree B: %s" format (treeA == treeB).toString)
println("Tree B == Tree C: %s" format (treeB == treeC).toString)

// Pattern matching:
treeA match {
  case Node(EmptyLeaf, right) => println("Can be reduced to "+right)
  case Node(left, EmptyLeaf) => println("Can be reduced to "+left)
  case _ => println(treeA+" cannot be reduced")
}

// Pattern matches can be safely done, because the compiler warns about
// non-exaustive matches:
def checkTree(t: Tree) = t match {
  case Node(EmptyLeaf, Node(left, right)) =>
  // case Node(EmptyLeaf, Leaf(el)) =>
  case Node(Node(left, right), EmptyLeaf) =>
  case Node(Leaf(el), EmptyLeaf) =>
  case Node(Node(l1, r1), Node(l2, r2)) =>
  case Node(Leaf(e1), Leaf(e2)) =>
  case Node(Node(left, right), Leaf(el)) =>
  case Node(Leaf(el), Node(left, right)) =>
  // case Node(EmptyLeaf, EmptyLeaf) =>
  case Leaf(el) =>
  case EmptyLeaf =>
}

请注意,树的构建和析构(通过模式匹配)使用相同的语法,这也是它们打印时的语法(减去空格)。
它们还可以与哈希映射或集合一起使用,因为它们具有有效且稳定的hashCode。

79
  • 案例类可以进行模式匹配。
  • 案例类会自动定义哈希码和等于方法。
  • 案例类会自动为构造函数参数定义getter方法。

这些是与普通类的唯一区别。


13
除非在构造函数参数中指定了 "var",否则无法为 case 类生成 setter。如果指定了 "var",则会像常规类一样生成相同的 getter/setter。 - Mitch Blevins
1
@Mitch:没错,是我的错。现在已经修复了。 - sepp2k
您忽略了2个差异,请看我的答案。 - Shelby Moore III
@MitchBlevins,普通类并不总是会生成getter/setter。 - Shelby Moore III
案例类定义了unapply方法,因此它们可以进行模式匹配。 - Happy Torturer
嘿,不要同意“那些只是差异”的说法。 CASE类会自动创建带有实现工厂apply()的COMPANION对象。Case类是可序列化的:您可以在分布式平台上使用实例,就像在这里使用jvm一样(例如Akka)。还有更多。所以,不要太严格了。 - Juozas

32
没有人提到,案例类有val构造函数参数,但这也是常规类的默认值(我认为这是Scala设计上的不一致之处)。Dario暗示了这一点,他指出它们是“不可变的”。
请注意,您可以通过在案例类中每个构造函数参数前面添加var来覆盖默认设置。然而,使案例类可变会导致它们的equalshashCode方法成为时间变量[1]。 sepp2k已经提到,案例类自动生成equalshashCode方法。
此外,没有人提到,案例类会自动创建一个与类名相同的伴生object,其中包含applyunapply方法。 apply方法使得不需要使用new即可构造实例。unapply提取器方法使得其他人提到的模式匹配成为可能。
此外,编译器还优化了针对案例类的match-case模式匹配的速度[2]。
[1] 案例类很酷 [2] 案例类和提取器,第15页

30

没有人提到,case class 也是 Product 的实例,因此继承了这些方法:

def productElement(n: Int): Any
def productArity: Int
def productIterator: Iterator[Any]

productArity 返回类参数的数量,productElement(i) 返回第 ith 个参数,productIterator 允许对它们进行迭代。


3
但它们并不是Product1,Product2等的实例。 - Jean-Philippe Pellet

18
在Scala中,case class的构造方式也可以看作是一种方便的方式来去除某些样板代码。
在构造一个case class时,Scala会提供以下内容:
  • 它创建一个类及其伴生对象。
  • 它的伴生对象实现了apply方法,您可以将其用作工厂方法。您获得了不必使用new关键字的语法糖优势。
因为类是不可变的,所以您会得到访问器,这些访问器只是类的变量(或属性),但没有任何变量改变的方法。构造函数参数自动作为公共只读字段提供给您使用,比Java bean更加易于使用。
此外,您还默认获得hashCodeequalstoString方法。其中equals方法会对一个对象进行结构性比较,并且会生成一个copy方法用于克隆对象(提供一些新值给该方法的字段)。
正如先前提到的那样,最大的优势在于您可以对case class进行模式匹配。原因是您可以得到unapply方法,该方法让您可以分解一个case class并提取其字段。
本质上,在创建case class(或者如果您的类不带参数,则创建case object)时,您会从Scala中得到一个单例对象,该对象具有工厂和提取器的作用。

为什么需要不可变对象的副本? - Paŭlo Ebermann
@PaŭloEbermann 因为 copy 方法可以修改字段:val x = y.copy(foo="newValue") - Thilo
1
谢谢您提供的描述。虽然有更受欢迎的答案,但这个答案用简单明了的英语最好地描述了案例的区别。作为一个刚接触Scala的人,这对我来说是最有意义和相对完整的解释。 - rockhowse

15

除了其他人已经提到的内容,classcase class之间还有一些基本差异。

1. Case Class不需要显式使用new,而class需要用new进行调用。

val classInst = new MyClass(...)  // For classes
val classInst = MyClass(..)       // For case class

2.在class中,默认构造函数参数是私有的,而在case class中是公共的。

// For class
class MyClass(x:Int) { }
val classInst = new MyClass(10)

classInst.x   // FAILURE : can't access

// For caseClass
case class MyClass(x:Int) { }
val classInst = MyClass(10)

classInst.x   // SUCCESS

3. case class按值比较自身

// For Class
class MyClass(x:Int) { }
 
val classInst = new MyClass(10)
val classInst2 = new MyClass(10)

classInst == classInst2 // FALSE

// For Case Class
case class MyClass(x:Int) { }
 
val classInst = MyClass(10)
val classInst2 = MyClass(10)

classInst == classInst2 // TRUE

11

为了对case class有最终的理解:

让我们假设以下的case class定义:

case class Foo(foo:String, bar: Int)

然后在终端中执行以下操作:

$ scalac -print src/main/scala/Foo.scala

Scala 2.12.8将输出:

...
case class Foo extends Object with Product with Serializable {

  <caseaccessor> <paramaccessor> private[this] val foo: String = _;

  <stable> <caseaccessor> <accessor> <paramaccessor> def foo(): String = Foo.this.foo;

  <caseaccessor> <paramaccessor> private[this] val bar: Int = _;

  <stable> <caseaccessor> <accessor> <paramaccessor> def bar(): Int = Foo.this.bar;

  <synthetic> def copy(foo: String, bar: Int): Foo = new Foo(foo, bar);

  <synthetic> def copy$default$1(): String = Foo.this.foo();

  <synthetic> def copy$default$2(): Int = Foo.this.bar();

  override <synthetic> def productPrefix(): String = "Foo";

  <synthetic> def productArity(): Int = 2;

  <synthetic> def productElement(x$1: Int): Object = {
    case <synthetic> val x1: Int = x$1;
        (x1: Int) match {
            case 0 => Foo.this.foo()
            case 1 => scala.Int.box(Foo.this.bar())
            case _ => throw new IndexOutOfBoundsException(scala.Int.box(x$1).toString())
        }
  };

  override <synthetic> def productIterator(): Iterator = scala.runtime.ScalaRunTime.typedProductIterator(Foo.this);

  <synthetic> def canEqual(x$1: Object): Boolean = x$1.$isInstanceOf[Foo]();

  override <synthetic> def hashCode(): Int = {
     <synthetic> var acc: Int = -889275714;
     acc = scala.runtime.Statics.mix(acc, scala.runtime.Statics.anyHash(Foo.this.foo()));
     acc = scala.runtime.Statics.mix(acc, Foo.this.bar());
     scala.runtime.Statics.finalizeHash(acc, 2)
  };

  override <synthetic> def toString(): String = scala.runtime.ScalaRunTime._toString(Foo.this);

  override <synthetic> def equals(x$1: Object): Boolean = Foo.this.eq(x$1).||({
      case <synthetic> val x1: Object = x$1;
        case5(){
          if (x1.$isInstanceOf[Foo]())
            matchEnd4(true)
          else
            case6()
        };
        case6(){
          matchEnd4(false)
        };
        matchEnd4(x: Boolean){
          x
        }
    }.&&({
      <synthetic> val Foo$1: Foo = x$1.$asInstanceOf[Foo]();
      Foo.this.foo().==(Foo$1.foo()).&&(Foo.this.bar().==(Foo$1.bar())).&&(Foo$1.canEqual(Foo.this))
  }));

  def <init>(foo: String, bar: Int): Foo = {
    Foo.this.foo = foo;
    Foo.this.bar = bar;
    Foo.super.<init>();
    Foo.super./*Product*/$init$();
    ()
  }
};

<synthetic> object Foo extends scala.runtime.AbstractFunction2 with Serializable {

  final override <synthetic> def toString(): String = "Foo";

  case <synthetic> def apply(foo: String, bar: Int): Foo = new Foo(foo, bar);

  case <synthetic> def unapply(x$0: Foo): Option =
     if (x$0.==(null))
        scala.None
     else
        new Some(new Tuple2(x$0.foo(), scala.Int.box(x$0.bar())));

  <synthetic> private def readResolve(): Object = Foo;

  case <synthetic> <bridge> <artifact> def apply(v1: Object, v2: Object): Object = Foo.this.apply(v1.$asInstanceOf[String](), scala.Int.unbox(v2));

  def <init>(): Foo.type = {
    Foo.super.<init>();
    ()
  }
}
...

正如我们所看到的,Scala编译器生成了一个常规类Foo和伴生对象Foo

让我们浏览编译后的类并评论一下我们得到了什么:

  • Foo类的内部状态是不可变的:
val foo: String
val bar: Int
  • 获取器:
def foo(): String
def bar(): Int
  • 复制方法:
def copy(foo: String, bar: Int): Foo
def copy$default$1(): String
def copy$default$2(): Int
  • 实现 scala.Product 特质:
override def productPrefix(): String
def productArity(): Int
def productElement(x$1: Int): Object
override def productIterator(): Iterator
  • 实现 scala.Equals 特质,使得 case class 实例可以通过 == 进行相等性比较:
def canEqual(x$1: Object): Boolean
override def equals(x$1: Object): Boolean
  • 覆盖 java.lang.Object.hashCode 以遵守equals-hashcode契约:
override <synthetic> def hashCode(): Int
  • 覆盖 java.lang.Object.toString 方法:
override def toString(): String
  • new 关键字实例化的构造函数:
def <init>(foo: String, bar: Int): Foo 

对象 Foo: - 使用 apply 方法实现无需 new 关键字的实例化:

case <synthetic> def apply(foo: String, bar: Int): Foo = new Foo(foo, bar);
  • 提取器方法unapply用于在模式匹配中使用案例类Foo:
case <synthetic> def unapply(x$0: Foo): Option
  • 保护对象作为单例模式不被反序列化以防止产生多个实例的方法:
<synthetic> private def readResolve(): Object = Foo;
  • scala.runtime.AbstractFunction2 是可用于执行此技巧的类,可以通过对象 Foo 进行调用:
scala> case class Foo(foo:String, bar: Int)
defined class Foo

scala> Foo.tupled
res1: ((String, Int)) => Foo = scala.Function2$$Lambda$224/1935637221@9ab310b

tupled 方法返回一个函数,通过应用包含两个元素的元组来创建一个新的 Foo。

因此,case class 只是一种语法糖。


7

根据Scala的文档

案例类只是常规类,它们具有以下特点:

  • 默认情况下不可变
  • 可以通过模式匹配进行分解
  • 通过结构相等性而非引用比较进行比较
  • 简洁易懂地实例化和操作

case关键字的另一个特点是编译器会自动生成一些方法,包括Java中熟悉的toString、equals和hashCode方法。


6

类:

scala> class Animal(name:String)
defined class Animal

scala> val an1 = new Animal("Padddington")
an1: Animal = Animal@748860cc

scala> an1.name
<console>:14: error: value name is not a member of Animal
       an1.name
           ^

但是如果我们使用相同的代码,但使用case class:

scala> case class Animal(name:String)
defined class Animal

scala> val an2 = new Animal("Paddington")
an2: Animal = Animal(Paddington)

scala> an2.name
res12: String = Paddington


scala> an2 == Animal("fred")
res14: Boolean = false

scala> an2 == Animal("Paddington")
res15: Boolean = true

人类:

scala> case class Person(first:String,last:String,age:Int)
defined class Person

scala> val harry = new Person("Harry","Potter",30)
harry: Person = Person(Harry,Potter,30)

scala> harry
res16: Person = Person(Harry,Potter,30)
scala> harry.first = "Saily"
<console>:14: error: reassignment to val
       harry.first = "Saily"
                   ^
scala>val saily =  harry.copy(first="Saily")
res17: Person = Person(Saily,Potter,30)

scala> harry.copy(age = harry.age+1)
res18: Person = Person(Harry,Potter,31)

Pattern Matching:

scala> harry match {
     | case Person("Harry",_,age) => println(age)
     | case _ => println("no match")
     | }
30

scala> res17 match {
     | case Person("Harry",_,age) => println(age)
     | case _ => println("no match")
     | }
no match

对象:单例模式:

scala> case class Person(first :String,last:String,age:Int)
defined class Person

scala> object Fred extends Person("Fred","Jones",22)
defined object Fred

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