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.

Create a free account to save this page, take notes and track your progress. Sign in

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, required forces 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, and with expressions 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 struct combines both ideas.
  • Access: public, private, protected, internal (the default for types: visible in the assembly). Keep members private unless 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

A class, a record and a struct
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.00

Run 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 override Equals; records compare values.
  • Using inheritance for data variation. Prefer records with a discriminator or small interfaces.
  • Forgetting required or initialization for non-nullable properties; the compiler warns about it.

Key points

  • Classes are reference types with properties, constructors and behavior; init and required make creation safe.
  • Records provide value equality, ToString, deconstruction and with copies for immutable data.
  • Structs are value types for small, immutable values; readonly record struct is 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.

Was this page helpful?