C# Fundamentals: From Your First Program to Async and JSON · C# Basics: Types, Collections, Classes and Pattern Matching
Classes, Records and Structs
Model data with classes, properties and constructors, use records for immutable values with value equality, and know when a struct is the right choice.
What you'll build
A small library model: a mutable Book class with validated properties, an immutable Member record that is compared by value and copied with with, and a Point struct.
The idea
- Classes are reference types. They have fields, properties (
public string Title { get; set; }), constructors and methods. Properties are the public interface to data:{ get; init; }allows setting only at creation,requiredforces callers to set it. - Primary constructors (C# 12) put constructor parameters on the type:
class Greeter(string name). The parameters are available throughout the class. - Records (
record Member(string Name, int Id);) are reference types designed for immutable data. The compiler generates value equality (two records with the same values are equal),ToString, deconstruction, andwithexpressions to create modified copies. - Structs are value types: copied on assignment and stored inline. Use small, immutable structs (
readonly struct) for tiny values like points or money; use classes or records otherwise.record structcombines both ideas. - Access:
public,private,protected,internal(the default for types: visible in the assembly). Keep membersprivateunless callers need them. - Object initializers:
new Book { Title = "..." }.
Rule of thumb: records for data, classes for behavior and identity, structs for tiny values.
The code
var book = new Book { Title = "Dune", Author = "Frank Herbert", Pages = 412 };
book.Borrow("Ada");
Console.WriteLine(book);
Console.WriteLine($"available? {book.IsAvailable}, borrower: {book.Borrower}");
try { book.Pages = -5; } catch (ArgumentOutOfRangeException e) { Console.WriteLine($"rejected: {e.ParamName}"); }
// Records: value equality, ToString and non-destructive mutation with `with`
var a = new Member("Ada", 1, "[email protected]");
var b = new Member("Ada", 1, "[email protected]");
var renamed = a with { Email = "[email protected]" };
Console.WriteLine(a);
Console.WriteLine($"a == b: {a == b}, same object: {ReferenceEquals(a, b)}");
Console.WriteLine($"a == renamed: {a == renamed}");
var (name, id, _) = a; // deconstruction
Console.WriteLine($"{name} has id {id}");
// Struct: copied on assignment
var p1 = new Point(1, 2);
var p2 = p1; // a copy
p2 = p2 with { X = 10 };
Console.WriteLine($"p1 {p1}, p2 {p2}, distance {p1.DistanceTo(p2):F2}");
class Book
{
private int _pages;
public required string Title { get; init; } // must be set when creating, never changed
public required string Author { get; init; }
public string? Borrower { get; private set; } // only the class can change it
public bool IsAvailable => Borrower is null; // computed property
public int Pages
{
get => _pages;
set => _pages = value > 0 ? value : throw new ArgumentOutOfRangeException(nameof(value), "pages must be positive");
}
public void Borrow(string member)
{
if (!IsAvailable) throw new InvalidOperationException("already borrowed");
Borrower = member;
}
public override string ToString() => $"{Title} by {Author} ({Pages} pages)";
}
record Member(string Name, int Id, string Email);
readonly record struct Point(double X, double Y)
{
public double DistanceTo(Point other) => Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2));
}
Output
Dune by Frank Herbert (412 pages)
available? False, borrower: Ada
rejected: value
Member { Name = Ada, Id = 1, Email = [email protected] }
a == b: True, same object: False
a == renamed: False
Ada has id 1
p1 Point { X = 1, Y = 2 }, p2 Point { X = 10, Y = 2 }, distance 9.00Run this on your computer — see “Get Started” for setup.
Common mistakes
- Public fields instead of properties. Properties let you add validation later without breaking callers.
- Mutable structs. Copies are easy to modify by accident; make structs
readonly. - Comparing classes with
==expecting value equality. Classes compare references unless you overrideEquals; records compare values. - Using inheritance for data variation. Prefer records with a discriminator or small interfaces.
- Forgetting
requiredor initialization for non-nullable properties; the compiler warns about it.
Key points
- Classes are reference types with properties, constructors and behavior;
initandrequiredmake creation safe. - Records provide value equality,
ToString, deconstruction andwithcopies for immutable data. - Structs are value types for small, immutable values;
readonly record structis a good default. - Keep members private by default and expose a small, validated surface through properties.
Further reading: Records.
Practice questions
How does equality differ between a class and a record?
Answer
Two class instances are equal only if they are the same object (reference equality) unless Equals is overridden. Records compare their property values, so two records with the same data are equal.
What does `{ get; init; }` give you compared with `{ get; set; }`?
Answer
An init accessor lets the property be set only while the object is being created (in a constructor or object initializer) and makes it read-only afterwards, which supports immutability.
Open in the app to mark lessons complete, take notes and track your progress.