Structs and Records
Structs
Structs are value types that can contain data members and function members, similar to classes but with some key differences.
Basic Struct Declaration
// Basic struct declaration
public struct Point
{
// Fields
private int _x;
private int _y;
// Properties
public int X
{
get { return _x; }
set { _x = value; }
}
public int Y
{
get { return _y; }
set { _y = value; }
}
// Constructor
public Point(int x, int y)
{
_x = x;
_y = y;
}
// Method
public double DistanceFromOrigin()
{
return Math.Sqrt(_x * _x + _y * _y);
}
// Override ToString method
public override string ToString()
{
return $"({_x}, {_y})";
}
}
Struct Instantiation and Usage
// Creating a struct using the constructor
Point p1 = new Point(3, 4);
// Creating a struct using object initializer
Point p2 = new Point { X = 5, Y = 12 };
// Default constructor (all fields initialized to default values)
Point p3 = new Point();
p3.X = 1;
p3.Y = 2;
// Using var keyword
var p4 = new Point(7, 8);
// Calling methods
double distance = p1.DistanceFromOrigin(); // 5.0
Console.WriteLine(p1.ToString()); // "(3, 4)"
// Structs are value types (copied by value)
Point p5 = p1;
p5.X = 10;
Console.WriteLine($"p1: {p1}"); // "p1: (3, 4)" - original unchanged
Console.WriteLine($"p5: {p5}"); // "p5: (10, 4)" - modified copy
Readonly Structs (C# 7.2+)
// Readonly struct (all members must be readonly)
public readonly struct Temperature
{
// Readonly fields
private readonly double _celsius;
// Constructor
public Temperature(double celsius)
{
_celsius = celsius;
}
// Readonly properties
public double Celsius => _celsius;
public double Fahrenheit => _celsius * 9 / 5 + 32;
public double Kelvin => _celsius + 273.15;
// Readonly methods
public readonly override string ToString()
{
return $"{_celsius}°C";
}
// Readonly methods can't modify state
// public void SetCelsius(double value) { _celsius = value; } // Error: Can't modify readonly field
}
// Usage
Temperature temp = new Temperature(25);
Console.WriteLine(temp.Fahrenheit); // 77
Console.WriteLine(temp); // "25°C"
Ref Structs (C# 7.2+)
// Ref struct (can only be stored on the stack, not the heap)
public ref struct StackOnlyData
{
public int Value;
public Span<byte> Buffer; // Span<T> is also a ref struct
public StackOnlyData(int value, Span<byte> buffer)
{
Value = value;
Buffer = buffer;
}
}
// Usage
void ProcessData()
{
Span<byte> buffer = stackalloc byte[100]; // Stack-allocated buffer
StackOnlyData data = new StackOnlyData(42, buffer);
data.Buffer[0] = 255;
// Ref structs can't be:
// - Used as field in a class or non-ref struct
// - Boxed to object or dynamic
// - Used in async methods
// - Used in iterator methods
// - Captured by lambda expressions
}
Record Structs (C# 10+)
// Record struct (value type with value-based equality)
public record struct Point3D(double X, double Y, double Z);
// Usage
Point3D p1 = new Point3D(1, 2, 3);
Point3D p2 = new Point3D(1, 2, 3);
Point3D p3 = new Point3D(4, 5, 6);
Console.WriteLine(p1 == p2); // True (value-based equality)
Console.WriteLine(p1 == p3); // False
// Deconstruction
var (x, y, z) = p1;
Console.WriteLine($"X: {x}, Y: {y}, Z: {z}"); // "X: 1, Y: 2, Z: 3"
// With-expression for non-destructive mutation
Point3D p4 = p1 with { Z = 10 };
Console.WriteLine(p4); // Point3D { X = 1, Y = 2, Z = 10 }
Readonly Record Structs (C# 10+)
// Readonly record struct (immutable value type with value-based equality)
public readonly record struct Coordinate(double Latitude, double Longitude);
// Usage
Coordinate c1 = new Coordinate(40.7128, -74.0060); // New York
Coordinate c2 = new Coordinate(40.7128, -74.0060); // Same coordinates
Coordinate c3 = new Coordinate(34.0522, -118.2437); // Los Angeles
Console.WriteLine(c1 == c2); // True
Console.WriteLine(c1 == c3); // False
// With-expression creates a new instance
Coordinate c4 = c1 with { Longitude = -73.9352 };
Console.WriteLine(c4); // Coordinate { Latitude = 40.7128, Longitude = -73.9352 }
Records (C# 9+)
Records are reference types designed for immutable data models with value-based equality semantics.
Basic Record Declaration
// Positional record (concise syntax)
public record Person(string FirstName, string LastName, int Age);
// Usage
Person person1 = new Person("John", "Doe", 30);
Person person2 = new Person("John", "Doe", 30);
Person person3 = new Person("Jane", "Smith", 25);
// Value-based equality
Console.WriteLine(person1 == person2); // True (same property values)
Console.WriteLine(person1 == person3); // False (different property values)
// ToString() is auto-implemented
Console.WriteLine(person1); // Person { FirstName = John, LastName = Doe, Age = 30 }
// Deconstruction
var (firstName, lastName, age) = person1;
Console.WriteLine($"{firstName} {lastName}, {age}"); // "John Doe, 30"
// With-expression for non-destructive mutation
Person olderPerson = person1 with { Age = 31 };
Console.WriteLine(olderPerson); // Person { FirstName = John, LastName = Doe, Age = 31 }
Record with Additional Members
// Record with additional members
public record Employee(string FirstName, string LastName, string Department, decimal Salary)
{
// Additional property
public string FullName => $"{FirstName} {LastName}";
// Additional method
public decimal CalculateAnnualSalary() => Salary * 12;
// Custom constructor that calls the primary constructor
public Employee(string firstName, string lastName)
: this(firstName, lastName, "General", 5000m)
{
Console.WriteLine("Employee created with default department and salary");
}
// Override ToString
public override string ToString()
{
return $"{FullName} - {Department}";
}
}
// Usage
Employee emp1 = new Employee("Alice", "Johnson", "Engineering", 8000m);
Console.WriteLine(emp1.FullName); // "Alice Johnson"
Console.WriteLine(emp1.CalculateAnnualSalary()); // 96000
Employee emp2 = new Employee("Bob", "Smith"); // Uses custom constructor
Console.WriteLine(emp2); // "Bob Smith - General"
// With-expression works with all properties
Employee promotedEmp = emp1 with { Department = "Engineering Management", Salary = 10000m };
Record Inheritance
// Base record
public record Person(string FirstName, string LastName);
// Derived record
public record Student(string FirstName, string LastName, string Major, double GPA)
: Person(FirstName, LastName);
// Another derived record
public record Teacher(string FirstName, string LastName, string Subject, int YearsOfExperience)
: Person(FirstName, LastName);
// Usage
Student student = new Student("Emma", "Davis", "Computer Science", 3.8);
Teacher teacher = new Teacher("Michael", "Wilson", "Mathematics", 15);
// Inheritance is considered in equality
Student student2 = new Student("Emma", "Davis", "Computer Science", 3.8);
Person person = new Person("Emma", "Davis");
Console.WriteLine(student == student2); // True
Console.WriteLine(student == person); // False (different types)
// With-expression with inheritance
Student transferStudent = student with { Major = "Data Science" };
Console.WriteLine(transferStudent); // Student { FirstName = Emma, LastName = Davis, Major = Data Science, GPA = 3.8 }
Record vs Class vs Struct
// Class (reference type, reference equality)
public class PersonClass
{
public string FirstName { get; init; }
public string LastName { get; init; }
public PersonClass(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
}
// Record (reference type, value equality)
public record PersonRecord(string FirstName, string LastName);
// Struct (value type, value equality)
public struct PersonStruct
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
// Record struct (value type, value equality with record features)
public record struct PersonRecordStruct(string FirstName, string LastName);
// Usage comparison
void CompareTypes()
{
// Class instances
var class1 = new PersonClass("John", "Doe");
var class2 = new PersonClass("John", "Doe");
Console.WriteLine(class1 == class2); // False (reference equality)
// Record instances
var record1 = new PersonRecord("John", "Doe");
var record2 = new PersonRecord("John", "Doe");
Console.WriteLine(record1 == record2); // True (value equality)
// Struct instances
var struct1 = new PersonStruct { FirstName = "John", LastName = "Doe" };
var struct2 = new PersonStruct { FirstName = "John", LastName = "Doe" };
Console.WriteLine(struct1.Equals(struct2)); // True (value equality)
// Record struct instances
var recordStruct1 = new PersonRecordStruct("John", "Doe");
var recordStruct2 = new PersonRecordStruct("John", "Doe");
Console.WriteLine(recordStruct1 == recordStruct2); // True (value equality)
// Mutation
// class1.FirstName = "Jane"; // Error if init-only property
// record1.FirstName = "Jane"; // Error (records are immutable)
// struct1.FirstName = "Jane"; // Error if init-only property
// recordStruct1.FirstName = "Jane"; // Error (record structs are immutable)
// Non-destructive mutation
var record3 = record1 with { FirstName = "Jane" };
var recordStruct3 = recordStruct1 with { FirstName = "Jane" };
}