A struct is a value type: a variable of a struct type holds the data itself, and assignment copies it (C# specification clause 16). A record is a class or struct whose compiler-written members give it value equality and a compact form for immutable data (clauses 15.16 and 16.5).
Structs
public struct Point
{
public int X;
public int Y;
public Point(int x, int y) { X = x; Y = y; }
public double Length() => Math.Sqrt(X * X + Y * Y);
}
Point a = new Point(1, 2);
Point b = a; // copies
b.X = 9; // a.X is still 1
- A struct derives from
System.ValueType, and cannot be inherited from or name a base class. It may implement interfaces. - A struct cannot have a finalizer.
- A struct’s value can never be null, unless it is a nullable value type.
default(Point)has every field zeroed. It does not run a constructor. Array elements and fields of struct type start this way.- Since C# 10 a struct may have field initialisers and declare a parameterless
constructor.
new Point()runs it, butdefaultdoes not. - Since C# 11 any field a constructor leaves unassigned gets its default value (auto default).
- In a struct method,
thisis a reference to the struct variable, so a mutating method changes the variable it is called on. Converting a struct to an interface boxes it, which copies it.
Readonly structs and members
A readonly struct (C# 7.2) has only read-only instance members: all
fields and auto-properties must be read-only. A readonly member (C# 8, clause
16.3.2) promises not to change the struct. The compiler then need not copy the value
before calling it.
public readonly struct Money(decimal amount)
{
public decimal Amount { get; } = amount;
}
public struct Counter
{
private int n;
public readonly int Value => n; // cannot change n
public void Increment() => n++;
}
Primary constructors
A struct can have a primary constructor (C# 12, clause 16.4), as a class can. The parameters are not fields and do not become properties.
Ref structs
A ref struct (C# 7.2, clause 16.2.3) is a struct that lives only on the
stack, so that it can safely hold references into memory. Span<T>
and ReadOnlySpan<T> are the best-known examples.
public ref struct Reader
{
private ReadOnlySpan<byte> data;
public Reader(ReadOnlySpan<byte> data) => this.data = data;
public byte Next() { byte b = data[0]; data = data[1..]; return b; }
}
A ref struct value cannot reach the heap, so the compiler forbids:
- being an array element, a field of a class, or a field of a non-ref struct;
- being boxed to
objectorSystem.ValueType; - capture by a lambda or a local function;
- use in an
asyncmethod or iterator in a block containingawait, or across ayield return(before C# 13, not at all); - conversion to an interface it implements. Since C# 13 it may implement interfaces, but must then implement every instance interface member, even those with default implementations.
Since C# 13 a ref struct may be a type argument where the type parameter
has the allows ref
struct anti-constraint. A readonly ref struct combines the
modifiers, with readonly first. A ref struct is disposable when it has an
accessible, parameterless, void Dispose method.
Ref fields
A ref struct may declare ref fields (C# 11). The compiler
makes sure the target outlives the reference. A ref field may be null;
test it with Unsafe.IsNullRef.
| Declaration | Rebind with = ref | Write with = |
|---|---|---|
ref int x | Yes | Yes |
readonly ref int x | Only in a constructor or init accessor | Yes |
ref readonly int x | Yes | No |
readonly ref readonly int x | Only in a constructor or init accessor | No |
Inline arrays
An inline array (C# 12, clause 16.6) is a struct of a fixed number of elements, held
directly in the struct. It is marked with
[System.Runtime.CompilerServices.InlineArray] and has one instance
field, the element.
[InlineArray(8)]
public struct Buffer8
{
private int element;
}
Buffer8 b = new();
b[3] = 42;
Span<int> span = b; // converts to a span
A collection expression cannot initialise an inline array.
Records
record (C# 9) declares a class with value equality, and record
struct (C# 10) a struct with the same. record class is the same
as plain record and states the kind clearly.
public record Person(string FirstName, string LastName); // record class
public readonly record struct Point(double X, double Y); // readonly record struct
public record struct Measure(DateTime At, double Value); // mutable record struct
var a = new Person("Ann", "Lee");
var b = new Person("Ann", "Lee");
a == b; // true
a.ToString(); // Person { FirstName = Ann, LastName = Lee }
var (first, last) = a; // deconstruction
var c = a with { LastName = "Ng" }; // non-destructive copy
Positional records
Parameters on the record declaration are its positional parameters. The compiler creates:
| Form | Each positional property is |
|---|---|
record, record class | public T Name { get; init; } |
record struct | public T Name { get; set; } |
readonly record struct | public T Name { get; init; } |
It also creates a primary constructor and a Deconstruct method, and for a
record struct, a parameterless constructor. Declaring your own property with the same
name replaces the generated one; it must be initialised from the parameter:
public record Person(string Name, string Id)
{
internal string Id { get; init; } = Id;
}
Attributes can target the generated pieces with property:,
field: or param:. A record need not be positional at all, and
a record can be mutable: public string Name { get; set; }. Immutability is
shallow: a referenced array or object can still change.
What the compiler writes
| Member | Form | Can be written by hand |
|---|---|---|
Equals(object) | Override comparing with Equals(R) | No |
Equals(R other) | virtual or sealed, implements IEquatable<R> | Yes |
Equals(Base other) | When the record has a base record | No |
GetHashCode() | Override, from the data members | Yes |
==, != | Operators using Equals | No |
EqualityContract | protected property of type Type, record classes only | Yes |
ToString() | Name { X = 1, Y = 2 } from public fields and properties | Yes, optionally sealed (C# 10) |
PrintMembers(StringBuilder) | Writes the members for ToString | Yes |
| Clone method | Hidden, used by with, record classes only | No, and no member may be named Clone |
| Copy constructor | protected R(R other), record classes only | Yes |
Two records are equal if they have the same run-time type and each data member,
including compiler-generated backing fields, is equal. For a record class the type check
is done through EqualityContract.
With expressions
x with { Name = value } copies the record and sets the listed properties
(C# 9). The copy has the same run-time type, but only members of the compile-time type
can be set. The copy is shallow, and computed properties should be calculated on access
rather than stored, so that they stay right. The same syntax works on any struct and
on anonymous types (C# 10).
Inheritance
Only record classes can inherit. A record inherits from another record, never from a
plain class, and a plain class cannot inherit from a record. A record may be
abstract or sealed, and from C# 15 a record class may be
closed.
public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
: Person(FirstName, LastName);
A derived record passes parameters to its base and only creates properties for
parameters the base does not have. Deconstruct uses the compile-time type.
Classes, structs and records compared
class | struct | record | record struct | |
|---|---|---|---|---|
| Kind | Reference | Value | Reference | Value |
| Assignment | Copies the reference | Copies the value | Copies the reference | Copies the value |
| Inheritance | Yes | No | Records only | No |
== | Reference, unless overloaded | Not defined | Value | Value |
Equals | Reference | Field by field | Value, written by the compiler | Value, written by the compiler |
| Can be null | Yes | No | Yes | No |