简体   繁体   中英

Using Generics in Base Classes in C#: How to ensure methods in the base class return the derived class's type?

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?

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;
    }
}

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM