Search code examples
c#.netgenericsenumsgeneric-constraints

Enforcing comparison contraints in Enum genereic parameters


I attempted using comparison constraints on Enum generic parameters as follows:

public abstract class StateMachine {...}

public abstract class StateMachine<TState, TCommand>:
    StateMachine
    where TState: struct, Enum
    where TCommand: struct, Enum
{
    public TState StateInitial { get; private set; }

    protected StateMachine(TState stateInitial) => this.StateInitial = stateInitial;
}

public abstract class FiniteStateMachine<TState, TCommand>:
    StateMachine<TState, TCommand>
    where TState: struct, Enum
    where TCommand: struct, Enum
{
    protected FiniteStateMachine(TState stateInitial)
        : base(stateInitial)
    {
        // Compiler Error CS0019:
        // Operator '==' cannot be applied to operands of type 'TState' and 'TState'.
        // Operator '==' cannot be applied to operands of type 'TCommand' and 'TCommand'.
        var stateEquals = (default(TState) == default(TState));
        var commandEquals = (default(TCommand) == default(TCommand));
    }
}

public sealed class RouterFiniteStateMachine:
    FiniteStateMachine<RouterFiniteStateMachine.EnumState, RouterFiniteStateMachine.EnumCommand>
{
    public enum EnumState { Unplugged, PluggedIn, }
    public enum EnumCommand { None, PlugIn, UnPlug, }

    public RouterFiniteStateMachine()
        : base(EnumState.Unplugged)
    {
        var stateEquals = (default(EnumState) == default(EnumState));
        var commandEquals = (default(EnumCommand) == default(EnumCommand));
    }
}

The comparison in the FiniteStateMachine constructor does not compile with:

Error CS0019: Operator '==' cannot be applied to operands of type 'TState' and 'TState'

I kind of understand the 'why', and would like to know how I could circumvent this in an elegant way. The base classes perform lots of actions that require == comparisons. If I try to use the IEquatable<T>, IComparable<T> constraints on generics, then RouterFiniteStateMachine will not compile.

Since these state machines are long-lived objects, any amount of statically preloaded operations/overheads are welcome as long as they allow the consuming code to be easier to write and more readable.

I thought of caching the enum values as Int64s but that will not handle UInt64 and vice cersa.

I noticed that by this time (.NET 9 C# 13), the built-in integral numeric types now implement a plethora of useful interfaces e.g.:

public readonly struct Int64:
    IComparable, IConvertible, ISpanFormattable, IFormattable, IComparable<long>, IEquatable<long>, IBinaryInteger<long>, IBinaryNumber<long>, IBitwiseOperators<long, long, long>, INumber<long>, IComparisonOperators<long, long, bool>, IEqualityOperators<long, long, bool>, IModulusOperators<long, long, long>, INumberBase<long>, IAdditionOperators<long, long, long>, IAdditiveIdentity<long, long>, IDecrementOperators<long>, IDivisionOperators<long, long, long>, IIncrementOperators<long>, IMultiplicativeIdentity<long, long>, IMultiplyOperators<long, long, long>, ISpanParsable<long>, IParsable<long>, ISubtractionOperators<long, long, long>, IUnaryPlusOperators<long, long>, IUnaryNegationOperators<long, long>, IShiftOperators<long, int, long>, IMinMaxValue<long>, ISignedNumber<long>

Short of the == operator, what are my best options for making type-safe and performant compositions on the TState TCommand generic parameters? Is this even possible without boxing?


Solution

  • You can use the EqualityComparer trick:

    var stateEquals = EqualityComparer<TState>.Default
        .Equals(stateInitial, default(TState));
    var commandEquals = EqualityComparer<TCommand>.Default
        .Equals(default(TCommand), default(TCommand));
    

    Which does not result in allocattions on every comparison and provides typesafety.

    See also: