Types
4 min readTypes
TL;DR
Choosing the right kind of type affects memory layout, equality semantics, mutability, and API contracts. Use these rules of thumb when designing models and DTOs:
How it works
Type Choices β class, record, struct, static (where & why)
class(reference type):- Use for objects with identity, lifecycle, or potentially large mutable state (e.g., domain entities, services).
- Instances are allocated on the heap; assignments copy references, not state.
- Good when you want shared mutable state or polymorphism (virtual methods, inheritance).
- Example:
public class Order { public int Id; public decimal Amount; }
struct(value type):- Use for small, immutable, copy-by-value types that represent a single value (e.g.,
Price,Coordinate). - Avoid large structs (copying cost) or mutable structs (surprising semantics when boxed or assigned).
- Prefer
readonly structfor immutable value semantics. - Example:
public readonly struct Price { public decimal Amount { get; } }
record(reference-record, C# 9+):- Use when you want concise immutable data carriers with value-based equality and non-destructive mutation (
withexpressions). - Ideal for DTOs, messages, and snapshots where equality by content is helpful.
- Records are reference types by default (there is also
record structfor value semantics). - Example:
public record OrderDto(int Id, string Symbol, decimal Amount);
record struct(C# 10+):- Combines record conveniences (auto equality,
with) with value-type semantics β useful for small immutable value objects where structural equality is desired.
staticclasses / members:- Use
staticclasses for stateless helpers, extension method containers, and singletons without state. staticmembers belong to the type rather than instances; no instantiation possible.- Be cautious:
staticmutable state is effectively global and introduces thread-safety concerns. - Example:
public static class Email { public static void Send(...) { ... } }
Practical guidance & trade-offs
- Prefer
recordfor DTOs and immutable data transfer where content equality is useful. - Prefer
classfor domain entities that have an identity (database ID) and lifecycle; they usually need to be mutable or proxied by ORMs. - Use
struct/record structfor very small value objects (e.g.,PriceTick,Timestamp) to avoid heap allocation when copying is cheap. - Favour immutability for simple data carriers β it reduces shared-state bugs and simplifies reasoning, especially across threads.
- Avoid mutable singletons or
staticmutable fields; prefer injected, testable services with clear lifetimes.
Quick examples
// record for DTO
public record OrderDto(int Id, string Symbol, decimal Amount);
// class for entity
public class Order
{
public int Id { get; set; }
public decimal Amount { get; set; }
}
// small readonly struct for value
public readonly struct PriceTick
{
public decimal Bid { get; }
public decimal Ask { get; }
public PriceTick(decimal bid, decimal ask) => (Bid, Ask) = (bid, ask);
}
// static helper
public static class MathHelpers { public static decimal RoundPrice(decimal p) => Math.Round(p, 5); }
If you'd like, I can link this file from core-concepts.md and/or expand it with interview bullet points for quick review.
Quick recall Q&A
record vs class?Use records when you want concise immutable data carriers with value-based equality (DTOs, events). Use classes when identity/lifecycle matters or when you need mutable state and inheritance.
record struct?It combines value semantics (no heap allocation) with generated equality/with expressions. Ideal for small immutable value objects where structural equality is desired.
Copying them is expensive and negates GC benefits. Keep structs small (β€16 bytes) or pass by in/ref to avoid copies.
static classes?For stateless helpers or extension method containers. Avoid static mutable state because itβs effectively global and complicates testing/threading.
record equality semantics work?Records implement value-based equality by comparing declared properties/fields. Classes use reference equality unless you override Equals/GetHashCode.
Yes, but it defeats their primary benefit. Prefer init-only setters or positional parameters to keep them immutable and safe across threads.
record inheritance make sense?When modeling hierarchies of immutable data (e.g., different message types). Remember records default to value equality, so ensure derived records add properties carefully.
If you need identity semantics, lazy-loaded navigation properties (e.g., EF proxies), or mutable behavior beyond data transfer.
struct and record struct?If you need value semantics plus generated equality/with, use record struct. If you want full control over equality/toString, a regular struct might suffice.
Many serializers support classes/records out of the box. Structs serialize fine but be mindful of default values and boxing when using polymorphic serialization.