Table of Contents
- What Are Structs and Classes?
- Key Differences Between Structs and Classes
- When to Use Structs
- When to Use Classes
- Performance Considerations
- Summary
- References
What Are Structs and Classes?
Structs
A struct is a value type that encapsulates small groups of related variables. It is designed for lightweight data holding and follows value semantics—meaning variables store the actual data, not a reference to it.
Example Syntax:
public struct Point
{
public int X;
public int Y;
public Point(int x, int y)
{
X = x;
Y = y;
}
public double DistanceFromOrigin() => Math.Sqrt(X * X + Y * Y);
}
Classes
A class is a reference type that defines a blueprint for objects. It supports complex behavior, inheritance, and follows reference semantics—variables store a reference (memory address) to the object’s data, which resides on the heap.
Example Syntax:
public class Person
{
public string Name;
public int Age;
public Person(string name, int age)
{
Name = name;
Age = age;
}
public void Greet() => Console.WriteLine($"Hello, I'm {Name}!");
}
Key Differences Between Structs and Classes
To understand when to use structs vs. classes, let’s examine their core differences:
1. Memory Allocation: Stack vs. Heap
-
Structs (Value Types):
By default, struct instances are allocated on the stack (a fast, short-lived memory region). However, if a struct is nested inside a reference type (e.g., a class field), it is stored on the heap as part of that reference type’s memory. -
Classes (Reference Types):
Class instances (objects) are always allocated on the heap (a larger, long-lived memory region). The variable holding the object stores only a reference (pointer) to the heap location.
2. Assignment Behavior: Copy vs. Reference
-
Structs: When you assign a struct variable to another, a full copy of the data is created. Changes to the copied variable do not affect the original.
Example:
Point p1 = new Point(1, 2); Point p2 = p1; // Copies p1's data to p2 p2.X = 10; // Modifies only p2 Console.WriteLine(p1.X); // Output: 1 (original unchanged) -
Classes: When you assign a class variable to another, only the reference (memory address) is copied. Both variables now point to the same object, so changes to one affect the other.
Example:
Person alice = new Person("Alice", 30); Person bob = alice; // Copies the reference to alice's object bob.Age = 31; // Modifies the shared object Console.WriteLine(alice.Age); // Output: 31 (original changed)
3. Inheritance
-
Structs:
- Cannot inherit from other structs or classes (no base struct/class).
- Can implement interfaces (e.g.,
struct MyStruct : IComparable<MyStruct>).
-
Classes:
- Can inherit from one base class (single inheritance) and multiple interfaces.
- Support polymorphism (overriding methods from base classes).
4. Mutability
-
Structs:
Mutable structs (those with modifiable fields/properties) are strongly discouraged. Since structs are copied on assignment, mutable structs can lead to unexpected behavior (e.g., modifying a copy instead of the original). Best practice: Make structs immutable (fields set only in the constructor, no public setters).Immutable Struct Example:
public readonly struct ImmutablePoint // "readonly" enforces immutability { public int X { get; } public int Y { get; } public ImmutablePoint(int x, int y) => (X, Y) = (x, y); } -
Classes:
Can be mutable or immutable. Mutable classes are common (e.g.,List<T>), but immutable classes (e.g.,string) are also widely used for thread safety and predictability.
5. Constructors
-
Structs:
- Have an implicit parameterless constructor that initializes all fields to their default values (e.g.,
0forint,nullforstring). - Prior to C# 10: No user-defined parameterless constructors allowed.
- C# 10+: Support user-defined parameterless constructors to set custom default values.
Example (C# 10+):
public struct Temperature { public double Celsius { get; } // User-defined parameterless constructor (C# 10+) public Temperature() => Celsius = 20.0; // Default room temp public Temperature(double celsius) => Celsius = celsius; } Temperature defaultTemp = new Temperature(); // Celsius = 20.0 - Have an implicit parameterless constructor that initializes all fields to their default values (e.g.,
-
Classes:
- No implicit parameterless constructor (unless no other constructors are defined).
- Require explicit definition of parameterless constructors if needed.
6. Destructors (Finalizers)
- Structs: Cannot have destructors (finalizers). Value types are automatically cleaned up when they go out of scope, so finalization is unnecessary.
- Classes: Can have destructors to clean up unmanaged resources (e.g., file handles, database connections).
7. Nullability
-
Structs:
Non-nullable structs cannot benull. To allownull, useNullable<T>(e.g.,int?) or the nullable modifier?(C# 8+):int nonNullableInt = null; // Compile error int? nullableInt = null; // Valid (Nullable<int>) -
Classes:
Reference types are nullable by default (unless markednonnullablein C# 8+ with nullable reference types enabled).Person? person = null; // Valid (nullable reference type)
8. Default Values
- Structs: The default value (
default(StructType)) is an instance with all fields initialized to their default values (e.g.,default(Point)hasX=0, Y=0). - Classes: The default value is
null(no object exists).
Summary Table of Key Differences
| Feature | Struct | Class |
|---|---|---|
| Type | Value type | Reference type |
| Memory | Stack (or heap if nested in reference type) | Heap |
| Assignment | Copies data | Copies reference |
| Inheritance | No base type; implements interfaces | Inherits from one class; implements interfaces |
| Mutability | Best practice: Immutable | Mutable or immutable |
| Constructors | Implicit parameterless (C# <10); user-defined (C# 10+) | No implicit; user-defined required |
| Destructors | Not allowed | Allowed |
| Nullability | Non-nullable (unless Nullable<T>) | Nullable (default) |
| Default Value | Instance with default field values | null |
When to Use Structs
Use structs for small, simple data types where value semantics and lightweight memory usage are critical. Ideal scenarios include:
1. Small Data Size
Structs with a small memory footprint (typically ≤ 16 bytes) perform best. Larger structs cause expensive copies during assignment or method calls.
Examples:
- Coordinates (
Point,Vector). - Dates/times (
DateTime,TimeSpan). - Simple measurements (
Temperature,Distance).
2. Value Semantics
Use structs when you want variables to store the actual data (not a reference). For example:
- Numeric types (e.g.,
int,doubleare internally structs). - Enums (though enums are a separate type, they behave like lightweight structs).
3. Immutability
Structs should be immutable (no public setters) to avoid unexpected side effects from copying. The .NET runtime uses immutable structs like DateTime and Decimal for this reason.
4. Avoiding Heap Allocation Overhead
For short-lived, small data, structs avoid the garbage collection (GC) overhead of heap-allocated classes.
When to Use Classes
Use classes for larger, complex objects requiring reference semantics, inheritance, or advanced features. Ideal scenarios include:
1. Large or Complex Data
Classes are better for objects with many fields, methods, or dependencies (e.g., Customer, Order).
2. Reference Semantics
When multiple variables should share the same object (e.g., a DatabaseConnection instance shared across a program).
3. Inheritance and Polymorphism
If you need inheritance (e.g., Animal → Dog, Cat) or polymorphism (overriding methods), use classes.
4. Mutable State
Mutable objects (e.g., List<T>, StringBuilder) are safer as classes, since changes propagate to all references.
5. Unmanaged Resource Management
Classes with destructors/finalizers handle unmanaged resources (e.g., FileStream).
Performance Considerations
- Avoid Large Structs: Structs larger than 16 bytes cause excessive copying, degrading performance. Use classes for large data.
- Boxing Overhead: Casting a struct to
objector an interface boxes it (moves it to the heap), introducing GC pressure. Prefer generics to avoid boxing. - GC Impact: Classes increase GC workload (heap allocation/deallocation). For high-performance scenarios with many small objects, structs may be better.
Summary
- Structs are value types ideal for small, immutable data with value semantics (e.g.,
Point,DateTime). Use them to avoid heap allocation and ensure variables store actual data. - Classes are reference types for large, complex objects needing inheritance, polymorphism, or shared state (e.g.,
Person,List<T>). Use them for mutable data or when multiple variables should reference the same object.
By choosing the right type, you’ll write code that’s efficient, predictable, and aligned with C#’s design principles.