Search code examples
c#enumsconstraintsnullable

How can I check if a generic enum has value?


In my C# application, I have an abstract class that is a wrapper around UnitsNet library to group closely related properties (such as a numeric value and unit for that value that user has chosen), to limit the available units of measurement, and also to handle complex inputs from the user (such as multiple number inputs, such as "5, 8, 10:1:20"). Here is the base class and one of the classes that extends it:

public abstract class PropertyBase<TQuantity, TUnit> : where TQuantity : IQuantity where TUnit : Enum
{
    public string InputString { get; set; }

    public TUnit? SelectedUnit
    {
        get => _selectedUnit;
        set
        {
            var oldUnit = _selectedUnit;
            _selectedUnit = value;
            if (oldUnit != null) //This check doesn't work
            {
                //This code should only be run if oldUnit is not null or 0
                ConvertInputString(oldUnit, value);
            }
        }
    }
    
    private TUnit? _selectedUnit;
    protected abstract TUnit[] AvailableUnits { get; }
    protected abstract TQuantity FromValue(double value, TUnit unit);
    protected abstract double As(TQuantity quantity, TUnit unit);
    
    private void ConvertInputString(TUnit oldUnit, TUnit newUnit)
    {
        //Parses and converts values in InputString
    }
    protected List<double> ParseInputValues(string input)
    {
        //...
    }
}

public class InputPropertyAngles : PropertyBase<Angle, AngleUnit>, IInputProperty 
{
    public InputPropertyAngle()
    {
        SelectedUnit = AngleUnit.Degree;
    }
    public override double[] ValuesInSi
    {
        get
        {
            var values = ParseInputValues(InputString).ToArray();
            return values.Select(value => Angle.From(value, SelectedUnit).Degrees).ToArray();
        }
    }
    
    protected override AngleUnit[] AvailableUnits => [AngleUnit.Degree, AngleUnit.Radian];
    protected override Angle FromValue(double value, AngleUnit unit) => Angle.From(value, unit);
    protected override double As(Angle quantity, AngleUnit unit) => quantity.As(unit);
}

Note that AngleUnit is an Enum that comes from UnitsNet library.

The idea is that when the InputPropertyAngle is instanciated, and SelectedUnit is set to default unit in the constructor, the ConvertInputString should not be run. This is done by checking if oldUnit is null (if (oldUnit != null)). However, since oldUnit is TUnit (generic Enum), it cannot be nullable. During runtime, it assumes a value of 0.

Luckily, in UnitsNet library, in AngleUnit Enum, 0 doesn't represent any meaningful value. So I thought I could replace that check with if (oldUnit != 0), but compiler says Cannot apply operator '!=' to operands of type 'TUnit' and 'int'. Unfortunately I cannot constrain TUnit to anything more specific than Enum, because AngleUnit, LengthUnit, MassUnit and other Enums that come from UnitsNet don't have any common interface (and as I understand Enums can't have that at all).

What is the most elegant way to solve this to make that null check possible?


Solution

  • You can restrict your TUnit to struct (since enums are value types) which will allow you to work with nullable values:

    public abstract class PropertyBase<TQuantity, TUnit> where TQuantity : IQuantity where TUnit : struct, Enum
    {
        public TUnit? SelectedUnit
        {
            get => _selectedUnit;
            set
            {
                var oldUnit = _selectedUnit;
                _selectedUnit = value;
                // TODO: rewrite logic taking in account that 
                // value.Value will throw if value is null
                if (oldUnit is not null && value is not null) 
                {
                    ConvertInputString(oldUnit.Value, value.Value);
                }
            }
        }
    
        // ...
    }
    

    or remove nullability and use default (I "moved" it to a virtual property so inheritor can redefine the Default if needed):

    public abstract class PropertyBase<TQuantity, TUnit> where TQuantity : IQuantity where TUnit : struct, Enum
    {
        protected virtual TUnit Default => default;
    
        public TUnit SelectedUnit
        {
            get => _selectedUnit;
            set
            {
                var oldUnit = _selectedUnit;
                _selectedUnit = value;
                if (!oldUnit.Equals(Default)) 
                {
                    ConvertInputString(oldUnit, value);
                }
            }
        }
    
        // ...
    }
    

    See also "The oddities of the Enum constraint" section of the Dissecting new generic constraints in C# 7.3 article:

    There is one very interesting caveat with the Enum constraint: the constraint does not imply but itself that the T is a struct. Moreover, you can actually combine the Enum constraint with the class constraint.

    The combination where TEnum: class, Enum makes no sense and the only reasonable way to use the Enum constraint is to use it with the struct constraint: where TEnum: struct, Enum.