C# Structs
A C# struct is a type you define when you want a small value to travel through your program as data. Structs matter because they behave differently from classes: assigning a struct usually copies the value, while assigning a class variable copies a reference. Used well, structs are excellent for values such as points, dates, measurements, money amounts, and other compact data with value semantics.
Overview: How Structs Work
A struct is a value type. Built-in types such as int, double, bool, and DateTime are value types, and your own structs follow the same broad model. A variable of a struct type directly contains the struct’s data in the storage location where that variable lives. If the variable is a local variable, the value is commonly stored in the current method’s stack frame or optimized into registers. If the struct is a field inside a class object, the struct’s fields are stored inline inside that object on the managed heap. The important idea is not “stack versus heap” as an absolute rule; the important idea is that the value is stored directly rather than through object identity.
Structs are useful when the type represents a single logical value. A Point with X and Y coordinates is a good example. The point (3, 4) does not usually need identity. Two separate Point variables with the same coordinates should normally be considered equal in meaning. That is different from a class such as BankAccount, where two accounts may have the same balance but are still different accounts.
Every struct has an implicit parameterless constructor that produces the default value: numeric fields become 0, bool fields become false, and reference fields become null. Modern C# also allows you to write a parameterless constructor in a struct, but default(MyStruct) still creates the zeroed default value. This means every struct design must tolerate a default value existing.
Structs can contain fields, properties, methods, constructors, static members, operators, and they can implement interfaces. They cannot inherit from another struct or class, and ordinary structs cannot be base types. All structs ultimately derive from System.ValueType, which itself derives from object. When a struct value is converted to object or to an interface it implements, the CLR may box it: it copies the value into an object wrapper on the managed heap. Boxing is sometimes necessary, but repeated boxing in hot code can create avoidable allocations.
Syntax
public readonly struct StructName
{
public StructName(int value)
{
Value = value;
}
public int Value { get; }
public override string ToString()
{
return Value.ToString();
}
}
| Part | Meaning |
|---|---|
struct StructName |
Declares a custom value type. Struct names use PascalCase, like class names. |
readonly struct |
Marks the whole struct as immutable: instance fields must be readonly, and properties should not change state. |
public StructName(...) |
Declares a constructor. Struct constructors must definitely assign all fields, often through get-only properties. |
public int Value { get; } |
Declares a read-only property backed by compiler-generated storage. |
override ToString |
Customizes how the value prints. Structs inherit virtual methods from object. |
Examples
Example 1: A Small Immutable Struct
using System;
public readonly struct Point
{
public Point(int x, int y)
{
X = x;
Y = y;
}
public int X { get; }
public int Y { get; }
public int ManhattanDistanceFromOrigin()
{
return Math.Abs(X) + Math.Abs(Y);
}
public override string ToString()
{
return $"({X}, {Y})";
}
}
class Program
{
static void Main()
{
Point start = new Point(3, -4);
Console.WriteLine(start);
Console.WriteLine(start.ManhattanDistanceFromOrigin());
}
}
Output:
(3, -4)
7
Point is a strong struct candidate because it is small, self-contained, and naturally behaves like a value. The readonly keyword makes the design clear: once a point is created, its coordinates do not change. Methods such as ManhattanDistanceFromOrigin calculate from the stored values without mutating the struct.
Example 2: Assignment Copies a Struct
using System;
public struct Score
{
public Score(int points)
{
Points = points;
}
public int Points { get; set; }
}
class Program
{
static void Main()
{
Score first = new Score(10);
Score second = first;
second.Points = 25;
Console.WriteLine(first.Points);
Console.WriteLine(second.Points);
}
}
Output:
10
25
The assignment Score second = first; copies the data. After that line, first and second are independent values. Changing second.Points does not affect first.Points. This is one of the biggest differences from class variables, which usually share an object through references.
Example 3: A Realistic Measurement Struct
using System;
public readonly struct Temperature
{
public Temperature(double celsius)
{
Celsius = celsius;
}
public double Celsius { get; }
public double Fahrenheit => Celsius * 9 / 5 + 32;
public static Temperature FromFahrenheit(double fahrenheit)
{
return new Temperature((fahrenheit - 32) * 5 / 9);
}
public override string ToString()
{
return $"{Celsius:F1} C";
}
}
class Program
{
static void Main()
{
Temperature freezer = new Temperature(-18);
Temperature room = Temperature.FromFahrenheit(68);
Console.WriteLine(freezer);
Console.WriteLine($"{freezer.Fahrenheit:F1} F");
Console.WriteLine(room);
}
}
Output:
-18.0 C
-0.4 F
20.0 C
This example shows a struct that wraps a primitive value with meaning. A plain double cannot tell you whether it stores Celsius, Fahrenheit, meters, or dollars. A Temperature struct keeps the unit rule in one place and provides a named factory method for Fahrenheit input.
How It Works Step by Step
- The compiler records the struct’s fields, properties, methods, constructors, and any interfaces it implements.
- When code declares
Point start, storage is reserved for the point value wherever that variable is stored. - When code runs
new Point(3, -4), the selected constructor creates a value withXset to3andYset to-4. For structs,newdoes not necessarily mean a heap allocation. - When a struct is assigned to another variable, passed to a method by value, or returned from a method, the value is copied unless the code uses references such as
ref,in, orout. - For a
readonly struct, the compiler enforces that instance state is not mutated after construction. This also helps avoid defensive copies in some readonly contexts. - If the value is converted to
object, stored in a non-generic collection, or used through an interface in certain ways, the runtime may box it by copying the struct into a heap object.
Common Mistakes
Choosing a Struct for a Large or Identity-Based Type
public struct CustomerAccount
{
public string AccountNumber;
public string OwnerName;
public string Email;
public decimal Balance;
public DateTime OpenedOn;
}
This is usually the wrong design. Accounts have identity, lifecycle, and behavior beyond being a tiny value. Copying an account value around can also be expensive and confusing. A class is a better default for entities.
using System;
public class CustomerAccount
{
public CustomerAccount(string accountNumber, string ownerName)
{
AccountNumber = accountNumber;
OwnerName = ownerName;
}
public string AccountNumber { get; }
public string OwnerName { get; }
public decimal Balance { get; private set; }
public void Deposit(decimal amount)
{
Balance += amount;
}
}
class Program
{
static void Main()
{
CustomerAccount account = new CustomerAccount("A100", "Rina");
account.Deposit(50m);
Console.WriteLine(account.AccountNumber);
Console.WriteLine(account.Balance);
}
}
Output:
A100
50
Expecting a Mutating Method to Change the Original After a Copy
using System;
public struct CounterValue
{
public int Value { get; private set; }
public void Increment()
{
Value++;
}
}
class Program
{
static void Main()
{
CounterValue original = new CounterValue();
CounterValue copy = original;
copy.Increment();
Console.WriteLine(original.Value);
Console.WriteLine(copy.Value);
}
}
Output:
0
1
This compiles, but it often surprises learners. The method changes copy, not original. Mutable structs are easy to misunderstand, especially when they are stored in properties, collections, or passed between methods. Prefer immutable structs that return a new value when a change is needed.
using System;
public readonly struct CounterValue
{
public CounterValue(int value)
{
Value = value;
}
public int Value { get; }
public CounterValue Incremented()
{
return new CounterValue(Value + 1);
}
}
class Program
{
static void Main()
{
CounterValue original = new CounterValue(0);
CounterValue updated = original.Incremented();
Console.WriteLine(original.Value);
Console.WriteLine(updated.Value);
}
}
Output:
0
1
Best Practices
- Use structs for small, single-purpose values, not for complex domain objects with identity.
- Prefer
readonly structwhen the value should not change after construction. - Keep structs small. Copying a very large struct repeatedly can cost more than using a reference type.
- Design every struct so its default value is valid or at least harmless.
- Avoid public mutable fields. Use get-only properties for immutable values.
- Avoid mutable structs unless you have a strong reason and understand copy behavior.
- Use classes by default for entities, services, controllers, repositories, and objects with shared identity.
- Consider a
record structwhen you want compiler-generated value equality, deconstruction, andwithexpressions; records are covered separately from ordinary structs. - Use generic collections such as
List<Point>instead of old non-generic collections to avoid unnecessary boxing.
Practice Exercises
- Create a
readonly struct MoneywithAmountandCurrencyproperties. OverrideToStringsonew Money(12.5m, "USD")prints12.50 USD. - Create a
readonly struct RectangleSizewithWidth,Height, and anAreaproperty. Test it with a few sizes. - Write a mutable struct example that copies a value, changes the copy, and prints both values. Then rewrite it as an immutable struct that returns a changed copy.
Summary
- A struct is a custom value type, useful for small values that do not need object identity.
- Struct assignment normally copies the stored data, while class assignment normally copies a reference.
- Struct values can live inline in locals, arrays, fields, or objects;
newdoes not automatically imply heap allocation for structs. - Every struct has a default value, so your design must handle zeroed fields sensibly.
- Mutable structs cause many bugs because mutations affect only the particular copy being changed.
readonly structis the safest default for value-like data types.- Boxing copies a struct into an object wrapper and can allocate, so avoid accidental boxing in performance-sensitive code.
