C# Structs and Records

Understand value types and reference types, create small value types with struct, model immutable data with records, compare by value, copy with changes using with, and choose between classes, structs, records and tuples.

Lesson 15 of 43Intermediate18 min read

So far every type you've created has been a class, a reference type: variables hold a reference, two variables can point to the same object, and == asks "is this the same object?". That's exactly right for things with an identity, like a customer or a bank account. But much of the data in a program is just values: a point on a map, a colour, an amount of money, a date range. Two amounts of £5 are simply equal; it doesn't matter whether they're "the same object".

C# gives you two tools for value-like data. A struct is a value type: assigning it copies all of its data, like an int or a DateTime. A record is a type designed for data: it compares by value, prints its contents nicely, and supports copying with changes through the with expression, all generated by the compiler from a single line.

In this lesson you'll see the difference between value and reference semantics, write structs and records, use with to create modified copies of immutable data, and learn a simple guide for choosing between classes, structs, records and tuples.

Value types and reference types

Reference typesValue types
Examplesclass, string, arrays, List<T>, recordint, double, bool, DateTime, struct, record struct
AssignmentCopies the reference; both variables share one objectCopies the data; each variable has its own
Can be nullYesNo, unless written as int?
Default ==Same object? (records compare values)Compares values (for built-in types)

Value types are usually small and short-lived, and avoiding separate objects can make performance-sensitive code faster.

Structs

C#
public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
    public Point(int x, int y) { X = x; Y = y; }
    public double DistanceFromOrigin => Math.Sqrt(X * X + Y * Y);
}

var a = new Point(3, 4);
var b = a;          // b is a COPY
b.X = 100;          // a.X is still 3
  • Structs can have fields, properties, methods and constructors, but can't inherit from other structs or classes (they can implement interfaces).
  • Keep structs small (a few fields) and preferably immutable: readonly struct Money with { get; } or init properties prevents confusing copy-and-change bugs.

Records

A record declares a data type in one line:

C#
public record Product(string Name, decimal Price);

var p1 = new Product("Mug", 8.50m);
var p2 = new Product("Mug", 8.50m);
Console.WriteLine(p1 == p2);       // True: same values
Console.WriteLine(p1);             // Product { Name = Mug, Price = 8.50 }
var sale = p1 with { Price = 6.00m };

From that one line, the compiler generates a constructor, init properties, value equality (== and Equals compare every property), a readable ToString, and deconstruction (var (name, price) = p1;).

The with expression creates a copy with some properties changed, leaving the original untouched. This is called non-destructive mutation, and it's the natural way to "change" immutable data. record struct gives you the same features on a value type.

Default values and boxing

A struct always has a value, even if you never call a constructor. default(Point) and every item in a new Point[] array have all fields set to their defaults, so X and Y are 0. Design structs so that this all-zero state still makes sense.

When a value type is stored in a variable of type object, or of an interface type, C# copies it into a new object on the heap. This is called boxing, and the box is a separate copy:

C#
Point p = new Point(1, 2);
object boxed = p;              // boxing makes a copy
p.X = 99;
Console.WriteLine(((Point)boxed).X);   // 1: the box didn't change

Getting the value back out with a cast is called unboxing. Boxing is rare in modern code thanks to generics, which is one reason List<int> replaced the old non-generic ArrayList. When it does happen in a tight loop, it costs extra memory and time, and the copy can cause confusing bugs like the one above.

C# Types Syntax

Structs, records and tuples

public readonly struct Money(decimal amount, string currency)
{
    public decimal Amount { get; } = amount;
    public string Currency { get; } = currency;
}
public record User(string Name, string Email);
public record struct Coordinate(double Lat, double Lng);
var u2 = u1 with { Email = "new@example.com" };
var (name, email) = u1;
bool same = u1 == u2;
(int Min, int Max) range = (1, 10);

C# Types Summary

  • Classes and records are reference types; structs and record structs are value types.
  • Assigning a struct copies its data; assigning a class copies the reference.
  • Records generate constructors, value equality, ToString and deconstruction.
  • with creates a modified copy of a record, leaving the original unchanged.
  • Choose classes for identity and behaviour, records for data, small structs for tiny values.

C# Types Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Struct copies versus class references

C#
var s1 = new PointStruct { X = 1 };
var s2 = s1;
s2.X = 50;

var c1 = new PointClass { X = 1 };
var c2 = c1;
c2.X = 50;

Console.WriteLine($"struct: {s1.X} {s2.X}");
Console.WriteLine($"class:  {c1.X} {c2.X}");

struct PointStruct { public int X; }
class PointClass { public int X; }
Output
struct: 1 50
class:  50 50
Line-by-line explanation
  1. Line 2Assigning a struct copies all of its data.
  2. Line 3Changing the copy leaves s1 alone.
  3. Lines 6 to 7Both class variables refer to one object, so the change shows through c1 too.
  4. Lines 12 to 13The only difference between the two types is the keyword.
Example 2

Records with value equality and with

C#
var order = new Order(1042, "Ada", 59.90m, "Pending");
var same = new Order(1042, "Ada", 59.90m, "Pending");
var shipped = order with { Status = "Shipped" };

Console.WriteLine(order);
Console.WriteLine(shipped);
Console.WriteLine(order == same);
Console.WriteLine(order == shipped);
Console.WriteLine(ReferenceEquals(order, same));

var (id, customer, total, status) = shipped;
Console.WriteLine($"#{id} for {customer}: {status}");

record Order(int Id, string Customer, decimal Total, string Status);
Output
Order { Id = 1042, Customer = Ada, Total = 59.90, Status = Pending }
Order { Id = 1042, Customer = Ada, Total = 59.90, Status = Shipped }
True
False
False
#1042 for Ada: Shipped
Line-by-line explanation
  1. Line 3with copies the order and changes only Status; order itself is unchanged.
  2. Line 5Records print their property names and values.
  3. Line 7== compares values, so two separately created records with the same data are equal.
  4. Line 9They are still two different objects, which ReferenceEquals shows.
  5. Line 11Positional records support deconstruction.
  6. Line 14One line declares the whole type.
Example 3

A readonly record struct for money

C#
var price = new Money(12.50m, "GBP");
var shipping = new Money(3.99m, "GBP");
var total = price + shipping;

Console.WriteLine(total);
Console.WriteLine(total == new Money(16.49m, "GBP"));

var (min, max) = (price.Amount, total.Amount);
Console.WriteLine($"Range: {min} to {max}");

readonly record struct Money(decimal Amount, string Currency)
{
    public static Money operator +(Money a, Money b)
    {
        if (a.Currency != b.Currency) throw new InvalidOperationException("Currencies differ");
        return new Money(a.Amount + b.Amount, a.Currency);
    }

    public override string ToString() => $"{Amount:F2} {Currency}";
}
Output
16.49 GBP
True
Range: 12.50 to 16.49
Line-by-line explanation
  1. Line 3The + operator is defined on the type, so money adds naturally.
  2. Line 6Record structs also compare by value.
  3. Line 8A tuple groups two values without declaring a type.
  4. Line 11readonly record struct is a small, immutable value type.
  5. Line 15Adding different currencies is refused rather than giving a wrong answer.

Common C# Types Mistakes

Changing a struct inside a list

Don't
var points = new List<PointStruct> { new() { X = 1 } };
points[0].X = 5;      // error CS1612: cannot modify the return value, because it's a copy
Do
var p = points[0];
p.X = 5;
points[0] = p;        // or make the struct immutable and replace it

Expecting classes to compare by value

Don't
class Tag { public string Name = ""; }
new Tag { Name = "sale" } == new Tag { Name = "sale" }   // False: different objects
Do
record Tag(string Name);
new Tag("sale") == new Tag("sale")                       // True

Trying to change a record property

Don't
var p = new Product("Mug", 8.50m);
p.Price = 6.00m;      // error CS8852: init-only property
Do
var cheaper = p with { Price = 6.00m };

C# Types in Real Projects

Records and structs in practice

Modern ASP.NET Core APIs often define request and response types as records, like record CreateOrderRequest(int ProductId, int Quantity). Event-driven systems model events as records because they never change once they happen. .NET's own DateTime, TimeSpan, Guid and decimal are structs, and game code in Unity uses structs like Vector3 for positions so millions of them can be processed quickly.

C# Types FAQ

When should I use a struct instead of a class in C#?

Use a struct for small, simple values that are treated as a whole, such as a coordinate, a colour or an amount of money, especially when you create many of them. Use a class for larger objects, things with identity that change over time, or anything that needs inheritance. When in doubt, a class or a record is the safer default.

What is the difference between a record and a class in C#?

A record is a class with extras generated for you: value-based equality, a readable ToString, deconstruction for positional records and support for with expressions. A regular class compares by reference unless you write that code yourself. Use records for data that's compared by content, and classes for objects with identity and behaviour.

Are C# records immutable?

Positional records, such as record Product(string Name, decimal Price), have init-only properties, so they can't be changed after creation. You can still declare mutable properties with set in a record's body, but that's unusual. Note that immutability is shallow: a record holding a List<T> can still have items added to that list.

What does the with keyword do in C#?

with creates a copy of a record or struct with some properties changed, such as var sale = product with { Price = 6.00m };. The original stays exactly as it was. It's the standard way to "modify" immutable data.

What is the difference between record and record struct?

record, also written record class, is a reference type, while record struct is a value type that's copied on assignment. Both get value equality, ToString and with support. A positional record struct has settable properties unless you declare it readonly record struct.

C# Types Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the line to make a copy of the record with a new price.

var sale = item  { Price = 5m };
Multiple choiceeasy

Which of these is a value type?

Predict the outputmedium

What does this print?

var a = new P(1, 2);
var b = new P(1, 2);
Console.WriteLine(a == b);
record P(int X, int Y);
Predict the outputmedium

What does this print?

var s = new S { V = 1 };
var t = s;
t.V = 9;
Console.WriteLine(s.V);
struct S { public int V; }
Multiple choicehard

What does the compiler generate for record Point(int X, int Y);?

C# Types Quiz

5 questions to check what you learned.

  1. Is a record a class or a struct by default?

    • A class (reference type)
    • A struct
    • Neither
    • It depends on the properties
  2. Can a struct inherit from another struct?

    • Yes
    • Only readonly structs
    • No, but it can implement interfaces
    • Only record structs
  3. Does a with expression change the original record?

    • No, it returns a new copy
    • Yes
    • Only for record structs
    • Only if properties are set
  4. Which is the best fit for an API response containing a product's name and price?

    • A static class
    • An enum
    • A record
    • An interface
  5. Why should structs usually be small and immutable?

    • The compiler requires it
    • They're copied on every assignment, and mutable copies cause confusing bugs
    • Large structs can't be created
    • Immutable structs can be null