Interface Support for Ref Struct Types in C#

Interface Support for ref struct Types in C#

In this article, I will discuss Interface Support for Ref Struct Types in C# with Examples. Please read our previous articles discussing Improved Support for ref and unsafe in C# with Examples. Starting with C# 13, ref struct types can now implement interfaces. This was a significant change, as ref struct types were previously restricted from implementing interfaces. This enhancement allows ref struct types (typically stack-allocated) to interact with the rest of the object-oriented features in C# that rely on interfaces, like polymorphism. However, C# 13 enforces ref safety for these types, meaning they cannot be boxed or implicitly converted to interface types.

What is a ref struct?

A ref struct is a stack-allocated type, meaning it exists in the call stack rather than on the heap. This is beneficial for performance reasons, especially when you need high-performance, low-latency memory access, such as large arrays, buffers, and stack-based memory management. Examples of ref struct types include Span<T> and ReadOnlySpan<T>, which are commonly used for efficient data manipulation in C#.

Example of a ref struct:
public ref struct MyRefStruct
{
    private int[] _array;

    public MyRefStruct(int size)
    {
        _array = new int[size];
    }

    public int this[int index]
    {
        get => _array[index];
        set => _array[index] = value;
    }
}

In this example, MyRefStruct is a ref struct that wraps an integer array and allows access to the array’s elements via the indexer.

Before C# 13: Restrictions on ref struct Types

Before C# 13, ref struct types had a major limitation: they could not implement interfaces. This was because ref struct types cannot be boxed, and boxing would break the ref safety principles of ref struct types. As a result, polymorphism and interface-based design were not possible with ref struct types.

What is Interface Support for ref struct Types in C# 13?

In C# 13, the language has been updated to allow ref struct types to implement interfaces, which provides more flexibility. With this feature, ref struct types can now:

  • Implement interfaces.
  • It is used in polymorphic scenarios, where the interface methods are called on instances of ref struct types.
  • Be passed as generic type arguments constrained to ref struct types.
Key Restrictions for Ref Struct Types Implementing Interfaces
  • No Boxing: A ref struct cannot be converted to an interface type directly because it would require boxing, which would violate ref safety.
  • Explicit Interface Implementations: Explicit interface methods can only be accessed through generic methods where the type parameter allows ref struct types.
  • New Members in Interfaces: If an interface defines a default implementation for a method, the ref struct type must implement that method explicitly.
Example: Using Interfaces with ref struct Types in C# 13

Now, we will create a bank account management system that uses ref struct types for different account types (e.g., SavingsAccount and CurrentAccount). We will implement the IAccount interface for polymorphic operations across various account types.

namespace CSharp13NewFeatures
{
    // Define an interface for bank account operations
    public interface IAccount
    {
        void Deposit(decimal amount);       // Deposit money into the account
        void Withdraw(decimal amount);      // Withdraw money from the account
        decimal GetBalance();               // Get the current balance
        int Length { get; }                 // Get the number of transactions
    }

    // C# 13: ref struct implementing IAccount for Savings Account
    public ref struct SavingsAccount : IAccount
    {
        private decimal _balance;  // Account balance
        private int _transactions; // Number of transactions

        // Constructor to initialize the savings account with an initial balance
        public SavingsAccount(decimal initialBalance)
        {
            _balance = initialBalance;
            _transactions = 0;
        }

        // Explicitly implement Deposit
        public void Deposit(decimal amount)
        {
            _balance += amount;  // Add deposit to balance
            _transactions++;     // Increment transaction count
            Console.WriteLine($"Deposited {amount} to Savings Account. New Balance: {_balance}");
        }

        // Explicitly implement Withdraw
        public void Withdraw(decimal amount)
        {
            if (_balance >= amount)
            {
                _balance -= amount;  // Deduct from balance
                _transactions++;      // Increment transaction count
                Console.WriteLine($"Withdrew {amount} from Savings Account. New Balance: {_balance}");
            }
            else
            {
                Console.WriteLine("Insufficient funds for withdrawal.");
            }
        }

        // Explicitly implement GetBalance
        public decimal GetBalance()
        {
            return _balance;
        }

        // Explicitly implement Length
        public int Length => _transactions;  // Return number of transactions
    }

    // C# 13: ref struct implementing IAccount for Current Account
    public ref struct CurrentAccount : IAccount
    {
        private decimal _balance;  // Account balance
        private int _transactions; // Number of transactions

        // Constructor to initialize the current account with an initial balance
        public CurrentAccount(decimal initialBalance)
        {
            _balance = initialBalance;
            _transactions = 0;
        }

        // Explicitly implement Deposit
        public void Deposit(decimal amount)
        {
            _balance += amount;  // Add deposit to balance
            _transactions++;     // Increment transaction count
            Console.WriteLine($"Deposited {amount} to Current Account. New Balance: {_balance}");
        }

        // Explicitly implement Withdraw
        public void Withdraw(decimal amount)
        {
            if (_balance >= amount)
            {
                _balance -= amount;  // Deduct from balance
                _transactions++;      // Increment transaction count
                Console.WriteLine($"Withdrew {amount} from Current Account. New Balance: {_balance}");
            }
            else
            {
                Console.WriteLine("Insufficient funds for withdrawal.");
            }
        }

        // Explicitly implement GetBalance
        public decimal GetBalance()
        {
            return _balance;
        }

        // Explicitly implement Length
        public int Length => _transactions;  // Return number of transactions
    }

    class Program
    {
        // Main method to demonstrate usage
        public static int Main()
        {
            try
            {
                // Create instances of SavingsAccount and CurrentAccount
                var savingsAccount = new SavingsAccount(1000m);  // Initial balance: 1000
                var currentAccount = new CurrentAccount(500m); // Initial balance: 500

                // Process each account through a common method
                Console.WriteLine("Processing Savings Account:");
                ProcessAccount(ref savingsAccount, 200m);    // Deposit 200 and Withdraw 100 from Savings

                Console.WriteLine("\nProcessing Current Account:");
                ProcessAccount(ref currentAccount, 50m);    // Deposit 50 and Withdraw 30 from Current

                return 0;  // success
            }
            catch (Exception ex)
            {
                // Top-level exception handling
                Console.WriteLine($"ERROR: {ex.GetType().Name} – {ex.Message}");
                return 1;  // failure
            }
        }

        // Generic method constrained to allow ref struct types
        static void ProcessAccount<T>(ref T account, decimal amount)
            where T : IAccount, allows ref struct  // Constrain to IAccount (allow ref struct)
        {
            // Print the initial state before processing
            Console.WriteLine($"Initial Balance: {account.GetBalance()}");

            // Deposit money into the account
            account.Deposit(amount);

            // Withdraw some money from the account
            account.Withdraw(amount / 2);  // Example: withdrawing half of the deposited amount

            // Print final balance and number of transactions
            Console.WriteLine(
                $"Final Balance: {account.GetBalance()} (Transactions: {account.Length})");
            Console.WriteLine("-----------------------------------");
        }
    }
}
Explanation of the Code:
  • Interface Definition (IAccount): This interface defines methods for managing bank accounts, including depositing, withdrawing, checking balance, and tracking transactions.
  • SavingsAccount (ref struct): SavingsAccount is a ref struct that implements the IAccount interface. It uses explicit interface implementations for methods like Deposit, Withdraw, GetBalance, and Length. The Deposit method adds funds to the account, and the Withdraw method deducts funds, tracking the number of transactions.
  • CurrentAccount (ref struct): CurrentAccount is another ref struct implementing the IAccount interface. It uses the same methods as SavingsAccount. The Deposit method increases the balance, while the Withdrawal decreases it, and the number of transactions is tracked similarly.
  • Generic Method (ProcessAccount): The ProcessAccount method is generic and operates on any type that implements IAccount. It performs the deposit and withdrawal, then prints the final balance and number of transactions. The method is constrained to allow ref struct types using where T: IAccount, allows ref struct.
  • Main Method: The Main method demonstrates how to create instances of SavingsAccount and CurrentAccount, process each account via ProcessAccount, and print the results.
Output:

Using Interfaces with ref struct Types in C# 13

Real-time Example: E-Commerce Inventory System

In this e-commerce inventory system, we must manage different product types, such as physical products (e.g., items in a warehouse) and digital products (e.g., downloadable software). We will use reference structures to improve our performance when managing the inventory. Additionally, we will implement an interface that allows us to interact polymorphically with these products, regardless of whether they’re physical or digital.

using System;

namespace CSharp13NewFeatures
{
    // Define an interface for product stock management
    public interface IProduct
    {
        void AddStock(int quantity);      // Add stock to product
        void Sell(int quantity);          // Sell a quantity of the product
        int GetStock();                   // Get the current stock quantity
        int Length { get; }               // Get the total number of items in stock
    }

    // C# 13: ref struct implementing IProduct for Physical Product
    public ref struct PhysicalProduct : IProduct
    {
        private int[] _stock;  // Array representing stock of a physical product

        // Constructor to initialize the product with a certain number of stock
        public PhysicalProduct(int size)
        {
            _stock = new int[size];
        }

        // Explicitly implement AddStock
        public void AddStock(int quantity)
        {
            _stock[0] += quantity;  // Add stock (simplified for this example)
            Console.WriteLine($"Added {quantity} items to Physical Product. New stock: {_stock[0]}");
        }

        // Explicitly implement Sell
        public void Sell(int quantity)
        {
            if (_stock[0] >= quantity)
            {
                _stock[0] -= quantity;  // Sell the product (simplified)
                Console.WriteLine($"Sold {quantity} items from Physical Product. Remaining stock: {_stock[0]}");
            }
            else
            {
                Console.WriteLine("Insufficient funds for withdrawal.");
            }
        }

        // Explicitly implement GetStock
        public int GetStock()
        {
            return _stock[0];  // Return the current stock (simplified)
        }

        // Explicitly implement Length
        public int Length => _stock.Length;  // Return the length of the stock array
    }

    // C# 13: ref struct implementing IProduct for Digital Product
    public ref struct DigitalProduct : IProduct
    {
        private int _downloads;  // Represent downloads for a digital product

        // Constructor to initialize the product with a certain number of downloads
        public DigitalProduct(int downloads)
        {
            _downloads = downloads;
        }

        // Explicitly implement AddStock for Digital Product
        public void AddStock(int quantity)
        {
            _downloads += quantity;  // Add digital product downloads
            Console.WriteLine($"Added {quantity} downloads to Digital Product. New download count: {_downloads}");
        }

        // Explicitly implement Sell for Digital Product
        public void Sell(int quantity)
        {
            if (_downloads >= quantity)
            {
                _downloads -= quantity;  // Decrease the number of downloads
                Console.WriteLine($"Sold {quantity} items from Digital Product. Remaining downloads: {_downloads}");
            }
            else
            {
                Console.WriteLine("Insufficient downloads available.");
            }
        }

        // Explicitly implement GetStock for Digital Product
        public int GetStock()
        {
            return _downloads;  // Return the number of downloads (stock equivalent)
        }

        // Explicitly implement Length for Digital Product
        public int Length => _downloads;  // Length is the number of downloads
    }

    class Program
    {
        // Main method to demonstrate usage
        public static int Main()
        {
            try
            {
                // Create instances of SavingsAccount and CheckingAccount
                var physicalProduct = new PhysicalProduct(10);  // 10 physical items
                var digitalProduct = new DigitalProduct(50);    // 50 digital downloads

                // Process each product through a common method
                Console.WriteLine("Processing Physical Product:");
                ProcessProduct(ref physicalProduct, 5);   // Add and sell physical product
                Console.WriteLine("\nProcessing Digital Product:");
                ProcessProduct(ref digitalProduct, 2);    // Add and sell digital product

                return 0;  // success
            }
            catch (Exception ex)
            {
                // Top-level exception handling
                Console.WriteLine($"ERROR: {ex.GetType().Name} – {ex.Message}");
                return 1;  // failure
            }
        }

        // Generic method constrained to allow ref struct types
        static void ProcessProduct<T>(ref T product, int quantity)
            where T : IProduct, allows ref struct // Constrain to IProduct (allow ref struct)
        {
            // Print the initial stock of the product before processing
            Console.WriteLine($"Initial Stock of {typeof(T).Name}: {product.GetStock()}");

            // Add stock to the product
            product.AddStock(quantity);

            // Sell some of the stock from the product
            product.Sell(quantity / 2);  // Example: selling half of the deposited amount

            // Print final stock and number of transactions
            Console.WriteLine($"Final Stock of {typeof(T).Name}: {product.GetStock()}");
            Console.WriteLine("-----------------------------------");
        }
    }
}
Explanation of the Code:
  • IProduct Interface: This interface defines methods to manage products in the inventory: AddStock, Sell, GetStock, and Length (to track transactions). Both PhysicalProduct and DigitalProduct types implement these methods explicitly.
  • PhysicalProduct and DigitalProduct: These are ref struct types that implement the IProduct interface explicitly. AddStock adds to the product’s stock, and Sell decreases it. Both methods update the transaction count. The GetStock method retrieves the current stock count, and the Length property tracks the number of transactions.
  • ProcessProduct Method: The ProcessProduct method is generic and works with any type that implements IProduct. It first prints the product’s initial stock, then performs a deposit (AddStock) and a withdrawal (Sell), printing the result at each step. The method demonstrates polymorphism: it can process both PhysicalProduct and DigitalProduct using the IProduct interface.
  • Main Method: The Main method demonstrates how to create PhysicalProduct and DigitalProduct instances and process them through the ProcessProduct method. Thanks to the interface, this method ensures that both product types are handled in a unified way.
Output:

Interface Support for Ref Struct Types in C# with Examples

With C# 13, ref struct types can now implement interfaces, enabling polymorphism while still benefiting from stack-based memory allocation for better performance. The ref struct constraint ensures ref safety, and this example shows how you can manage bank accounts efficiently with memory-safe operations.

In the next article, I will discuss the introduction of the Field Keyword in C# with Examples. In this article, I explain Interface Support for Ref Struct Types in C# with Examples. I would like your feedback. Please post your feedback, questions, or comments about this article.

Leave a Reply

Your email address will not be published. Required fields are marked *