C# Structs vs Classes
C# structs and classes both let you create your own types, but they model data differently. A struct is a value type: variables usually contain the value itself, and assignment copies that value. A class is a reference type: variables usually contain a reference to an object, so multiple variables can point to the same object. Choosing the right one affects correctness, memory use, equality, and how surprising your code feels to other developers.
Overview: How Structs and Classes Work
A class is the usual choice for objects with identity, behavior, lifecycle, or shared mutable state. When you create a class instance with new, the CLR allocates an object on the managed heap. A class variable stores a reference to that object, not the object data directly. If you assign one class variable to another, both variables refer to the same object. Changing the object through one variable is visible through the other.
A struct is a value type. A variable of a struct type stores the struct value directly in its storage location. If the variable is local, the JIT compiler may store the value on the stack, in registers, or optimize it away. If the struct is a field inside a class, it is stored inline inside that heap object. If the struct is an element in an array, each element is stored inline in the array. The reliable rule is not simply “structs are on the stack”; the reliable rule is that structs have value semantics and are copied by value unless you deliberately use references such as ref, in, or out.
The best design question is: does this type represent a value or an entity? A point, color, date range, measurement, and money amount are often values. If two values contain the same data, they usually mean the same thing. A customer, bank account, shopping cart, database connection, and game player are usually entities. Two entities can have the same visible fields and still be different things.
Structs can reduce allocations when used for small values, but they are not a performance shortcut for every type. Large structs can be expensive because assignment, method calls, and returns may copy many bytes. Mutable structs can also create confusing bugs because you may mutate a copy instead of the value you thought you were changing. Classes have one extra level of indirection through a reference, but they avoid copying large object data on assignment and naturally express shared identity.
Both structs and classes can have fields, properties, constructors, methods, static members, operators, and interface implementations. Classes support inheritance from a base class and can be used as base types. Structs cannot inherit from another struct or class, although every struct ultimately derives from System.ValueType. A struct can be boxed when converted to object or sometimes when used through an interface; boxing copies the struct into a heap object wrapper.
Syntax
public class ClassName
{
public ClassName(string name)
{
Name = name;
}
public string Name { get; set; }
}
public readonly struct StructName
{
public StructName(int value)
{
Value = value;
}
public int Value { get; }
}
| Feature | Class | Struct |
|---|---|---|
| Kind | Reference type | Value type |
| Assignment | Copies a reference | Copies the value |
| Default value | null for an unassigned reference variable |
Zeroed value: numbers are 0, bool is false, references are null |
| Identity | Has object identity | Usually no separate identity beyond its data |
| Inheritance | Can inherit from a base class | Cannot inherit from another struct or class |
| Best fit | Entities, services, large mutable objects | Small immutable values |
Examples
Example 1: Assignment Means Different Things
using System;
public class CounterClass
{
public int Value { get; set; }
}
public struct CounterStruct
{
public int Value { get; set; }
}
class Program
{
static void Main()
{
CounterClass classA = new CounterClass { Value = 1 };
CounterClass classB = classA;
classB.Value = 2;
CounterStruct structA = new CounterStruct { Value = 1 };
CounterStruct structB = structA;
structB.Value = 2;
Console.WriteLine($"classA: {classA.Value}");
Console.WriteLine($"classB: {classB.Value}");
Console.WriteLine($"structA: {structA.Value}");
Console.WriteLine($"structB: {structB.Value}");
}
}
Output:
classA: 2
classB: 2
structA: 1
structB: 2
classB = classA copies a reference, so both variables point to the same CounterClass object. Updating classB.Value changes that shared object. structB = structA copies the struct data, so changing structB.Value does not affect structA.
Example 2: A Struct for a Small Value
using System;
using System.Globalization;
public readonly struct Money
{
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
public decimal Amount { get; }
public string Currency { get; }
public Money Add(Money other)
{
if (Currency != other.Currency)
{
throw new InvalidOperationException("Currencies must match.");
}
return new Money(Amount + other.Amount, Currency);
}
public override string ToString()
{
return $"{Amount.ToString("0.00", CultureInfo.InvariantCulture)} {Currency}";
}
}
class Program
{
static void Main()
{
Money subtotal = new Money(19.99m, "USD");
Money shipping = new Money(4.50m, "USD");
Money total = subtotal.Add(shipping);
Console.WriteLine(subtotal);
Console.WriteLine(shipping);
Console.WriteLine(total);
}
}
Output:
19.99 USD
4.50 USD
24.49 USD
Money is a good struct candidate because it is small and value-like. The Add method returns a new value instead of changing the existing one. The type also protects a business rule: adding dollars to euros would be invalid, so the rule lives with the value instead of being scattered through the program.
Example 3: A Class for an Entity
using System;
public class CustomerAccount
{
public CustomerAccount(string id, string owner)
{
Id = id;
Owner = owner;
}
public string Id { get; }
public string Owner { get; }
public decimal Balance { get; private set; }
public void Deposit(decimal amount)
{
Balance += amount;
}
}
class Program
{
static void Main()
{
CustomerAccount account = new CustomerAccount("A100", "Mina");
CustomerAccount sameAccount = account;
sameAccount.Deposit(75m);
Console.WriteLine(account.Id);
Console.WriteLine(account.Owner);
Console.WriteLine(account.Balance);
Console.WriteLine(ReferenceEquals(account, sameAccount));
}
}
Output:
A100
Mina
75
True
An account is not just a bundle of values. It has identity, history, and operations that change the state of one tracked object. A class expresses that naturally: account and sameAccount refer to the same account object, so a deposit through either variable affects the same balance.
Example 4: Passing to Methods
using System;
public struct ReadingStruct
{
public int Count { get; set; }
}
public class ReadingClass
{
public int Count { get; set; }
}
class Program
{
static void Increase(ReadingStruct reading)
{
reading.Count++;
}
static void Increase(ReadingClass reading)
{
reading.Count++;
}
static void Main()
{
ReadingStruct valueReading = new ReadingStruct { Count = 10 };
ReadingClass objectReading = new ReadingClass { Count = 10 };
Increase(valueReading);
Increase(objectReading);
Console.WriteLine(valueReading.Count);
Console.WriteLine(objectReading.Count);
}
}
Output:
10
11
Method parameters are passed by value by default. For the struct, the method receives a copy of the value, so incrementing it does not change the caller’s variable. For the class, the method receives a copy of the reference, but that copied reference still points to the same object, so changing the object’s property is visible after the method returns.
How It Works Step by Step
- The compiler marks each custom type as either a value type or a reference type in the assembly metadata.
- When a class instance is created, the CLR allocates an object with a header and fields on the managed heap, then the variable stores a reference to that object.
- When a struct value is created, its fields are stored directly wherever the variable, field, array element, or temporary value is stored.
- Assignment copies the contents of the variable. For a class variable, the contents are a reference. For a struct variable, the contents are the fields of the value.
- Passing an argument to a normal parameter also copies the variable contents. Use
refwhen a method must replace or mutate the caller’s struct variable directly, and useinfor large readonly structs when you want to avoid copying. - If a struct is converted to
object, the runtime boxes it by copying the value into a heap object. Later unboxing copies the value out again. - The garbage collector tracks class objects and boxed structs on the managed heap. Unboxed struct values stored inline do not require separate garbage-collected objects.
Common Mistakes
Using a Mutable Struct Like a Shared Object
using System;
public struct CartSummary
{
public int ItemCount { get; set; }
}
class Program
{
static void AddItem(CartSummary summary)
{
summary.ItemCount++;
}
static void Main()
{
CartSummary summary = new CartSummary { ItemCount = 3 };
AddItem(summary);
Console.WriteLine(summary.ItemCount);
}
}
Output:
3
This compiles, but it is often not what the programmer intended. AddItem changed only its local copy. One fix is to make the type immutable and return the updated value explicitly.
using System;
public readonly struct CartSummary
{
public CartSummary(int itemCount)
{
ItemCount = itemCount;
}
public int ItemCount { get; }
public CartSummary WithAddedItem()
{
return new CartSummary(ItemCount + 1);
}
}
class Program
{
static void Main()
{
CartSummary summary = new CartSummary(3);
summary = summary.WithAddedItem();
Console.WriteLine(summary.ItemCount);
}
}
Output:
4
Forgetting That Structs Always Have a Default Value
using System;
public readonly struct Percentage
{
public Percentage(double value)
{
if (value < 0 || value > 100)
{
throw new ArgumentOutOfRangeException(nameof(value));
}
Value = value;
}
public double Value { get; }
public override string ToString()
{
return $"{Value:0.#}%";
}
}
class Program
{
static void Main()
{
Percentage discount = default;
Percentage taxRate = new Percentage(8.24);
Console.WriteLine(discount);
Console.WriteLine(taxRate);
}
}
Output:
0%
8.2%
Struct constructors do not prevent default from existing. Here the default percentage is harmless because 0 is a valid value. If your type cannot tolerate an all-zero value, a class may be a better design.
Choosing Structs Only Because They Sound Faster
public struct LargeReport
{
public decimal A;
public decimal B;
public decimal C;
public decimal D;
public decimal E;
public decimal F;
public decimal G;
public decimal H;
}
A large struct like this may be copied repeatedly as it moves through your program. If the type is large, mutable, or identity-based, use a class unless measurement proves otherwise. Performance decisions should be based on profiling, not on the keyword alone.
Best Practices
- Use classes by default for most application objects, especially entities, services, controllers, repositories, UI components, and objects with identity.
- Use structs for small, immutable, single-purpose values where equality is naturally based on contents.
- Keep structs small. A common guideline is a few fields, not a large record of many values.
- Prefer
readonly structfor custom structs so accidental mutation and defensive copies are less likely. - Make every struct’s default value valid or harmless.
- Avoid public mutable fields in both structs and classes; use properties and methods to protect invariants.
- Do not use a struct simply to avoid heap allocation. Copying, boxing, and API design may cost more.
- Use
record classorrecord structwhen you want compiler-generated value equality and copying support. - Be careful when passing large structs to methods. Consider
inparameters only after the design is clear and performance matters.
Practice Exercises
- Create a
readonly struct Distancewith meters as its stored value, aKilometersproperty, and a method that returns a new added distance. - Create a
class TodoListwith a name and item count. Assign one variable to another, add an item, and print both variables to prove they share one object. - Write two versions of a
Rectangletype: one as an immutable struct for width and height, and one as a class with a mutable color or label. Explain which one better fits each scenario.
Summary
- Classes are reference types; assignment copies a reference to the same object.
- Structs are value types; assignment usually copies the stored data.
- Use classes for identity, lifecycle, inheritance, shared mutation, and larger objects.
- Use structs for small immutable values such as measurements, coordinates, and simple numeric concepts.
- Do not reduce the choice to stack versus heap. Structs can live inline in many places, and boxed structs can still allocate.
- Mutable structs are easy to misuse because methods and assignments often operate on copies.
- The safest struct designs are small, readonly, and valid when default-initialized.
