In the .NET Framework, both struct and class keywords are used to define types. However, they represent fundamentally different concepts: value types and reference types, respectively. Structs are value types, meaning instances of structs contain their data directly. When you assign a struct to a new variable, the data is copied, and each variable has its own independent copy of the data.
Understanding structs is crucial for efficient memory management and for building performant .NET applications. They are particularly useful for small, immutable data structures that represent single values.
The primary distinction between structs and classes lies in their memory management and behavior:
int, bool) where operations directly manipulate the value itself. Classes are more like pointers to objects.
You define a struct using the struct keyword, similar to how you define a class with the class keyword.
public struct Point
{
public int X;
public int Y;
// Structs can have methods
public void Move(int xOffset, int yOffset)
{
X += xOffset;
Y += yOffset;
}
// Structs can have properties (though less common for simple data)
public int Magnitude => (int)Math.Sqrt(X * X + Y * Y);
// Structs cannot have parameterless constructors (pre-C# 10)
// Public parameterless constructors are now allowed in C# 10 and later.
// public Point() { X = 0; Y = 0; } // Allowed in C# 10+
// You can define constructors with parameters
public Point(int x, int y)
{
X = x;
Y = y;
}
}
All structs in .NET are value types. This has significant implications:
null by default. To make a struct nullable, you must use the Nullable<T> struct (or its shorthand T?).Nullable<T>.
Every struct has a default value. For most structs, this default value is an instance where all its fields are set to their default values (0 for numeric types, false for bool, null for reference type fields within the struct, etc.).
Before C# 10: Structs could not have explicit parameterless constructors. The system-provided parameterless constructor would always initialize fields to their default values. You could only use parameterless constructors if they were explicitly defined and allowed (e.g., within a class). You had to explicitly assign values to fields or use a constructor with parameters.
C# 10 and later: You can now define an explicit parameterless constructor for a struct. If you do, it will be used for initialization. If you don't, the compiler will provide one that sets all fields to their default values.
// Before C# 10:
// You MUST initialize fields or use a constructor with parameters.
Point p1; // p1 is a valid Point struct, X=0, Y=0
p1.X = 10;
p1.Y = 20; // Now p1.X=10, p1.Y=20
Point p2 = new Point(5, 15); // Uses the parameterized constructor
// C# 10 and later:
// You can define a parameterless constructor:
public struct MyStructWithParamlessCtor
{
public int Value { get; set; }
public MyStructWithParamlessCtor() // Explicit parameterless constructor
{
Value = 100;
}
}
MyStructWithParamlessCtor m1 = new MyStructWithParamlessCtor(); // m1.Value will be 100
// If no parameterless constructor is defined (C# 10+),
// the compiler provides one that initializes fields to default:
Point p3 = new Point(); // If Point has no explicit parameterless ctor, X=0, Y=0
Structs can offer performance benefits in certain scenarios:
However, be mindful of the copying overhead:
ref or in parameter.Structs are generally recommended for types that:
Examples include:
int, float, double, bool, char (built-in value types)DateTimeTimeSpanPoint, Rectangle (custom geometric types)KeyValuePair<TKey, TValue>Structs are excellent for encapsulating simple data and providing related functionality.
using System;
public readonly struct Currency // 'readonly struct' enforces immutability
{
public decimal Amount { get; }
public string Symbol { get; }
public Currency(decimal amount, string symbol)
{
if (amount < 0) throw new ArgumentOutOfRangeException(nameof(amount), "Amount cannot be negative.");
if (string.IsNullOrWhiteSpace(symbol)) throw new ArgumentNullException(nameof(symbol));
Amount = amount;
Symbol = symbol;
}
public override string ToString()
{
return $"{Symbol}{Amount:N2}"; // Format with 2 decimal places
}
// Implementing common operators for convenience
public static Currency operator +(Currency c1, Currency c2)
{
if (c1.Symbol != c2.Symbol)
{
throw new InvalidOperationException("Cannot add currencies with different symbols.");
}
return new Currency(c1.Amount + c2.Amount, c1.Symbol);
}
public static Currency operator -(Currency c1, Currency c2)
{
if (c1.Symbol != c2.Symbol)
{
throw new InvalidOperationException("Cannot subtract currencies with different symbols.");
}
decimal newAmount = c1.Amount - c2.Amount;
if (newAmount < 0) newAmount = 0; // Prevent negative amounts if desired
return new Currency(newAmount, c1.Symbol);
}
}
// Usage:
// Currency price = new Currency(19.99m, "$");
// Currency tax = new Currency(1.50m, "$");
// Currency total = price + tax;
// Console.WriteLine(total); // Output: $21.49
By using readonly struct, you ensure that the struct's fields cannot be modified after initialization, promoting immutability which is a good practice for value types.