This chapter began with a discussion comparing reference equality with value equality and how, by default, reference types use reference equality when checking for equality, where value types check the values themselves or, for custom value types, the data within the custom value type. Recall, however, that custom-built value types, structs, are relatively rare compared to the number of custom-built classes and are generally limited to code intended to interoperate with unmanaged code.
We then introduced record structs and record classes, identifying that while the code generation is mostly the same, there are some important differences. Most importantly, both record structs and record classes override equality to use value type equality. This provides a significant feature if value equivalence is needed on a type because implementing such is complex and error prone. Another similarity is that they both use the positional parameters to generate properties, a constructor, ToString() implementations, and a deconstructor. Where they differ, however, is that by default record structs are read/write, while record classes use init-only setters by default for all positional parameters. That said, because it is easy to write confusing or buggy code when mutating value types and because value types are typically used to model immutable values, the best practice is to declare record structs with the readonly modifier. Also, only record classes support inheritance—although only from other record classes. In fact, all structs (record or standard) don’t support inheritance.
Value types are boxed when they must be treated polymorphically as reference types. The idiosyncrasies introduced by boxing are subtle, and the vast majority of them lead to problematic issues at execution time rather than at compile time. Although it is important to know about these quirks to try to avoid them, in many ways paying too much attention to the potential pitfalls overshadows the usefulness and performance advantages of value types. Programmers should not be overly concerned about using value types. Value types permeate virtually every chapter of this book, yet the idiosyncrasies associated with them come into play infrequently. We have staged the code surrounding each issue to demonstrate the concern, but in reality, these types of patterns rarely occur. The key to avoiding most of them is to follow the guideline of not creating mutable value types—and following this constraint explains why you don’t encounter them within the built-in value types.
Perhaps the only issue to occur with some frequency is repetitive boxing operations within loops. However, generics greatly reduce boxing, and even without them, performance is rarely affected enough to warrant their avoidance until a particular algorithm with boxing is identified as a bottleneck.
This chapter also introduced enums. Enumerated types are a standard construct available in many programming languages. They help improve both API usability and code readability.
Chapter 10 presents more guidelines for creating well-formed types—both value types and reference types. It begins by defining operator-overloading methods. These two topics apply to both structs and classes, but they are somewhat more important when completing a struct definition and making it well formed.