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.
We then introduced record structs and record classes, identifying the fact that while the code generation is mostly the same, there are some important differences. Most importantly, both record struct and record class override equality to use value type equality. This provides a significant feature if value equivalence is needed on a type as implementing such is complex and error prone. Other similarities 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.
Furthermore, custom-built structs are relatively rare. They obviously play an important role within C# development, but the number of custom-built structs declared by typical developers is usually tiny compared to the number of custom-built classes. Heavy use of custom-built structs is most common in code intended to interoperate with unmanaged code.
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.