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?
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 theT
is astruct
. Moreover, you can actually combine theEnum
constraint with theclass
constraint.The combination
where TEnum: class, Enum
makes no sense and the only reasonable way to use theEnum
constraint is to use it with thestruct
constraint:where TEnum: struct, Enum
.