1. The Data Passing Problem
Let's say we have an app for a school, and we need to pass info about a student between different modules: name, year of birth, class. How do we usually do it?
We create a class like this:
public class Student
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
Looks familiar. But this approach has a few issues:
- To compare two students for equality, by default only the object reference is compared. So two students with the same fields but different objects — not equal!
- The class can be changed after creation, which sometimes leads to bugs (especially if the object is already used somewhere);
- Lots of "boilerplate" code: constructors, comparison methods, copying (clones), ToString.
And as you probably guessed: C# can save us from all this hassle! Meet — record.
2. What is a record?
record is a special type in C# designed specifically for storing data. A record has two main features:
- Immutability: objects of type record are immutable by default, meaning their property values are set once at creation and can't be changed (well, the setters are private). You can declare mutable records too, but by default — immutability.
- Value comparison: if two record objects have the same values in all their fields, they're considered equal (== and .Equals() work differently!).
Actually, record is the perfect wrapper for passing data between layers of your app (like from DB to controller, from controller to view, etc.).
3. Record Syntax
The simplest way — positional syntax
When you just need to pass a set of values, you declare a type with one short line:
public record Student(string Name, int YearOfBirth, string Class);
What's happening under the hood? The compiler auto-generates for us:
- Auto read-only properties (with private setters);
- A constructor that takes all the parameters;
- Comparison and copy methods;
- A cool ToString that formats output nicely!
Using a positional record
Let's try using this new type in our school app:
var student1 = new Student("Ivan", 2008, "8A");
var student2 = new Student("Maria", 2008, "8B");
Accessing properties works as usual (just can't change them):
Console.WriteLine($"{student1.Name}, {student1.YearOfBirth}, {student1.Class}");
Trying to change a property after creation
student1.Name = "Petr"; // Error! Property is read-only.
If you uncomment that line — the compiler will instantly complain: can't set value for a read-only property.
This is what the auto-generated ToString looks like
Console.WriteLine(student1); // Outputs: Student { Name = Ivan, YearOfBirth = 2008, Class = 8A }
Looks nice and clear even without manual formatting!
4. Comparing record objects
Reminder: if you create two different objects of a classic class with the same data, they're still not equal to each other:
var a = new Student("Ivan", 2008, "8A");
var b = new Student("Ivan", 2008, "8A");
Console.WriteLine(a == b); // For class: false
But if Student is a record, equality works just like you'd want:
public record Student(string Name, int YearOfBirth, string Class);
var a = new Student("Ivan", 2008, "8A");
var b = new Student("Ivan", 2008, "8A");
Console.WriteLine(a == b); // For record: true!
So two records with the same fields are equal even if they're two different objects in memory.
5. What it looks like inside
Students are often surprised how much the compiler does for us with records. Let's compare for clarity what code you'd have to write by hand for a class and what record does.
Old school class written by hand
public class Student
{
public string Name { get; }
public int YearOfBirth { get; }
public string Class { get; }
public Student(string name, int yearOfBirth, string @class)
{
Name = name;
YearOfBirth = yearOfBirth;
Class = @class; //reference to its own class
}
public override bool Equals(object? obj)
{
if (obj is not Student other) return false;
return Name == other.Name && YearOfBirth == other.YearOfBirth && Class == other.Class;
}
public override int GetHashCode()
{
return HashCode.Combine(Name, YearOfBirth, Class);
}
public override string ToString()
{
return $"Student {{ Name = {Name}, YearOfBirth = {YearOfBirth}, Class = {Class} }}";
}
}
No wonder programmers get paranoid — we write the same stuff 10 times!
Record — one line
public record Student(string Name, int YearOfBirth, string Class);
6. Record and immutability: what you can and can't do
Records have read-only properties by default, and that's awesome for preventing a ton of bugs. But if you really want (like, you're writing for some ancient API), you can declare mutable records:
public record MutableStudent
{
public string Name { get; set; }
public int YearOfBirth { get; set; }
public string Class { get; set; }
}
Now you can change these fields, but you lose some advantages (like safety).
7. Destructuring a record
Since positional syntax is a lot like tuples, you can easily destructure a record:
var student = new Student("Ivan", 2008, "8A");
var (name, year, className) = student;
Console.WriteLine($"{name} - {year}, {className}"); // Ivan - 2008, 8A
The compiler generates a Deconstruct method for every positional record, and this makes working with LINQ, switch-patterns, and life in general way easier.
8. Is a record a class or a struct?
By default, record is a reference type, like a class. So all the usual reference type behavior (heap storage, copying the reference, etc.) works as usual.
If you want a value type, C# has an operator for that too — you can always write:
public record struct Point(int X, int Y);
But for passing data, you almost always use the classic form of records, i.e. reference type. More about record struct — in upcoming lectures :P
Comparison of class, struct, record
| Type | Immutability by default | Value comparison | Easy destructuring | Auto ToString |
|---|---|---|---|---|
| class | No | No (by reference) | No | No |
| struct | No | Yes | No | No |
| record | Yes | Yes | Yes | Yes |
9. Features and common mistakes
A lot of newbies get tripped up by the nuances of using records. For example, sometimes they expect that changing a field in one record object will change another (like with classes and reference copying). Nope! Also, when you use with, remember it always creates a copy, not changes the original. Records are perfect for logic where your app cares about data purity and predictability.
Yeah, if you declared fields with init instead of set, you can only set them at creation or with with, but not later.
GO TO FULL VERSION