Structs, Records and Enums
A struct is a value type: assignment copies all its fields, it has no inheritance (except interfaces), and default(T) zeroes all fields.
Concept
A struct is a value type: assignment copies all its fields, it has no inheritance (except interfaces), and default(T) zeroes all fields.
Use for small (≤ ~16 bytes), immutable, value-like data: coordinates, colors, time spans, CAN IDs.
readonly struct guarantees immutability and lets the compiler avoid defensive copies.
ref struct (like Span<T>) can only live on the stack — it cannot be boxed or stored in fields of classes (Module 41).
Structs can have constructors, methods and properties.
A record is a type designed for data, with compiler-generated value-based equality, ToString, deconstruction and with support.
record Vehicle(string Model, int Year); Positional record class (reference type, immutable properties) record struct Point(int X, int Y);
Mutable positional record struct (value type) readonly record struct GpsPoint(double Lat, double Lon); Immutable record struct
Two records are equal when all their members are equal (a == b is true for different objects with the same values).
with creates a modified copy: a with { Year = 2026 } (non-destructive mutation).
Member-wise equality compares references for reference-type members such as arrays — two identical byte[] payloads are not "equal".
An enum is a named set of constants over an integral underlying type (int by default; byte in enum Gear : byte).
Conversion: (int)Gear.Drive, (Gear)3. Any value of the underlying type can be cast, even undefined ones ((Gear)42).
Enum.Parse<T>, Enum.TryParse<T>, Enum.GetValues<T>(), Enum.IsDefined(...).
[Flags] enums use powers of two so values can be combined with |, tested with & or HasFlag, and printed as Read, Write.
Why It Matters
Records remove boilerplate from DTOs, messages and events and make equality correct by default. Structs avoid heap allocations for small values. Enums make code self-documenting and switch expressions exhaustive.
Syntax
public readonly struct S { public S(int x) { X = x; } public int X { get; } }
public record R(string A, int B);
public readonly record struct RS(double X, double Y);
public enum E : byte { A, B, C }
[Flags] public enum F { None = 0, X = 1, Y = 2, Z = 4 }Example
// ---- Structs: value semantics ----
var p1 = new GpsPoint(12.97, 77.59);
var p2 = p1; // copy
p2 = p2 with { Lat = 0 }; // 'with' works on structs too (C# 10)
Console.WriteLine($"p1 = {p1}, p2 = {p2}");
Console.WriteLine($"default(GpsPoint) = {default(GpsPoint)}");
// ---- Records: value-based equality, with-expressions, deconstruction ----
var a = new Vehicle("Aurora EV", 2025);
var b = new Vehicle("Aurora EV", 2025);
var c = a with { Year = 2026 }; // non-destructive mutation
Console.WriteLine($"a = {a}");
Console.WriteLine($"a == b: {a == b}, ReferenceEquals: {ReferenceEquals(a, b)}, a == c: {a == c}");
var (model, year) = c;
Console.WriteLine($"deconstructed: {model}, {year}");
var frame = new CanFrame(0x100, [1, 2, 3]);
Console.WriteLine($"record struct: {frame}");
Console.WriteLine($"same array -> equal: {frame == new CanFrame(0x100, frame.Data)}, identical new array -> equal: {frame == new CanFrame(0x100, [1, 2, 3])}");
// ---- Enums ----
Gear g = Gear.Drive;
Console.WriteLine($"{g} = {(int)g}, underlying type {Enum.GetUnderlyingType(typeof(Gear)).Name}, defined 7? {Enum.IsDefined((Gear)7)}");
Console.WriteLine($"Enum.Parse(\"Reverse\")
…Expected Result
p1 = GpsPoint { Lat = 12.97, Lon = 77.59 }, p2 = GpsPoint { Lat = 0, Lon = 77.59 }
default(GpsPoint) = GpsPoint { Lat = 0, Lon = 0 }
a = Vehicle { Model = Aurora EV, Year = 2025 }
a == b: True, ReferenceEquals: False, a == c: False
deconstructed: Aurora EV, 2026
record struct: CanFrame { Id = 256, Data = System.Byte[] }
same array -> equal: True, identical new array -> equal: False
Drive = 3, underlying type Byte, defined 7? False
Enum.Parse("Reverse") = Reverse, TryParse("Fly") = False
all gears: Park, Reverse, Neutral, Drive, Sport
(Gear)42 prints as 42
perms = Read, Write (3), can write: True, can execute: False
after changes: Read, ExecuteCommon Mistakes
Enum.IsDefined(Type, object) requires the value's type to match the underlying type exactly — with a byte enum and an int value it throws. Real output:
Large or mutable structs (copies are expensive and changes to copies are lost).
Assuming record equality compares array contents.
Casting integers to enums without validation ((Gear)42 is legal).
[Flags] values that are not powers of two, or missing None = 0.
Using records for entities with identity (e.g. a database row that changes over time) where reference identity matters.
try
{
Console.WriteLine(Enum.IsDefined(typeof(Gear), 7)); // 7 is an int, Gear is byte-based
}
catch (ArgumentException ex)
{
Console.WriteLine($"ArgumentException: {ex.Message}");
}
Console.WriteLine($"Enum.IsDefined((Gear)7) = {Enum.IsDefined((Gear)7)}"); // generic overload: safe
enum Gear : byte { Park, Reverse, Neutral, Drive, Sport }ArgumentException: Enum underlying type and the object must be same type or object must be a String. Type passed in was 'System.Int32'; the enum underlying type was 'System.Byte'.
Enum.IsDefined((Gear)7) = FalseBest Practices
Use record for immutable data carriers (DTOs, events, messages, configuration).
Use readonly record struct for small value objects (coordinates, units).
Keep structs small and immutable.
Validate enum values from outside sources with the generic Enum.IsDefined<T>.
Name [Flags] enums in plural (Permissions), others in singular (Gear).
Interview Relevance
Very high: "struct vs class", "what is a record and how is equality different?", "what does with do?", "record class vs record struct", "Flags enums".
Practice Questions
Create a record Dtc(string Code, string Description, Severity Severity) and compare two instances.
Write a readonly struct Rgb with a method Blend.
Define a [Flags] enum Channels { None = 0, Can1 = 1, Can2 = 2, Lin = 4, Eth = 8 } and print the active channels.
Show that changing a struct inside a List<T> via list[0].X = 5 does not compile (why?).
Override equality in a record so that two CanFrames compare their payload bytes.
Quick Quiz
Q1. (Predict the output) var a = new Vehicle("Aurora EV", 2025); var b = new Vehicle("Aurora EV", 2025); Console.WriteLine(a == b); for record Vehicle(string Model, int Year).
Answer: True — records compare values (verified).
Q2. (Multiple choice) What does a with { Year = 2026 } do? A) Changes a B) Returns a modified copy C) Compares a with 2026 D) Does not compile Answer: B.
Q3. (Predict the output) Console.WriteLine((Gear)42); for enum Gear : byte { Park, Reverse, Neutral, Drive, Sport }.
Answer: 42 — undefined values print as numbers (verified).
Q4. (True/False) Structs can inherit from other structs.
Answer: False. Structs can only implement interfaces.
Coding Challenge
Create a readonly record struct CanId(uint Value, bool Extended) with a static Parse(string) that accepts "0x7E0" (standard) and "0x18FEF100x" (trailing x = extended), and validate that standard IDs are ≤ 0x7FF and extended IDs ≤ 0x1FFFFFFF.
using System.Globalization;
Console.WriteLine(CanId.Parse("0x7E0"));
Console.WriteLine(CanId.Parse("0x18FEF100x"));
try { CanId.Parse("0x800"); } catch (FormatException ex) { Console.WriteLine(ex.Message); }
readonly record struct CanId(uint Value, bool Extended)
{
public static CanId Parse(string text)
{
bool ext = text.EndsWith('x');
string hex = text[2..(ext ? ^1 : ^0)];
uint value = uint.Parse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture);
if (value > (ext ? 0x1FFFFFFFu : 0x7FFu)) throw new FormatException($"{text} out of range");
return new CanId(value, ext);
}
public override string ToString() => Extended ? $"0x{Value:X8} (29-bit)" : $"0x{Value:X3} (11-bit)";
}