Reputation: 177
I'm working on a class library for various units of measure. I'm trying to avoid having to duplicate code as much as possible which should be pretty straightforward since, mathematically, it doesn't matter if you're adding two lengths together or two velocities together - you just convert them to the same unit and add. I thought I could do this pretty easily using a base class and generics...
// A measurement consists of a Value (double) and
// a Unit (an Enum for the various unit types).
public class Measurement<TUnit>
where TUnit : struct
{
protected Measurement(double value, TUnit unit)
{
_value = value;
_unit = unit;
}
protected double _value;
protected TUnit _unit;
public double Value => _value;
public TUnit Unit => _unit;
...
// Conversion Methods - these will get overridden in the derived classes.
protected virtual double GetValueAs(TUnit unit) => throw new NotImplementedException();
...
// Operator overloads
public static Measurement<TUnit> operator +(Measurement<TUnit> left,
Measurement<TUnit> right)
{
return new Measurement<TUnit>(left.Value + right.GetValueAs(left.Unit), left.Unit);
}
}
And this class gets derived for each of the units like this:
public sealed class Length : Measurement<LengthUnit>
{
// Using a private constructor and public static factory methods
private Length(double value, LengthUnit unit)
: base(value, unit) { }
...
}
My problem is that whenever I try to use any of the base class types that return Measurement<TUnit>
, the methods obviously return Measurement<TUnit>
objects.
Length length1 = Length.FromMeters(1); // length1 is 1 meter
Length length2 = Length.FromMeters(2); // length2 is 2 meters
Length length3 = length1 + length2; // Error CS0266: Cannot implicitly convert type
// 'Measurement<LengthUnit>' to 'Length'.
// An explicit conversion exists (are you missing a cast?)
var varlength3 = length1 + length2; // This works, but varlength3 is type Measurement<LengthUnit>
// so I can't use any methods in the Length class.
What am I missing? Is it even possible to do what I'm trying to do? Do I have to bite the bullet and copy the same code into each unit class?
Upvotes: 3
Views: 357
Reputation: 91
Short answer: when you need to operate derived classes from base, it can be done with implementing Curiously Recurring Template Pattern, which originally comes from C++, but can be applied to C# as well.
Long answer:
In your case, base Measurement
class should have one additional generic parameter, which is derived class type.
The only limitation there is that you cannot create derived instances with new
in base classes if derived classes has constructor with parameters. In example below I use MemberwiseClone which might solve the problem.
public class Measurement<TUnit, TMeasurement>
where TUnit : struct
where TMeasurement: Measurement<TUnit, TMeasurement>
{
private readonly double _value;
protected Measurement(double value, TUnit unit)
{
_value = value;
Unit = unit;
}
protected double Value { get; set; }
public TUnit Unit { get; protected set; }
// Conversion Methods - these will get overridden in the derived classes.
protected virtual double GetValueAs(TUnit unit) => throw new NotImplementedException();
// Operator overloads
public static TMeasurement operator +(Measurement<TUnit, TMeasurement> left, Measurement<TUnit, TMeasurement> right)
{
//we cannot create new instance of derived class, TMeasurement, which is limitation of generics in C#, so need some workaround there
//Some kind of clone might be solution for that
var leftClone = (TMeasurement)left.MemberwiseClone();
var resultValue = leftClone.Value + right.GetValueAs(left.Unit);
leftClone.Unit = left.Unit;
leftClone.Value = resultValue;
return leftClone;
}
}
public struct LengthUnit
{
}
public sealed class LengthMeasurement : Measurement<LengthUnit, LengthMeasurement>
{
private LengthMeasurement(double value, LengthUnit unit): base(value, unit)
{
}
public static LengthMeasurement FromMeters(double meters) => throw new NotImplementedException();
}
class Program
{
static void Main(string[] args)
{
var length1 = LengthMeasurement.FromMeters(5);
var length2 = LengthMeasurement.FromMeters(10);
LengthMeasurement length3 = length1 + length2;
}
}
Upvotes: 2
Reputation: 26362
Check your operator overload:
// Operator overloads
public static Measurement<TUnit> operator +(Measurement<TUnit> left,
Measurement<TUnit> right)
{
return new Measurement<TUnit>(left.Value + right.GetValueAs(left.Unit), left.Unit);
}
You are returning a Measurement<TUnit>
object. So the compiler which can't know if a Measurement<TUnit>
class is always a length object, requires you to do the conversion like
Length length3 = (Length)(length1 + length2);
You can ofcource avoid this, by creating a new operator for adding Length
that adds Length
objects.
Upvotes: 1