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 Int64
s 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?
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:
IEquatable<T>
IEquatable
/IComparable
interfacesIBitwiseOperators
, IEqualityOperators
- enums implementing IEqualityOperators
will allow using ==
in the generic methods.