C# Structs

Structs are a fundamental data type in C# that provide value-type semantics for encapsulating small groups of related variables. Understanding structs is essential for writing efficient and maintainable code, especially when dealing with performance-critical applications or representing simple data structures.

Table of Contents

  1. Introduction to Structs
  2. Declaring and Initializing Structs
  3. Struct Members
  4. Value Types vs. Reference Types
  5. When to Use Structs
  6. Differences Between Structs and Classes
  7. Best Practices for Structs
  8. Common Mistakes with Structs
  9. Advanced Topics
  10. Real-World Example
  11. Summary

1. Introduction to Structs

A struct in C# is a value type that can encapsulate data and related functionality. Structs are similar to classes but have key differences that make them suitable for specific scenarios.

Key Characteristics:
- Value Type: Stored on the stack, copied by value.
- No Inheritance: Cannot inherit from other structs or classes (except implicitly from `System.ValueType`).
- Default Constructor: Implicit parameterless constructor that initializes all members to their default values.
- Efficient for Small Data Structures: Ideal for representing lightweight objects.

2. Declaring and Initializing Structs

2.1 Declaring a Struct

Syntax:
struct StructName
{
    // Fields
    public int Field1;
    public string Field2;

    // Constructor
    public StructName(int field1, string field2)
    {
        Field1 = field1;
        Field2 = field2;
    }

    // Methods
    public void Display()
    {
        Console.WriteLine($"Field1: {Field1}, Field2: {Field2}");
    }
}
Example:
using System;

struct Point
{
    public int X;
    public int Y;

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    public void Display()
    {
        Console.WriteLine($"Point: ({X}, {Y})");
    }
}

class Program
{
    static void Main()
    {
        Point p1 = new Point(10, 20);
        p1.Display(); // Output: Point: (10, 20)

        Point p2 = p1; // Copy by value
        p2.X = 30;
        p2.Display(); // Output: Point: (30, 20)
        p1.Display(); // Output: Point: (10, 20)
    }
}

Sample Output:
Point: (10, 20)
Point: (30, 20)
Point: (10, 20)

Explanation:
- Struct Declaration: The `Point` struct has two fields, `X` and `Y`.
- Initialization: `p1` is initialized using the parameterized constructor.
- Value Copy: Assigning `p1` to `p2` creates a copy. Modifying `p2` does not affect `p1` because structs are value types.

3. Struct Members

Structs can contain various members similar to classes:
- Fields: Variables to store data.
- Properties: Encapsulated data access.
- Methods: Functions to perform operations.
- Constructors: Initialize struct instances.
- Events: Notifications for subscribers.
- Operators: Overload operators for custom behavior.

3.1 Example: Struct with Properties and Methods

using System;

struct Rectangle
{
    // Auto-implemented properties
    public double Width { get; set; }
    public double Height { get; set; }

    // Constructor
    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }

    // Method to calculate area
    public double Area()
    {
        return Width * Height;
    }

    // Method to display dimensions
    public void Display()
    {
        Console.WriteLine($"Rectangle: Width = {Width}, Height = {Height}, Area = {Area()}");
    }
}

class Program
{
    static void Main()
    {
        Rectangle rect = new Rectangle(5.0, 3.0);
        rect.Display(); // Output: Rectangle: Width = 5, Height = 3, Area = 15
    }
}

Sample Output:
Rectangle: Width = 5, Height = 3, Area = 15

Explanation:
- Properties: `Width` and `Height` are properties with getters and setters.
- Methods: `Area` calculates the area, and `Display` outputs the rectangle's details.
- Initialization: The `Rectangle` is created using the parameterized constructor and displayed.

4. Value Types vs. Reference Types

Structs are value types, whereas classes are reference types. This distinction affects how they are stored and passed around in memory.

Feature Structs Classes
Type Value Type Reference Type
Memory Allocation Typically on the stack On the heap
Copy Behavior Copied by value Copied by reference
Nullability Cannot be null (unless using nullable types) Can be null
Inheritance Cannot inherit from other structs/classes (except System.ValueType) Supports inheritance; can inherit from other classes
Default Constructor Implicit parameterless constructor; cannot define one Can define multiple constructors, including parameterless
Performance More efficient for small data structures More flexible; suitable for complex objects
Immutability Often used for immutable data Can be mutable or immutable

4.1 Example: Value vs. Reference Type Behavior

using System;

struct ValueType
{
    public int Number;
}

class ReferenceType
{
    public int Number;
}

class Program
{
    static void Main()
    {
        // Value Type
        ValueType vt1 = new ValueType { Number = 10 };
        ValueType vt2 = vt1; // Copy by value
        vt2.Number = 20;
        Console.WriteLine($"vt1.Number: {vt1.Number}"); // Output: 10
        Console.WriteLine($"vt2.Number: {vt2.Number}"); // Output: 20

        // Reference Type
        ReferenceType rt1 = new ReferenceType { Number = 10 };
        ReferenceType rt2 = rt1; // Copy by reference
        rt2.Number = 20;
        Console.WriteLine($"rt1.Number: {rt1.Number}"); // Output: 20
        Console.WriteLine($"rt2.Number: {rt2.Number}"); // Output: 20
    }
}

Sample Output:
vt1.Number: 10
vt2.Number: 20
rt1.Number: 20
rt2.Number: 20

Explanation:
- Value Types: `vt1` and `vt2` are independent copies. Modifying `vt2` does not affect `vt1`.
- Reference Types: `rt1` and `rt2` reference the same object. Modifying `rt2` affects `rt1`.

5. When to Use Structs

Structs are best suited for:
- Small Data Structures: Typically under 16 bytes for performance efficiency.
- Immutable Data: Data that does not change after creation.
- Performance-Critical Applications: Where avoiding heap allocations and garbage collection is beneficial.
- Representing Simple Data Types: Such as points, rectangles, or complex numbers.

Example Use Cases:
- Geometric Data: Points, vectors, and rectangles.
- Key-Value Pairs: Used in dictionaries.
- Complex Numbers: Representing mathematical complex numbers.
- Immutable Data Transfer Objects (DTOs): For passing data without modification.

5.1 Example: Immutable Point Struct

using System;

readonly struct ImmutablePoint
{
    public double X { get; }
    public double Y { get; }

    public ImmutablePoint(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double DistanceTo(ImmutablePoint other)
    {
        double dx = X - other.X;
        double dy = Y - other.Y;
        return Math.Sqrt(dx * dx + dy * dy);
    }

    public override string ToString()
    {
        return $"({X}, {Y})";
    }
}

class Program
{
    static void Main()
    {
        ImmutablePoint p1 = new ImmutablePoint(3.0, 4.0);
        ImmutablePoint p2 = new ImmutablePoint(0.0, 0.0);
        Console.WriteLine($"Point 1: {p1}"); // Output: (3, 4)
        Console.WriteLine($"Point 2: {p2}"); // Output: (0, 0)
        Console.WriteLine($"Distance: {p1.DistanceTo(p2)}"); // Output: 5
    }
}

Sample Output:
Point 1: (3, 4)
Point 2: (0, 0)
Distance: 5

Explanation:
- Immutable Struct: The `ImmutablePoint` struct is marked as `readonly`, ensuring immutability.
- Methods: `DistanceTo` calculates the Euclidean distance between two points.
- ToString Override: Provides a readable string representation of the point.

6. Differences Between Structs and Classes

Structs and classes are both used to define custom data types, but they have fundamental differences that affect how they are used.

Feature Structs Classes
Type Value Type Reference Type
Inheritance Cannot inherit from other structs/classes (except System.ValueType) Support inheritance; can inherit from other classes
Default Constructor Implicit parameterless constructor; cannot define one Can define multiple constructors, including parameterless
Memory Allocation Typically on the stack On the heap
Nullability Cannot be null (unless using nullable types) Can be null
Copy Behavior Copied by value Copied by reference
Performance More efficient for small data structures More flexible; suitable for complex objects
Immutability Often used for immutable data Can be mutable or immutable

6.1 Summary of Differences

- Inheritance: Classes support inheritance and polymorphism; structs do not.
- Memory Allocation: Structs are allocated on the stack (more efficient for small objects), while classes are allocated on the heap.
- Copying Behavior: Structs are copied by value, leading to independent copies. Classes are copied by reference, meaning multiple references point to the same object.
- Use Cases: Structs are ideal for small, immutable data structures. Classes are suitable for larger, more complex objects that may require inheritance and reference semantics.

7. Best Practices for Structs

- Keep Structs Small: Prefer structs with fewer than 16 bytes for optimal performance.
- Immutable Design: Make structs immutable by providing read-only properties and avoiding methods that modify state.
- Avoid Inheritance: Since structs do not support inheritance, design them accordingly.
- Provide Meaningful Constructors: Ensure constructors initialize all fields.
- Implement Interfaces Carefully: Be cautious when implementing interfaces to avoid boxing.
- Use Readonly Structs: In C# 7.2 and later, mark structs as `readonly` to enforce immutability and optimize performance.

7.1 Example: Readonly Struct

using System;

readonly struct ImmutablePoint
{
    public double X { get; }
    public double Y { get; }

    public ImmutablePoint(double x, double y)
    {
        X = x;
        Y = y;
    }

    public override string ToString()
    {
        return $"({X}, {Y})";
    }
}

class Program
{
    static void Main()
    {
        ImmutablePoint p = new ImmutablePoint(5.0, 12.0);
        Console.WriteLine(p); // Output: (5, 12)
    }
}
Explanation: - Readonly Struct: The `readonly` keyword ensures that all fields are immutable after construction.
- Immutable Properties: `X` and `Y` can only be set through the constructor.
- ToString Override: Provides a readable string representation of the point.

8. Common Mistakes with Structs

- Large Structs: Using structs for large data structures can lead to performance issues due to frequent copying.
Mistake Example:
struct LargeStruct
{
  public int[] Data; // Large array increases struct size
}
Solution: Use classes for large or complex data structures.

- Mutable Structs: Allowing struct fields to be modified can lead to unexpected behavior.
Mistake Example:
struct MutablePoint
{
  public double X;
  public double Y;
}

class Program
{
  static void Main()
  {
      MutablePoint p = new MutablePoint { X = 1, Y = 2 };
      ModifyPoint(p);
      Console.WriteLine($"Point: ({p.X}, {p.Y})"); // Output: (1, 2), modification not reflected
  }
  
  static void ModifyPoint(MutablePoint point)
  {
      point.X = 10;
      point.Y = 20;
  }
}
Solution: Design structs to be immutable.

- Boxing and Unboxing: Implementing interfaces on structs can cause boxing, leading to performance penalties.
Mistake Example:
using System;

struct Point : IComparable
{
  public int X, Y;

  public int CompareTo(object obj)
  {
      // Implementation
      return 0;
  }
}
Solution: Use generic interfaces like `IComparable<T>` to avoid boxing.

- Missing Parameter Initialization: Not initializing all fields in a parameterized constructor can lead to unexpected default values.
Mistake Example:
struct Rectangle
{
  public double Width;
  public double Height;

  public Rectangle(double width)
  {
      Width = width;
      // Height is not initialized, remains default 0
  }
}
Solution: Initialize all fields in constructors.

9. Advanced Topics

9.1 Boxing and Unboxing

Boxing is the process of converting a value type to a reference type (`object`). Unboxing is the reverse process.

Example: Boxing and Unboxing
using System;

struct Point
{
    public int X;
    public int Y;

    public override string ToString()
    {
        return $"({X}, {Y})";
    }
}

class Program
{
    static void Main()
    {
        Point p = new Point { X = 10, Y = 20 };
        object obj = p; // Boxing
        Console.WriteLine(obj); // Output: (10, 20)

        Point p2 = (Point)obj; // Unboxing
        Console.WriteLine($"Point2: ({p2.X}, {p2.Y})"); // Output: Point2: (10, 20)
    }
}

Sample Output:
(10, 20)
Point2: (10, 20)

Explanation:
- Boxing: Assigning a struct to an `object` type wraps the value type in a reference type, allocating memory on the heap.
- Unboxing: Casting the `object` back to the struct type retrieves the original value.

Performance Consideration: Boxing and unboxing can impact performance, especially in tight loops or high-frequency operations. Prefer using generic interfaces (e.g., `IComparable<T>`) to minimize boxing.

9.2 Implementing Interfaces on Structs

Structs can implement interfaces, but care must be taken to avoid boxing.

Example: Implementing IComparable<T>
using System;

struct Point : IComparable<Point>
{
    public int X;
    public int Y;

    public int CompareTo(Point other)
    {
        if (X != other.X)
            return X.CompareTo(other.X);
        return Y.CompareTo(other.Y);
    }

    public override string ToString()
    {
        return $"({X}, {Y})";
    }
}

class Program
{
    static void Main()
    {
        Point p1 = new Point { X = 1, Y = 2 };
        Point p2 = new Point { X = 3, Y = 4 };
        Console.WriteLine(p1.CompareTo(p2)); // Output: -1
    }
}

Sample Output:
-1

Explanation:
- IComparable: Generic interface avoids boxing by specifying the type.
- CompareTo Method: Provides a way to compare two `Point` instances without boxing.

9.3 Structs with Properties

While structs can have properties, it's recommended to keep them simple and avoid complex logic within property getters/setters to maintain performance and predictability.

Example: Struct with Properties
using System;

struct Circle
{
    public double Radius { get; }

    public Circle(double radius)
    {
        Radius = radius;
    }

    public double Area => Math.PI * Radius * Radius;

    public override string ToString()
    {
        return $"Circle with Radius = {Radius}, Area = {Area}";
    }
}

class Program
{
    static void Main()
    {
        Circle c = new Circle(5.0);
        Console.WriteLine(c); // Output: Circle with Radius = 5, Area = 78.53981633974483
    }
}

Sample Output:
Circle with Radius = 5, Area = 78.53981633974483

Explanation:
- Auto-Implemented Property: `Radius` has only a getter, ensuring immutability.
- Computed Property: `Area` calculates the area based on `Radius`.

9.4 Readonly Structs (C# 7.2 and Later)

Marking structs as `readonly` enforces immutability and can lead to performance optimizations by preventing defensive copies.

Example: Readonly Struct
using System;

readonly struct ImmutablePoint
{
    public double X { get; }
    public double Y { get; }

    public ImmutablePoint(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double DistanceTo(ImmutablePoint other)
    {
        double dx = X - other.X;
        double dy = Y - other.Y;
        return Math.Sqrt(dx * dx + dy * dy);
    }

    public override string ToString()
    {
        return $"({X}, {Y})";
    }
}

class Program
{
    static void Main()
    {
        ImmutablePoint p1 = new ImmutablePoint(3.0, 4.0);
        ImmutablePoint p2 = new ImmutablePoint(0.0, 0.0);
        Console.WriteLine($"Point 1: {p1}"); // Output: (3, 4)
        Console.WriteLine($"Point 2: {p2}"); // Output: (0, 0)
        Console.WriteLine($"Distance: {p1.DistanceTo(p2)}"); // Output: 5
    }
}

Sample Output:
Point 1: (3, 4)
Point 2: (0, 0)
Distance: 5

Explanation:
- Readonly Keyword: Ensures that all fields are immutable after construction.
- Immutability: Prevents accidental modification of struct members, enhancing thread safety.

9.5 Structs and Generics

Using structs with generics can improve performance by avoiding heap allocations and boxing, especially with value-type constraints.

Example: Generic Struct with Constraints
using System;

struct Pair<T1, T2>
{
    public T1 First { get; }
    public T2 Second { get; }

    public Pair(T1 first, T2 second)
    {
        First = first;
        Second = second;
    }

    public override string ToString()
    {
        return $"Pair: ({First}, {Second})";
    }
}

class Program
{
    static void Main()
    {
        Pair<int, string> pair = new Pair<int, string>(1, "One");
        Console.WriteLine(pair); // Output: Pair: (1, One)
    }
}

Sample Output:
Pair: (1, One)

Explanation:
- Generic Struct: `Pair<T1, T2>` can hold two related values of any types.
- Type Safety: Ensures that the types are consistent and checked at compile time.

10. Real-World Example

Example: Immutable Complex Number Struct

Complex numbers are a perfect example of a struct, as they are small, immutable, and represent a single concept.

using System;

readonly struct Complex
{
    public double Real { get; }
    public double Imaginary { get; }

    public Complex(double real, double imaginary)
    {
        Real = real;
        Imaginary = imaginary;
    }

    // Method to add two complex numbers
    public Complex Add(Complex other)
    {
        return new Complex(this.Real + other.Real, this.Imaginary + other.Imaginary);
    }

    // Method to multiply two complex numbers
    public Complex Multiply(Complex other)
    {
        double realPart = this.Real * other.Real - this.Imaginary * other.Imaginary;
        double imaginaryPart = this.Real * other.Imaginary + this.Imaginary * other.Real;
        return new Complex(realPart, imaginaryPart);
    }

    public override string ToString()
    {
        return $"{Real} + {Imaginary}i";
    }
}

class Program
{
    static void Main()
    {
        Complex c1 = new Complex(2.0, 3.0);
        Complex c2 = new Complex(4.0, -1.0);

        Complex sum = c1.Add(c2);
        Complex product = c1.Multiply(c2);

        Console.WriteLine($"c1: {c1}"); // Output: 2 + 3i
        Console.WriteLine($"c2: {c2}"); // Output: 4 + -1i
        Console.WriteLine($"Sum: {sum}"); // Output: 6 + 2i
        Console.WriteLine($"Product: {product}"); // Output: 11 + 10i
    }
}

Sample Output:
c1: 2 + 3i
c2: 4 + -1i
Sum: 6 + 2i
Product: 11 + 10i

Explanation:
- Immutable Struct: The `Complex` struct is marked as `readonly`, ensuring that instances cannot be modified after creation.
- Methods: `Add` and `Multiply` perform operations on complex numbers, returning new `Complex` instances.
- Usage: Demonstrates creating complex numbers, adding, and multiplying them.

11. Summary

Structs in C# are powerful value types that offer performance benefits and type safety for representing simple, immutable data structures. They are ideal for scenarios where small, lightweight objects are needed without the overhead of heap allocations associated with classes.

Key Takeaways:
- Value Types: Structs are stored on the stack and copied by value, making them efficient for small data.

- Immutability: Designing structs to be immutable enhances thread safety and predictability.

- No Inheritance: Structs do not support inheritance, which simplifies their usage but limits extensibility.

- Best Practices: Keep structs small, immutable, and avoid complex members to maintain performance.

- Common Pitfalls: Large or mutable structs can lead to performance issues and unexpected behaviors.

- Advanced Features: Readonly structs, generic structs, and implementing interfaces can enhance struct functionality while maintaining performance.

By mastering structs, you can effectively utilize value-type semantics in C#, leading to more efficient and maintainable applications.

Previous: C# Multithreading | Next: C# Enums

<
>