Every way to model data
class, struct, record, record struct, tuple and enum, plus init, required and primary constructors. Which type to declare for the thing you're describing.
Describe the thing out loud, then match the description.
- A fixed set of named options (order status, a card suit) →
an
enum. Stored as a number, so give the numbers yourself if they're saved or sent anywhere. - A few values returned together, used right away (a min and a
max, a parsed name and age) → a tuple,
(int Min, int Max). Once it travels beyond one method or two, give it a real name as a record. - Data that's defined by its values (an address, a money
amount, a DTO) → a
record. Equality compares the values,withmakes a changed copy, andToStringprints the contents. - A small value, a few fields, never changed in place
(a map point, a colour) → a
readonly record struct, or a plainstruct. Under about 16 bytes; otherwise the copying costs more than it saves. - Something with identity and behaviour that changes over its
life (a shopping cart, a bank account, a service) → a
class. Two carts with the same items are still two carts.
Then decide how it gets built: a constructor when values need
checking, init and required for named creation that can't be
forgotten, a primary constructor for a class that uses what it was
given. Whatever you pick, check what == means for it: see "Every
way to compare two values".
Reference types
Section
Classes and objects
A class is a type you design; a variable of that type holds a reference to an object.
Value equality and non-destructive changes for data, and the choice between record class, record struct and plain class.
Formal. source with { P = v, ... } copies every field of
source into a new instance (for a record, through its synthesized
copy constructor) and then assigns the listed members. Fields are
copied as values. A field of reference type therefore copies the
reference, and the original and the copy share the object it refers
to. Nothing is cloned recursively.
In plain English. with photocopies the top sheet only. Numbers
and text on that sheet are copied. A line that says "the shopping
list is in the kitchen" is copied as that sentence, so both copies
send you to the same list in the same kitchen.
Example. For record Cart(string Owner, List<string> Items),
var b = a with { Owner = "Raj" }; b.Items.Add("eggs"); adds eggs to
a.Items as well.
Why this rule exists. A deep copy would have to guess how to copy
every kind of object, and would be slow for large graphs. A shallow
copy is cheap and predictable. When the copies must not share a
collection, copy it yourself: a with { Items = [.. a.Items] }.
with copies each field. A list inside is shared between the original and the copy.
Value types
Section
Structs, and when one fits
Small, immutable, value-like, and the mutated-copy trap when they aren't.
Formal. Assigning, passing, or returning a value-type value (a
number, bool, a struct, an enum, a tuple) copies the data, so the two
variables are independent afterwards. Assigning a reference-type value
(a class instance, an array, a string, a delegate) copies only the
reference; both variables then refer to the same object, and a change
made through one is visible through the other. string is a reference
type but immutable, so this sharing is never observable for text.
In plain English. A value type is like handing someone a photocopy: they can scribble on it and your page is untouched. A reference type is like handing someone the address of a house: you both now visit the same house, so if they repaint it, you see the new paint.
Example. int a = 1; int b = a; b = 2; leaves a as 1. With
var p = new Player(); var q = p; q.Score = 10;, p.Score is also 10.
Why this rule exists. Small values are cheap to copy and safer when copied; large or shared things (a document, a bank account) need one identity that everyone refers to.
Assigning a struct copies the data; assigning a class copies the reference.
Section
Enums
Named constants instead of magic numbers, [Flags] for combinations, and parsing them safely.
Section
Tuples and deconstruction
Several values without declaring a type, and pulling them apart again with var (a, b) = ...
How it gets built
Check values on the way in, or set them by name with { ... } after new.
The table that picks between a constructor, init, required, a primary constructor and a positional record.
Shaping data for other code
Section
DTOs and mapping
A separate record for what crosses the wire, so the API's shape and the domain's can change apart.