DEV Community

Discussion on: Answer: When to use record vs class vs struct

Collapse
 
jayjeckel profile image
Jay Jeckel

If you're writing a struct, you should not be relying on the default ValueType.Equals, because as you mention the unneeded reflection. You should always implement IEquatable<T> and override the Equals(object) method as well as the equality operators for every custom struct you implement.

I have three rules for deciding if something should be a struct:

  1. Structs should always be readonly. No mutable structs, ever.
  2. Structs should be composed of only primitive types, other structs, string references, and, if absolutely necessary, immutable arrays of the same.
  3. Most importantly, a custom struct type must represent a logical value. For example, a phone number, a semantic version, an address, something that represents a single piece of information composed of a strict set of data. If a simple string can represent the information, "1919-12-01", "02:12:55.010", "(000) 123-1234", "1.0.4-pre.3", etc, then there is a good chance it could be a proper logical value.

If it meets those three rules, then I consider it a candidate for being a struct. If it doesn't meet all three, then it should be a record or other type of class.

Not really one of my rules, but if inheritance is a concern, then, after double and triple checking the design reason for needing inheritance instead of using interfaces, I know for sure that it definitely shouldn't be a struct because it doesn't represent a value.

I don't worry about memory size of the struct. If a struct properly represents a logical value, then worrying about its memory size before you've profiled actual memory usage is a prime example of premature optimization.

Besides the normal logical value struct, there is also the wrapper struct. That is a struct that wraps an object (be it value type or reference type) for the sole purpose of doing work on that object. The most recent example that pops to mind was in a proposal I was reading the other day and involved using a ref readonly struct to wrap an array reference just to have a type that extension methods could be hung on to operate on the wrapped referenced array.

I'm sure many have done the same kind of thing with classes, but using a struct as the wrapper removes the overhead of instancing a class that would need to be garbage collected. In the case of the mentioned proposal, it helped with zero-allocation string creation, a place where when allocation count matters it really matters.

In conclusion, of course one should always consider the pros and cons of every design decision, but I strongly believe that the structs of modern languages carry a lot of cargo cult baggage from the bygone days of needing to count the bytes of your field types and are much more useful than a lot of programmers allow.