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
- Recap: value types versus reference types
struct: defining a value type- When to use
struct: small, immutable data record: value-based equality and readable printing for classesrecord struct: the best of both worlds- Full comparison:
class,struct,record,record struct - Applying the right choice in BiblioTech
- 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 tooA 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 |
struct: defining a value type
struct: defining a value typeA 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)
- When to use
struct: small, immutable data
struct: small, immutable dataNot 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
structcannot inherit from anotherstructor 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) |
record: value-based equality and readable printing for classes
record: value-based equality and readable printing for classesBesides 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.
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 memoryCompare 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().
record struct: the best of both worlds
record struct: the best of both worldsC# 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:
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
- Full comparison:
class, struct, record, record struct
class, struct, record, record structclass |
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 |
- 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 aMember, where both value-copy behavior and automatic equality andToString()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
structfor an entity with its own identity: if a type needs rich behavior, free mutability, or participation in an inheritance hierarchy (likeBookorLoan), it must be aclass, not astruct; forcing astructthere would produce unexpected copies of data that should be shared. - Being surprised that a copy of a
structdoesn'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 aclass, not astruct. - Confusing
recordwithrecord struct: a plainrecordis still a reference type (it gains value-based equality andToString(), but not copy behavior); if you also want value-copy behavior, you needrecord struct. - Defining very large structs: if a
structaccumulates many fields, copying it stops being cheap and can hurt performance; in that case, aclass(or rethinking the design) is usually a better choice. - Tip: when in doubt between
classandstruct/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
-
Define a
struct DateRangewith the propertiesStartandEnd(bothDateTime), a constructor that receives them, and a methodint DurationInDays()that returns the difference in days between both dates. Create two different date ranges and show their duration. -
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 therecord structand check with==that they're considered equal, and print one of them directly withConsole.WriteLineto check the automaticToString(). -
Define
record LoanSummary(string BookTitle, string MemberName, DateTime LoanDate);. Create twoLoanSummaryobjects with exactly the same values but created separately withnew, 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.
C# Programming Course
Module 1: Introduction to C#
- Introduction to C#
- Setting Up the Development Environment
- Hello World Program
- Basic Syntax and Structure
- Variables and Data Types
- Arrays and Strings
Module 2: Control Structures
Module 3: Object-Oriented Programming
- Classes and Objects
- Methods
- Constructors and Destructors
- Inheritance
- Polymorphism
- Encapsulation
- Abstraction
- Structs and Records: Value Types and Reference Types
Module 4: Advanced C# Concepts
- Interfaces
- Delegates and Events
- Pattern Matching and Modern C# Features
- Generics
- Collections
- LINQ (Language Integrated Query)
- Asynchronous Programming
Module 5: Working with Data
- File I/O
- Serialization
- Database Connectivity
- Entity Framework
- Working with JSON and Consuming REST APIs
Module 6: Advanced Topics
- Reflection
- Attributes
- Dynamic Programming
- Memory Management and Garbage Collection
- Multithreading and Parallel Programming
Module 7: Building Applications
Module 8: Best Practices and Design Patterns
- Coding Standards and Best Practices
- Design Patterns
- Dependency Injection and Inversion of Control
- Unit Testing
- Code Review and Refactoring
