声明 Scala 案例类有哪些缺点?

发布于 2024-10-11 05:15:00 字数 302 浏览 2 评论 0原文

如果您正在编写使用大量漂亮的、不可变的数据结构的代码,则案例类似乎是天赐之物,只需一个关键字即可免费为您提供以下所有功能:

  • 默认情况下一切都是不可变的
  • Getters 自动定义
  • 体面的 toString() 实现
  • 兼容equals() 和 hashCode()
  • 伴随对象与 unapply() 方法进行匹配

但是将不可变数据结构定义为案例类有哪些缺点呢?

它对班级或其客户施加了哪些限制?

在某些情况下您应该更喜欢非案例课程吗?

If you're writing code that's using lots of beautiful, immutable data structures, case classes appear to be a godsend, giving you all of the following for free with just one keyword:

  • Everything immutable by default
  • Getters automatically defined
  • Decent toString() implementation
  • Compliant equals() and hashCode()
  • Companion object with unapply() method for matching

But what are the disadvantages of defining an immutable data structure as a case class?

What restrictions does it place on the class or its clients?

Are there situations where you should prefer a non-case class?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

偏爱你一生 2024-10-18 05:15:00

首先是好的方面:

默认情况下一切都是不可变的

是的,如果需要的话甚至可以覆盖(使用var

自动获取器定义

可以在任何类中通过在参数前添加 val

体面的 toString() 实现

是的,非常有用,但如果需要,可以在任何类上手动完成

兼容 equals()hashCode()

与简单的模式匹配相结合,这是人们使用案例类的主要原因

带有 unapply() 方法的 Companion 对象进行匹配

也可以使用提取器手动处理任何类

这个列表还应该包括超级强大的复制方法,这是 Scala 2.8 中最好的东西之一


然后不好的是,案例类只有少数真正的限制:

您不能使用与编译器生成的方法相同的签名在伴生对象中定义 apply

但实际上,这很少是问题。更改生成的 apply 方法的行为肯定会让用户感到惊讶,并且强烈建议不要这样做,这样做的唯一理由是验证输入参数 - 最好在主构造函数主体中完成这项任务(这也使得在使用 时可以进行验证>copy)

你不能子类化

True,尽管案例类本身仍然有可能是后代。一种常见的模式是构建特征的类层次结构,使用案例类作为树的叶节点。

还值得注意的是 sealed 修饰符。具有此修饰符的特征的任何子类必须在同一文件中声明。当对特征的实例进行模式匹配时,如果您没有检查所有可能的具体子类,编译器可以警告您。当与案例类结合使用时,如果代码在没有警告的情况下进行编译,则可以为您提供非常高水平的信心。

作为 Product 的子类,case 类不能有超过 22 个参数

没有真正的解决方法,除了停止滥用具有这么多参数的类:)

另外...

有时会注意到的另一个限制是 Scala(当前)不支持惰性参数(类似于惰性值,但作为参数)。解决方法是使用按名称参数并将其分配给构造函数中的惰性 val。不幸的是,按名称参数不会与模式匹配混合,这会阻止该技术与案例类一起使用,因为它会破坏编译器生成的提取器。

如果您想要实现功能强大的惰性数据结构,这一点是相关的,并且有望通过在 Scala 的未来版本中添加惰性参数来解决。

First the good bits:

Everything immutable by default

Yes, and can even be overridden (using var) if you need it

Getters automatically defined

Possible in any class by prefixing params with val

Decent toString() implementation

Yes, very useful, but doable by hand on any class if necessary

Compliant equals() and hashCode()

Combined with easy pattern-matching, this is the main reason that people use case classes

Companion object with unapply() method for matching

Also possible to do by hand on any class by using extractors

This list should also include the uber-powerful copy method, one of the best things to come to Scala 2.8


Then the bad, there are only a handful of real restrictions with case classes:

You can't define apply in the companion object using the same signature as the compiler-generated method

In practice though, this is rarely a problem. Changing behaviour of the generated apply method is guaranteed to surprise users and should be strongly discouraged, the only justification for doing so is to validate input parameters - a task best done in the main constructor body (which also makes the validation available when using copy)

You can't subclass

True, though it's still possible for a case class to itself be a descendant. One common pattern is to build up a class hierarchy of traits, using case classes as the leaf nodes of the tree.

It's also worth noting the sealed modifier. Any subclass of a trait with this modifier must be declared in the same file. When pattern-matching against instances of the trait, the compiler can then warn you if you haven't checked for all possible concrete subclasses. When combined with case classes this can offer you a very high level level of confidence in your code if it compiles without warning.

As a subclass of Product, case classes can't have more than 22 parameters

No real workaround, except to stop abusing classes with this many params :)

Also...

One other restriction sometimes noted is that Scala doesn't (currently) support lazy params (like lazy vals, but as parameters). The workaround to this is to use a by-name param and assign it to a lazy val in the constructor. Unfortunately, by-name params don't mix with pattern matching, which prevents the technique being used with case classes as it breaks the compiler-generated extractor.

This is relevant if you want to implement highly-functional lazy data structures, and will hopefully be resolved with the addition of lazy params to a future release of Scala.

难忘№最初的完美 2024-10-18 05:15:00

一个很大的缺点:案例类不能扩展案例类。这就是限制。

您错过的其他优点,为了完整性而列出:兼容的序列化/反序列化,无需使用“new”关键字来创建。

对于具有可变状态、私有状态或无状态的对象(例如大多数单例组件),我更喜欢非大小写类。几乎所有其他内容的案例课程。

One big disadvantage: a case classes can't extend a case class. That's the restriction.

Other advantages you missed, listed for completeness: compliant serialization/deserialization, no need to use "new" keyword to create.

I prefer non-case classes for objects with mutable state, private state, or no state (e.g. most singleton components). Case classes for pretty much everything else.

树深时见影 2024-10-18 05:15:00

我认为 TDD 原则适用于此:不要过度设计。当您将某个东西声明为 case 类 时,您就声明了很多功能。这会降低你将来改变班级的灵活性。

例如,case 类 在构造函数参数上有一个 equals 方法。当你第一次编写你的类时,你可能不关心这一点,但是,后来,你可能会决定你希望平等忽略其中一些参数,或者做一些不同的事情。但是,客户端代码可能会同时编写,这取决于case class 相等性。

I think the TDD principle apply here: do not over-design. When you declare something to be a case class, you are declaring a lot of functionality. That will decrease the flexibility you have in changing the class in the future.

For example, a case class has an equals method over the constructor parameters. You may not care about that when you first write your class, but, latter, may decide you want equality to ignore some of these parameters, or do something a bit different. However, client code may be written in the mean time that depends on case class equality.

原谅过去的我 2024-10-18 05:15:00

在某些情况下您应该更喜欢非案例类吗?

Martin Odersky 在他的课程Scala 函数式编程原理(讲座 4.6 - 模式)中为我们提供了一个良好的起点当我们必须在类和案例类之间进行选择时可以使用匹配)。
Scala By Examples 的第 7 章包含相同的示例。

比如说,我们想为算术表达式编写一个解释器。到
首先让事情变得简单,我们只限于数字
和 + 操作。这样的表达式可以表示为一个类
层次结构,以抽象基类 Expr 为根,两个
Number 和 Sum 的子类。那么,表达式 1 + (3 + 7) 将表示为

new Sum( 新 Number(1), new Sum( 新 Number(3), new Number(7)))

abstract class Expr {
  def eval: Int
}

class Number(n: Int) extends Expr {
  def eval: Int = n
}

class Sum(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval + e2.eval
}

此外,添加新的 Prod 类不需要对现有代码进行任何更改:

class Prod(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval * e2.eval
}

相反,添加新方法需要修改所有现有类。

abstract class Expr { 
  def eval: Int 
  def print
} 

class Number(n: Int) extends Expr { 
  def eval: Int = n 
  def print { Console.print(n) }
}

class Sum(e1: Expr, e2: Expr) extends Expr { 
  def eval: Int = e1.eval + e2.eval
  def print { 
   Console.print("(")
   print(e1)
   Console.print("+")
   print(e2)
   Console.print(")")
  }
}

案例类解决了同样的问题。

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
}
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr

添加新方法是局部更改。

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
  }
}

添加新的 Prod 类可能需要更改所有模式匹配。

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
    case Prod(e1,e2) => e1.eval * e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
    case Prod(e1,e2) => ...
  }
}

视频讲座的文字记录4.6 模式匹配

这两种设计都非常好,在它们之间进行选择有时是一个风格问题,但无论如何,有一些标准很重要。

一个标准可能是,您是否更经常创建新的表达式子类,或者您是否更经常创建新方法?因此,这是一个着眼于未来可扩展性和可能的​​扩展传递的标准您的系统。

如果你所做的主要是创建新的子类,那么实际上面向对象的分解解决方案就占据了上风。原因是,使用 eval 方法创建一个新的子类是非常简单且非常局部的更改,而在功能解决方案中,您必须返回并更改 eval 方法内的代码,并且向其中添加一个新案例。

另一方面,如果您所做的将创建大量新方法,但类层次结构本身将保持相对稳定,那么模式匹配实际上是有利的。因为,模式匹配解决方案中的每个新方法都只是本地更改,无论您将其放在基类中,还是放在类层次结构之外。而诸如面向对象分解中所示的新方法则需要每个子类都有一个新的增量。所以会有更多的部分,你必须触摸。

因此,这种二维可扩展性的问题(您可能想要向层次结构添加新类,或者您可能想要添加新方法,或者两者兼而有之)已被命名为表达式问题.

请记住:我们必须将此作为起点,而不是唯一的标准。

输入图像描述这里

Are there situations where you should prefer a non-case class?

Martin Odersky gives us a good starting point in his course Functional Programming Principles in Scala (Lecture 4.6 - Pattern Matching) that we could use when we must choose between class and case class.
The chapter 7 of Scala By Example contains the same example.

Say, we want to write an interpreter for arithmetic expressions. To
keep things simple initially, we restrict ourselves to just numbers
and + operations. Such expres- sions can be represented as a class
hierarchy, with an abstract base class Expr as the root, and two
subclasses Number and Sum. Then, an expression 1 + (3 + 7) would be represented as

new Sum( new Number(1), new Sum( new Number(3), new Number(7)))

abstract class Expr {
  def eval: Int
}

class Number(n: Int) extends Expr {
  def eval: Int = n
}

class Sum(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval + e2.eval
}

Furthermore, adding a new Prod class does not entail any changes to existing code:

class Prod(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval * e2.eval
}

In contrast, add a new method requires modification of all existing classes.

abstract class Expr { 
  def eval: Int 
  def print
} 

class Number(n: Int) extends Expr { 
  def eval: Int = n 
  def print { Console.print(n) }
}

class Sum(e1: Expr, e2: Expr) extends Expr { 
  def eval: Int = e1.eval + e2.eval
  def print { 
   Console.print("(")
   print(e1)
   Console.print("+")
   print(e2)
   Console.print(")")
  }
}

The same problem solved with case classes.

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
}
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr

Adding a new method is a local change.

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
  }
}

Adding a new Prod class requires potentially change all pattern matching.

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
    case Prod(e1,e2) => e1.eval * e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
    case Prod(e1,e2) => ...
  }
}

Transcript from the videolecture 4.6 Pattern Matching

Both of these designs are perfectly fine and choosing between them is sometimes a matter of style, but then nevertheless there are some criteria that are important.

One criteria could be, are you more often creating new sub-classes of expression or are you more often creating new methods? So it's a criterion that looks at the future extensibility and the possible extension pass of your system.

If what you do is mostly creating new subclasses, then actually the object oriented decomposition solution has the upper hand. The reason is that it's very easy and a very local change to just create a new subclass with an eval method, where as in the functional solution, you'd have to go back and change the code inside the eval method and add a new case to it.

On the other hand, if what you do will be create lots of new methods, but the class hierarchy itself will be kept relatively stable, then pattern matching is actually advantageous. Because, again, each new method in the pattern matching solution is just a local change, whether you put it in the base class, or maybe even outside the class hierarchy. Whereas a new method such as show in the object oriented decomposition would require a new incrementation is each sub class. So there would be more parts, That you have to touch.

So the problematic of this extensibility in two dimensions, where you might want to add new classes to a hierarchy, or you might want to add new methods, or maybe both, has been named the expression problem.

Remember: we must use this like a starting point and not like the only criteria.

enter image description here

灼疼热情 2024-10-18 05:15:00

我引用了 Alvin AlexanderScala Cookbook 第 6 章:对象

这是我在这本书中发现的众多有趣的事情之一。

要为案例类提供多个构造函数,了解案例类声明的实际用途非常重要。

case class Person (var name: String)

如果您查看 Scala 编译器为 case 类示例生成的代码,您会发现它创建了两个输出文件:Person$.class 和 Person.class。如果使用 javap 命令反汇编 Person$.class,您将看到它包含一个 apply 方法以及许多其他方法:

$ javap Person$
Compiled from "Person.scala"
public final class Person$ extends scala.runtime.AbstractFunction1 implements scala.ScalaObject,scala.Serializable{
public static final Person$ MODULE$;
public static {};
public final java.lang.String toString();
public scala.Option unapply(Person);
public Person apply(java.lang.String); // the apply method (returns a Person) public java.lang.Object readResolve();
        public java.lang.Object apply(java.lang.Object);
    }

您还可以反汇编 Person.class 以查看它包含的内容。对于像这样的简单类,它包含额外的 20 个方法;这种隐藏的膨胀是一些开发人员不喜欢案例类的原因之一。

I am quoting this from Scala cookbook by Alvin Alexander chapter 6: objects.

This is one of the many things that I found interesting in this book.

To provide multiple constructors for a case class, it’s important to know what the case class declaration actually does.

case class Person (var name: String)

If you look at the code the Scala compiler generates for the case class example, you’ll see that see it creates two output files, Person$.class and Person.class. If you disassemble Person$.class with the javap command, you’ll see that it contains an apply method, along with many others:

$ javap Person$
Compiled from "Person.scala"
public final class Person$ extends scala.runtime.AbstractFunction1 implements scala.ScalaObject,scala.Serializable{
public static final Person$ MODULE$;
public static {};
public final java.lang.String toString();
public scala.Option unapply(Person);
public Person apply(java.lang.String); // the apply method (returns a Person) public java.lang.Object readResolve();
        public java.lang.Object apply(java.lang.Object);
    }

You can also disassemble Person.class to see what it contains. For a simple class like this, it contains an additional 20 methods; this hidden bloat is one reason some developers don’t like case classes.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文