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?
First the good bits:
Everything immutable by default
Yes, and can even be overridden (using
var
) if you need itGetters automatically defined
Possible in any class by prefixing params with
val
Decent
toString()
implementationYes, very useful, but doable by hand on any class if necessary
Compliant
equals()
andhashCode()
Combined with easy pattern-matching, this is the main reason that people use case classes
Companion object with
unapply()
method for matchingAlso 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 methodIn 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 val
s, 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.
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.
Furthermore, adding a new Prod class does not entail any changes to existing code:
In contrast, add a new method requires modification of all existing classes.
The same problem solved with case classes.
Adding a new method is a local change.
Adding a new Prod class requires potentially change all pattern matching.
Transcript from the videolecture 4.6 Pattern Matching
Remember: we must use this like a starting point and not like the only criteria.
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 anequals
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 oncase class
equality.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.
I am quoting this from
Scala cookbook
byAlvin 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.
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:
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.