Sự khác biệt giữa lớp trường hợp và lớp của Scala là gì?


439

Tôi đã tìm kiếm trong Google để tìm sự khác biệt giữa a case classvà a class. Mọi người đều đề cập rằng khi bạn muốn thực hiện khớp mẫu trên lớp, hãy sử dụng lớp case. Mặt khác, sử dụng các lớp và cũng đề cập đến một số đặc quyền bổ sung như bằng và mã băm ghi đè. Nhưng đây có phải là những lý do duy nhất tại sao người ta nên sử dụng một lớp trường hợp thay vì lớp?

Tôi đoán nên có một số lý do rất quan trọng cho tính năng này trong Scala. Giải thích là gì hoặc có một nguồn để tìm hiểu thêm về các lớp trường hợp Scala từ đâu?

Câu trả lời:


393

Các lớp trường hợp có thể được xem như là các đối tượng lưu trữ dữ liệu đơn giản và không thay đổi, phụ thuộc hoàn toàn vào các đối số hàm tạo của chúng .

Khái niệm chức năng này cho phép chúng ta

  • sử dụng cú pháp khởi tạo nhỏ gọn ( Node(1, Leaf(2), None)))
  • phân tách chúng bằng cách sử dụng khớp mẫu
  • có sự so sánh bình đẳng ngầm định nghĩa

Kết hợp với kế thừa, các lớp trường hợp được sử dụng để bắt chước các kiểu dữ liệu đại số .

Nếu một đối tượng thực hiện các tính toán trạng thái ở bên trong hoặc thể hiện các loại hành vi phức tạp khác, thì đó phải là một lớp bình thường.


11
@Teja: Theo một cách nào đó. ADT là loại enum tham số , cực kỳ mạnh mẽ và an toàn.
Dario

8
Các lớp trường hợp được niêm phong được sử dụng để bắt chước các kiểu dữ liệu đại số. Nếu không, số lượng các lớp con không bị giới hạn.
Thomas Jung

6
@Thomas: Nói một cách chính xác, các lớp trường hợp xuất phát từ các lớp trừu tượng kín bắt chước các kiểu dữ liệu đại số đóng trong khi ADT lại mở .
Dario

2
@Dario ... và loại khác là mở và không và ADT. :-)
Thomas Jung

1
@Thomas: Đúng, đó chỉ là một sự tồn tại;)
Dario

165

Về mặt kỹ thuật, không có sự khác biệt giữa một lớp và một lớp trường hợp - ngay cả khi trình biên dịch không tối ưu hóa một số nội dung khi sử dụng các lớp trường hợp. Tuy nhiên, một lớp trường hợp được sử dụng để loại bỏ tấm nồi hơi cho một mẫu cụ thể, đó là triển khai các kiểu dữ liệu đại số .

Một ví dụ rất đơn giản về các loại như vậy là cây. Cây nhị phân, ví dụ, có thể được thực hiện như thế này:

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

Điều đó cho phép chúng tôi làm như sau:

// 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 =>
}

Lưu ý rằng cây xây dựng và giải cấu trúc (thông qua khớp mẫu) với cùng một cú pháp, đó cũng chính xác là cách chúng được in (trừ khoảng trắng).

Và chúng cũng có thể được sử dụng với các bản đồ hoặc bộ băm, vì chúng có mã băm ổn định, hợp lệ.


71
  • Các lớp trường hợp có thể được khớp mẫu
  • Các lớp trường hợp tự động xác định mã băm và bằng
  • Các lớp trường hợp tự động định nghĩa các phương thức getter cho các đối số hàm tạo.

(Bạn đã đề cập tất cả trừ cái cuối cùng).

Đó là những khác biệt duy nhất cho các lớp học thông thường.


13
Setters không được tạo cho các lớp trường hợp trừ khi "var" được chỉ định trong đối số hàm tạo, trong trường hợp đó bạn có cùng thế hệ getter / setter như các lớp thông thường.
Mitch Blevins

1
@Mitch: Đúng, xấu của tôi. Đã sửa bây giờ.
sepp2k

Bạn đã bỏ qua 2 sự khác biệt, xem câu trả lời của tôi.
Shelby Moore III

@MitchBlevins, các lớp thông thường không phải lúc nào cũng có thế hệ getter / setter.
Shelby Moore III

Các lớp trường hợp định nghĩa phương thức không phù hợp đó là lý do tại sao chúng có thể được khớp mẫu.
Chúc mừng kẻ phản bội

30

Không ai đề cập rằng các lớp trường hợp cũng là trường hợp Productvà do đó kế thừa các phương thức này:

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

trong đó productAritytrả về số lượng tham số lớp, productElement(i)trả về tham số thứ i và cho phép lặp qua chúng.productIterator


2
Tuy nhiên, chúng không phải là phiên bản của Product1, Product2, v.v.
Jean-Philippe Pellet

27

Không ai đề cập rằng các lớp case có valcác tham số constructor nhưng đây cũng là mặc định cho các lớp thông thường (mà tôi nghĩ là không nhất quán trong thiết kế của Scala). Dario ngụ ý như vậy nơi anh lưu ý rằng họ là " bất biến ".

Lưu ý rằng bạn có thể ghi đè mặc định bằng cách thêm vào từng đối số hàm tạo với varcác lớp trường hợp. Tuy nhiên, làm cho các lớp trường hợp có thể thay đổi làm cho chúng equalshashCodecác phương thức biến đổi theo thời gian. [1]

sepp2k đã đề cập rằng các lớp trường hợp tự động tạo equalshashCodecác phương thức.

Ngoài ra không ai đề cập rằng các lớp trường hợp tự động tạo ra một người bạn đồng hành objectcó cùng tên với lớp, trong đó có chứa applyunapplycác phương thức. Các applyphương pháp cho phép xây dựng các trường hợp mà không cần thêm vào trước với new. Các unapplyphương pháp vắt cho phép phù hợp với mô hình mà những người khác nói.

Ngoài ra trình biên dịch tối ưu hóa tốc độ khớp mẫu match- casecho các lớp trường hợp [2].

[1] Lớp học rất tuyệt

[2] Lớp học và trích xuất trường hợp, trang 15 .


12

Cấu trúc lớp trường hợp trong Scala cũng có thể được coi là một sự thuận tiện để loại bỏ một số mẫu soạn sẵn.

Khi xây dựng một lớp trường hợp Scala cung cấp cho bạn như sau.

  • Nó tạo ra một lớp cũng như đối tượng đồng hành của nó
  • Đối tượng đồng hành của nó thực hiện applyphương thức mà bạn có thể sử dụng làm phương thức xuất xưởng. Bạn có được lợi thế cú pháp của việc không phải sử dụng từ khóa mới.

Bởi vì lớp là bất biến, bạn có được các bộ truy cập, chỉ là các biến (hoặc thuộc tính) của lớp nhưng không có bộ biến đổi (vì vậy không có khả năng thay đổi các biến). Các tham số hàm tạo tự động có sẵn cho bạn dưới dạng các trường chỉ đọc công khai. Sử dụng tốt hơn nhiều so với xây dựng Java bean.

  • Bạn cũng nhận được hashCode, equalstoStringcác phương thức theo mặc định và equalsphương thức so sánh một đối tượng có cấu trúc. Một copyphương thức được tạo để có thể sao chép một đối tượng (với một số trường có các giá trị mới được cung cấp cho phương thức).

Ưu điểm lớn nhất như đã được đề cập trước đây là thực tế là bạn có thể tạo mẫu khớp với các lớp tình huống. Lý do cho điều này là bởi vì bạn có được unapplyphương thức cho phép bạn giải cấu trúc một lớp trường hợp để trích xuất các trường của nó.


Về bản chất những gì bạn đang nhận được từ Scala khi tạo một lớp trường hợp (hoặc một đối tượng trường hợp nếu lớp của bạn không có đối số) là một đối tượng đơn lẻ phục vụ mục đích như một nhà máy và như một trình trích xuất .


Tại sao bạn cần một bản sao của một đối tượng bất biến?
Paŭlo Ebermann

@ PaŭloEbermann Vì copyphương pháp có thể sửa đổi các trường:val x = y.copy(foo="newValue")
Thilo

8

Ngoài những gì mọi người đã nói, có một số khác biệt cơ bản hơn giữa classcase class

1. Case Classkhông cần rõ ràng new, trong khi lớp cần được gọi vớinew

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

2.By Tham số hàm tạo mặc định là riêng tư class, trong khi công khai trongcase 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 classso sánh bản thân theo giá trị

// case 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

6

Theo tài liệu của Scala :

Các lớp tình huống chỉ là các lớp thông thường là:

  • Bất biến theo mặc định
  • Có thể phân tách thông qua khớp mẫu
  • So sánh bằng bình đẳng cấu trúc thay vì tham chiếu
  • Tuyệt vời để khởi tạo và hoạt động trên

Một tính năng khác của từ khóa case là trình biên dịch tự động tạo ra một số phương thức cho chúng ta, bao gồm các phương thức toString, bằng và hashCode quen thuộc trong Java.


5

Lớp học:

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
           ^

Nhưng nếu chúng ta sử dụng cùng một mã nhưng sử dụng lớp trường hợp:

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

Lớp người:

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)

Kết hợp mẫu:

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

đối tượng: đơn

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

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

5

Để có sự hiểu biết cuối cùng về một lớp trường hợp là gì:

hãy giả sử định nghĩa lớp trường hợp sau:

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

và sau đó làm như sau trong thiết bị đầu cuối:

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

Scala 2.12.8 sẽ xuất ra:

...
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>();
    ()
  }
}
...

Như chúng ta có thể thấy trình biên dịch Scala tạo ra một lớp Foovà đối tượng đồng hành thông thường Foo.

Chúng ta hãy đi qua lớp tổng hợp và nhận xét về những gì chúng ta đã có:

  • trạng thái nội bộ của Foolớp, bất biến:
val foo: String
val bar: Int
  • getters:
def foo(): String
def bar(): Int
  • phương pháp sao chép:
def copy(foo: String, bar: Int): Foo
def copy$default$1(): String
def copy$default$2(): Int
  • thực hiện scala.Productđặc điểm:
override def productPrefix(): String
def productArity(): Int
def productElement(x$1: Int): Object
override def productIterator(): Iterator
  • thực hiện scala.Equalsđặc điểm để làm cho các trường hợp lớp trường hợp có thể so sánh cho bằng bằng ==:
def canEqual(x$1: Object): Boolean
override def equals(x$1: Object): Boolean
  • ghi đè java.lang.Object.hashCodecho việc tuân theo hợp đồng bằng mã băm:
override <synthetic> def hashCode(): Int
  • ghi đè java.lang.Object.toString:
override def toString(): String
  • constructor để khởi tạo theo newtừ khóa:
def <init>(foo: String, bar: Int): Foo 

Đối tượng Foo: - phương thức applykhởi tạo mà không cần newtừ khóa:

case <synthetic> def apply(foo: String, bar: Int): Foo = new Foo(foo, bar);
  • Phương thức trích xuất unupplyđể sử dụng lớp trường hợp Foo trong khớp mẫu:
case <synthetic> def unapply(x$0: Foo): Option
  • phương pháp để bảo vệ đối tượng dưới dạng singleton khỏi khử lưu huỳnh vì không cho phép tạo thêm một thể hiện:
<synthetic> private def readResolve(): Object = Foo;
  • Đối tượng Foo mở rộng scala.runtime.AbstractFunction2để thực hiện thủ thuật đó:
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 từ đối tượng trả về một funtion để tạo Foo mới bằng cách áp dụng một bộ gồm 2 phần tử.

Vì vậy, trường hợp lớp chỉ là cú pháp đường.


4

Không giống như các lớp, các lớp trường hợp chỉ được sử dụng để giữ dữ liệu.

Các lớp trường hợp linh hoạt cho các ứng dụng tập trung vào dữ liệu, có nghĩa là bạn có thể xác định các trường dữ liệu trong lớp trường hợp và xác định logic nghiệp vụ trong một đối tượng đồng hành. Theo cách này, bạn đang tách dữ liệu khỏi logic nghiệp vụ.

Với phương thức sao chép, bạn có thể kế thừa bất kỳ hoặc tất cả các thuộc tính cần thiết từ nguồn và có thể thay đổi chúng theo ý muốn.


3

Không ai đề cập rằng đối tượng đồng hành lớp trường hợp có sự tupledbảo vệ, trong đó có một loại:

case class Person(name: String, age: Int)
//Person.tupled is def tupled: ((String, Int)) => Person

Trường hợp sử dụng duy nhất tôi có thể tìm thấy là khi bạn cần xây dựng lớp case từ tuple, ví dụ:

val bobAsTuple = ("bob", 14)
val bob = (Person.apply _).tupled(bobAsTuple) //bob: Person = Person(bob,14)

Bạn có thể làm tương tự, mà không cần tupled, bằng cách tạo đối tượng trực tiếp, nhưng nếu bộ dữ liệu của bạn được biểu thị dưới dạng danh sách tuple với arity 20 (tuple với 20 phần tử), có thể sử dụng tupled là lựa chọn của bạn.


3

Một lớp trường hợp là một lớp có thể được sử dụng với match/casecâu lệnh.

def isIdentityFun(term: Term): Boolean = term match {
  case Fun(x, Var(y)) if x == y => true
  case _ => false
}

Bạn thấy điều đó case được theo sau bởi một thể hiện của lớp Fun có tham số thứ 2 là Var. Đây là một cú pháp rất hay và mạnh mẽ, nhưng nó không thể hoạt động với các thể hiện của bất kỳ lớp nào, do đó có một số hạn chế đối với các lớp tình huống. Và nếu những hạn chế này được tuân theo, có thể tự động xác định mã băm và bằng.

Cụm từ mơ hồ "một cơ chế phân rã đệ quy thông qua khớp mẫu" có nghĩa là "nó hoạt động với case". (Thật vậy, trường hợp theo sau matchđược so sánh với (đối sánh với) trường hợp tiếp theo case, Scala phải phân tách cả hai và phải phân tách đệ quy những gì chúng được tạo ra.)

Những trường hợp nào là hữu ích cho? Các bài viết trên Wikipedia về đại số loại dữ liệu đưa ra hai ví dụ tốt cổ điển, danh sách và cây cối. Hỗ trợ cho các loại dữ liệu đại số (bao gồm cả cách biết so sánh chúng) là điều bắt buộc đối với bất kỳ ngôn ngữ chức năng hiện đại nào.

Có gì trường hợp lớp họckhông hữu ích cho? Một số đối tượng có trạng thái, mã như connection.setConnectTimeout(connectTimeout)không dành cho các lớp trường hợp.

Và bây giờ bạn có thể đọc A Tour of Scala: Case Classes


2

Tôi nghĩ rằng tất cả các câu trả lời đã đưa ra một lời giải thích ngữ nghĩa về các lớp và các lớp tình huống. Điều này có thể rất liên quan, nhưng mọi người mới chơi scala nên biết điều gì xảy ra khi bạn tạo một lớp tình huống. Tôi đã viết câu trả lời này , trong đó giải thích trường hợp lớp một cách ngắn gọn.

Mọi lập trình viên nên biết rằng nếu họ đang sử dụng bất kỳ chức năng dựng sẵn nào, thì họ đang viết một mã tương đối ít hơn, điều này cho phép họ bằng cách cho phép viết mã được tối ưu hóa nhất, nhưng sức mạnh đi kèm với trách nhiệm lớn. Vì vậy, sử dụng các chức năng dựng sẵn với rất thận trọng.

Một số nhà phát triển tránh viết các lớp tình huống do có thêm 20 phương thức mà bạn có thể thấy bằng cách phân tách tệp lớp.

Vui lòng tham khảo liên kết này nếu bạn muốn kiểm tra tất cả các phương thức bên trong một lớp trường hợp .


1
  • Các lớp trường hợp định nghĩa một đối tượng compagnon với các phương thức áp dụng và không thích hợp
  • Các lớp trường hợp mở rộng nối tiếp
  • Các lớp trường hợp định nghĩa bằng hashCode và các phương thức sao chép
  • Tất cả các thuộc tính của hàm tạo là val (đường cú pháp)

1

Một số tính năng chính case classesđược liệt kê dưới đây

  1. trường hợp lớp là bất biến.
  2. Bạn có thể khởi tạo các lớp trường hợp mà không cần newtừ khóa.
  3. trường hợp lớp có thể được so sánh theo giá trị

Mã scala mẫu trên scala fiddle, được lấy từ các tài liệu scala.

https://scalafiddle.io/sf 432XEQyE / 0

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.