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 types | Value types | |
|---|---|---|
| Examples | class, string, arrays, List<T>, record | int, double, bool, DateTime, struct, record struct |
| Assignment | Copies the reference; both variables share one object | Copies the data; each variable has its own |
| Can be null | Yes | No, 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
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 Moneywith{ get; }orinitproperties prevents confusing copy-and-change bugs.
Records
A record declares a data type in one line:
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:
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 changeGetting 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.
Struct copies versus class references
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; }struct: 1 50
class: 50 50Line-by-line explanation
- Line 2Assigning a struct copies all of its data.
- Line 3Changing the copy leaves s1 alone.
- Lines 6 to 7Both class variables refer to one object, so the change shows through c1 too.
- Lines 12 to 13The only difference between the two types is the keyword.
Records with value equality and with
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);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: ShippedLine-by-line explanation
- Line 3with copies the order and changes only Status; order itself is unchanged.
- Line 5Records print their property names and values.
- Line 7== compares values, so two separately created records with the same data are equal.
- Line 9They are still two different objects, which ReferenceEquals shows.
- Line 11Positional records support deconstruction.
- Line 14One line declares the whole type.
A readonly record struct for money
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}";
}16.49 GBP
True
Range: 12.50 to 16.49Line-by-line explanation
- Line 3The + operator is defined on the type, so money adds naturally.
- Line 6Record structs also compare by value.
- Line 8A tuple groups two values without declaring a type.
- Line 11readonly record struct is a small, immutable value type.
- Line 15Adding different currencies is refused rather than giving a wrong answer.
Common C# Types Mistakes
Changing a struct inside a list
var points = new List<PointStruct> { new() { X = 1 } };
points[0].X = 5; // error CS1612: cannot modify the return value, because it's a copyvar p = points[0];
p.X = 5;
points[0] = p; // or make the struct immutable and replace itExpecting classes to compare by value
class Tag { public string Name = ""; }
new Tag { Name = "sale" } == new Tag { Name = "sale" } // False: different objectsrecord Tag(string Name);
new Tag("sale") == new Tag("sale") // TrueTrying to change a record property
var p = new Product("Mug", 8.50m);
p.Price = 6.00m; // error CS8852: init-only propertyvar 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.
Complete the line to make a copy of the record with a new price.
var sale = item { Price = 5m };
Which of these is a value type?
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);
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; }
What does the compiler generate for record Point(int X, int Y);?
C# Types Quiz
5 questions to check what you learned.
Is a record a class or a struct by default?
- A class (reference type)
- A struct
- Neither
- It depends on the properties
Can a struct inherit from another struct?
- Yes
- Only readonly structs
- No, but it can implement interfaces
- Only record structs
Does a with expression change the original record?
- No, it returns a new copy
- Yes
- Only for record structs
- Only if properties are set
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
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