Throughout this module, everything you've modeled — LibraryItem, Book, Magazine, Member, Loan — are classes: reference types, designed to represent entities with their own identity and behavior (recall Lend(), Return(), Describe()). But not all of BiblioTech's data fits well into that mold: a member's postal address, or a loan's date range, are small pieces of data with no relevant behavior of their own, where what matters is the value they hold, not the identity of the object representing them. This last lesson of the module closes the loop on object orientation by presenting structs (value types) and records (a modern, concise way of modeling immutable data), and will help you decide when each one is more appropriate than a traditional class.

Contents

  1. Recap: value types versus reference types
  2. struct: defining a value type
  3. When to use struct: small, immutable data
  4. record: value-based equality and readable printing for classes
  5. record struct: the best of both worlds
  6. Full comparison: class, struct, record, record struct
  7. Applying the right choice in BiblioTech

  1. Recap: value types versus reference types

In the first lesson of the module you saw that classes are reference types: copying a variable of type Book doesn't duplicate the object, it creates a second variable that points to the same object in memory.

Book original = new Book("Hopscotch", "Julio Cortazar", "978-84-376-0495-4");
Book copy = original;
copy.Title = "Another title";
Console.WriteLine(original.Title); // "Another title" -- the original changed too

A value type, on the other hand, behaves the opposite way: copying a variable copies all its data, creating a second, completely independent instance. Modifying the copy never affects the original. The primitive types you already know (int, double, bool, char) are value types; in this lesson you'll learn to create your own value types with struct.

Reference type (class) Value type (struct)
When copying a variable The reference is copied; both variables point to the same object The data is copied; they are two independent instances
Modifying the copy affects the original Yes No
Examples already seen Book, Member, Loan int, double, bool, DateTime

  1. struct: defining a value type

A struct is defined very similarly to a class, replacing the class keyword with struct:

struct Address
{
    public string Street;
    public string City;
    public string PostalCode;

    public Address(string street, string city, string postalCode)
    {
        Street = street;
        City = city;
        PostalCode = postalCode;
    }

    public override string ToString()
    {
        return $"{Street}, {City} ({PostalCode})";
    }
}

And it's used exactly like a class, in appearance:

Address memberAddress = new Address("10 Main Street", "Madrid", "28013");
Console.WriteLine(memberAddress); // 10 Main Street, Madrid (28013)

But its behavior when copied is that of a value type:

Address originalAddress = new Address("10 Main Street", "Madrid", "28013");
Address copiedAddress = originalAddress; // ALL the data is copied, not a reference

copiedAddress.City = "Barcelona";

Console.WriteLine(originalAddress.City); // "Madrid" -- the original did NOT change
Console.WriteLine(copiedAddress.City);  // "Barcelona"

Unlike what happened with Book (a class) at the start of this lesson, modifying copiedAddress doesn't affect originalAddress at all: they're two independent structs, each with its own copy of the data.

This address can now be added to the Member model:

class Member
{
    public int Id { get; }
    public string Name { get; set; }
    public Address Address { get; set; }

    public Member(int id, string name)
    {
        Id = id;
        Name = name;
    }
}
Member member1 = new Member(1, "Ana Martinez");
member1.Address = new Address("10 Main Street", "Madrid", "28013");

Console.WriteLine(member1.Address); // 10 Main Street, Madrid (28013)

  1. When to use struct: small, immutable data

Not everything should be a struct: they're appropriate in a specific set of situations. The practical rule most cited in .NET documentation is to prefer struct when several conditions are met at once:

  • The type represents a single, small conceptual value (few fields), like an address, a coordinate, or a date range.
  • Its instances are, ideally, immutable (they don't change after being created) or change infrequently.
  • It doesn't need to participate in a class inheritance hierarchy (a struct cannot inherit from another struct or from a class, although it can implement interfaces, which you'll see in Module 4).
  • Copying it repeatedly isn't a relevant performance cost (very large structs can be more expensive to copy than passing a reference).

A second useful example in BiblioTech is a date range, for instance to represent the expected period of a loan:

struct DateRange
{
    public DateTime Start;
    public DateTime End;

    public DateRange(DateTime start, DateTime end)
    {
        Start = start;
        End = end;
    }

    public int DurationInDays()
    {
        return (End - Start).Days;
    }
}
DateRange loanPeriod = new DateRange(new DateTime(2026, 1, 10), new DateTime(2026, 1, 24));
Console.WriteLine($"Loan duration: {loanPeriod.DurationInDays()} days"); // 14 days
Signal struct or class?
Small piece of data, few fields, no relevant identity of its own (an address, a date range) struct
Needs inheritance from other classes, or represents an entity with its own identity (Book, Member, Loan) class
Will be copied very frequently and is very small struct (copying is cheap)
Will be shared and modified from several parts of the program, expecting changes to show up everywhere class (reference behavior is exactly what's needed)

  1. record: value-based equality and readable printing for classes

Besides struct, modern C# offers records, designed to model data concisely and immutably, mainly with two automatic advantages over a traditional class: value-based equality and a readable ToString() implementation, both generated by the compiler without you having to write them yourself.

record LoanSummary(string BookTitle, string MemberName, DateTime LoanDate);

This single line — using the so-called positional syntax, with the parameters in parentheses next to the record's name — already defines three read-only properties (BookTitle, MemberName, LoanDate), a constructor that receives all of them, a readable ToString(), and an equality comparison based on the value of its properties, all generated automatically.

LoanSummary summary1 = new LoanSummary("Hopscotch", "Ana Martinez", new DateTime(2026, 1, 10));
LoanSummary summary2 = new LoanSummary("Hopscotch", "Ana Martinez", new DateTime(2026, 1, 10));

Console.WriteLine(summary1);
// LoanSummary { BookTitle = Hopscotch, MemberName = Ana Martinez, LoanDate = 1/10/2026 12:00:00 AM }

Console.WriteLine(summary1 == summary2); // True: same information, even though they are two different objects in memory

Compare this with what would happen if LoanSummary were a traditional class: == would compare whether both variables point to the same object in memory (reference equality), and summary1 == summary2 would be false, even though they held exactly the same data, because new would have created two different objects. Records redefine == (and Equals) to instead compare content: two records are equal if all their properties are.

LoanSummary is a good example of real-world use: it's a read-only piece of data, intended to display or export a loan's summary (for example, from an existing Loan object), with no need for its own behavior or mutable identity — exactly the profile a record models better than a traditional class.

LoanSummary CreateSummary(Loan loan)
{
    return new LoanSummary(loan.Book.Title, loan.Member.Name, loan.LoanDate);
}

Note that, by default, a record (unlike a struct) is still a reference type underneath: what it brings isn't value-copy behavior, but value-based equality and automatic ToString().

  1. record struct: the best of both worlds

C# also lets you combine both ideas with record struct: a type that is, at the same time, a value type (like struct) and that automatically gets value-based equality and generated ToString() (like record). Going back to the Address example from section 2, here's its version as a record struct:

record struct Address(string Street, string City, string PostalCode);

This single line completely replaces the hand-written struct Address from section 2: same value-copy behavior, but with constructor, ToString() and equality generated automatically, without writing a single extra line.

Address address1 = new Address("10 Main Street", "Madrid", "28013");
Address address2 = new Address("10 Main Street", "Madrid", "28013");

Console.WriteLine(address1);              // Address { Street = 10 Main Street, City = Madrid, PostalCode = 28013 }
Console.WriteLine(address1 == address2); // True: value-based equality, plus struct copy behavior

  1. Full comparison: class, struct, record, record struct

class struct record (class) record struct
Behavior when copied By reference By value By reference By value
Default equality (==) By reference (same object) By value of its fields By value of its properties By value of its properties
Readable ToString() generated automatically No (must be written) No (must be written) Yes Yes
Supports class inheritance Yes No Yes (between records) No
Typical use in BiblioTech Entities with identity and behavior: Book, Member, Loan Small value-based data with its own behavior: DateRange Immutable data for display/export: LoanSummary Small, immutable data, with equality and ToString() for free: Address

  1. Applying the right choice in BiblioTech

With everything covered in the module, here's how BiblioTech's complete domain model is distributed across C#'s different tools:

classDiagram
    class LibraryItem {
        <<abstract>>
        +string Title
        +string Author
        +bool Available
        +Lend()
        +Return()
        +Describe()* string
    }
    class Book {
        +string Isbn
    }
    class Magazine {
        +int IssueNumber
    }
    class Member {
        +int Id
        +string Name
        +Address Address
    }
    class Loan {
        +Book Book
        +Member Member
        +DateTime LoanDate
        +DateTime? ReturnDate
        +RegisterReturn()
    }
    LibraryItem <|-- Book
    LibraryItem <|-- Magazine
    Loan --> Book
    Loan --> Member
    Member --> Address
  • Classes (LibraryItem, Book, Magazine, Member, Loan): entities with their own identity, controlled mutable state (encapsulation) and behavior (Lend(), Describe()...). The core of the model, built in the previous lessons.
  • struct (DateRange): a small, self-contained value, with no identity of its own, used by value wherever a duration needs to be calculated.
  • record struct (Address): a small, immutable piece of data associated with a Member, where both value-copy behavior and automatic equality and ToString() matter.
  • record (LoanSummary): an immutable snapshot of a loan, intended to display or export information, with no need for behavior or mutable identity.

Common Mistakes and Tips

  • Using struct for an entity with its own identity: if a type needs rich behavior, free mutability, or participation in an inheritance hierarchy (like Book or Loan), it must be a class, not a struct; forcing a struct there would produce unexpected copies of data that should be shared.
  • Being surprised that a copy of a struct doesn't reflect changes: that's precisely the expected behavior of a value type; if you need two variables to share the same data and both to see the changes, you need a class, not a struct.
  • Confusing record with record struct: a plain record is still a reference type (it gains value-based equality and ToString(), but not copy behavior); if you also want value-copy behavior, you need record struct.
  • Defining very large structs: if a struct accumulates many fields, copying it stops being cheap and can hurt performance; in that case, a class (or rethinking the design) is usually a better choice.
  • Tip: when in doubt between class and struct/record struct, ask yourself whether the type represents "a value" (a date, an address, a coordinate) or "an entity" (something with its own identity that changes over time, like a member or a loan); the answer almost always points to the right choice.

Exercises

  1. Define a struct DateRange with the properties Start and End (both DateTime), a constructor that receives them, and a method int DurationInDays() that returns the difference in days between both dates. Create two different date ranges and show their duration.

  2. Define record struct Address(string Street, string City, string PostalCode); and compare it with the manual version from section 2: create two addresses with the same data using the record struct and check with == that they're considered equal, and print one of them directly with Console.WriteLine to check the automatic ToString().

  3. Define record LoanSummary(string BookTitle, string MemberName, DateTime LoanDate);. Create two LoanSummary objects with exactly the same values but created separately with new, and check with == that, despite being two different objects in memory, they're considered equal.

Solutions

struct DateRange
{
    public DateTime Start;
    public DateTime End;

    public DateRange(DateTime start, DateTime end)
    {
        Start = start;
        End = end;
    }

    public int DurationInDays()
    {
        return (End - Start).Days;
    }
}

DateRange range1 = new DateRange(new DateTime(2026, 1, 10), new DateTime(2026, 1, 24));
DateRange range2 = new DateRange(new DateTime(2026, 2, 1), new DateTime(2026, 2, 5));

Console.WriteLine(range1.DurationInDays()); // 14
Console.WriteLine(range2.DurationInDays()); // 4
record struct Address(string Street, string City, string PostalCode);

Address address1 = new Address("10 Main Street", "Madrid", "28013");
Address address2 = new Address("10 Main Street", "Madrid", "28013");

Console.WriteLine(address1 == address2); // True
Console.WriteLine(address1); // Address { Street = 10 Main Street, City = Madrid, PostalCode = 28013 }
record LoanSummary(string BookTitle, string MemberName, DateTime LoanDate);

LoanSummary summary1 = new LoanSummary("Ficciones", "Luis Gomez", new DateTime(2026, 3, 5));
LoanSummary summary2 = new LoanSummary("Ficciones", "Luis Gomez", new DateTime(2026, 3, 5));

Console.WriteLine(summary1 == summary2); // True

Even though summary1 and summary2 are created separately with new (and therefore occupy different positions in memory), a record's == operator compares their properties one by one, so the result is True.

Conclusion

This lesson completes Module 3: Object-Oriented Programming. You've learned to define classes and create objects, to give them behavior with methods, to guarantee their correct initialization with constructors, to share code through inheritance, to treat different types uniformly with polymorphism, to protect their internal state with encapsulation, to express purely foundational concepts with abstract classes, and, in this last lesson, to choose among class, struct, record and record struct depending on whether what you're modeling is an entity with identity or a simple value. BiblioTech's domain model is now made up of LibraryItem (abstract), Book and Magazine (its concrete inheritors), Member (with its Address), Loan, and supporting types such as DateRange and LoanSummary.

This model, with its classes, properties and methods already defined, is exactly what you'll reuse in Module 4: Advanced C# Concepts: there you'll learn to define interfaces to express shared capabilities without class inheritance, to use delegates and events to react to changes (such as a book becoming available), to take advantage of modern pattern matching to simplify the is/as usage you already know, to write generic code with generics, and above all to store and query entire collections of books, members and loans with collections and LINQ — the tool that will finally let you leave fixed-size arrays behind and work with BiblioTech's full catalog comfortably and expressively.

© Copyright 2026. All rights reserved