0
votes

Why do I get a type conversion compile error for the following piece of code?

I have quite a few instances of derived Def/View classes in my project. All of them have some code base, say persistence, retrieval etc. I thought by writing a helper class using generics I could achieve maintainability of this common code base.

However I get 'Type Conversion' compilation error in the DoSomeStuff method on the line that assigns the view to def. I have taken care to write implicit cast conversion for all of base and derived classes.

Note that Def & View classes intentionally do not derive from some common class. Additionally I always want to convert only from View to Def and never the other way round, hence only my View classes have the implicit conversion defined on them.

I did try to follow Eric Lipert's discussion on Covariance and Contravariance, but got quite muddled up in my head as he progressed with his examples. Any help with this problem is greatly appreciated.

public class BaseDef
{
    public int Id { get; set; }
}

public class DerivedDef : BaseDef
{
    public string Name { get; set; }

    public DerivedDef()
        : base()
    {

    }

    public DerivedDef(BaseDef bd)
    {
        this.Id = bd.Id;
    }
}

public class BaseView
{
    public int Id { get; set; }

    public BaseView()
    {

    }

    public BaseView(BaseDef bd)
    {
        Id = bd.Id;
    }

    public BaseDef ToBaseDef()
    {
        return new BaseDef { Id = this.Id };
    }

    public static implicit operator BaseView(BaseDef bd)
    {
        return new BaseView(bd);
    }

    public static implicit operator BaseDef(BaseView bv)
    {
        return bv.ToBaseDef();
    }
}

public class DerivedView : BaseView
{
    public string Name { get; set; }

    public DerivedView()
        : base()
    {

    }

    public DerivedView(DerivedDef dd)
        : base(dd)
    {
        Name = this.Name;
    }

    public DerivedDef ToDerivedDef()
    {
        return new DerivedDef(this)
        {
            Name = this.Name,
        };
    }
}

public class SomeHelper<Tdef, Tview>
    where Tdef : BaseDef
    where Tview : BaseView
{
    public void DoSomeStuff(Tview vm)
    {
        Tdef df = vm;   // this line give a compile error 'Cannot convert type 'Tview' to 'Tdef'
        // work with df from here on
    }
}
2
The code you've given compiles fine. Did you mean to declare df as type Tdef rather than using var? (I suspect you could significantly simplify your example, by the way.) - Jon Skeet
Are you sure that's the exact line and error you are getting? I don't see how that's possible since "df" isn't typed explicitly. You used "var" so it's type is only implied by the right hand side anyway. There wouldn't be any conversion taking place there. - Bill Gregg
@Jon, you are right. My mistake, I've corrected the line. I'd be very much interested in hearing your ideas... - kapil

2 Answers

2
votes

There's no guarantee that there is a conversion to Tdef. There's definitely a conversion to BaseDef, and the compiler will use that conversion:

BaseDef df = vm; // This is fine

... but that's not the same thing.

In this case, that conversion is actually going to return a BaseDef anyway - there is no conversion operator from DerivedView to DerivedDef... there's a method (ToDerivedDef) but there's nothing in your code which would call it. Even if the conversion existed in this particular case, the compiler can't guarantee that it exists.

You could use:

Tdef df = (Tdef) (BaseDef) vm;

... that would perform the user-defined conversion to BaseDef, and then a normal cast to Tdef - which would fail at execution time in your case, but could work if the conversion called an appropriate virtual method. It can't be guaranteed at compile-time though.

0
votes

I really couldn't use Jon's approach as I had layering constraints. The def model(s) are defined in the DB layer while the view model(s) in the UI layer.

However, drawing inspiration from Jon's comment, how I solved the issue at hand was to add implicit conversion on all the view model(s) and exposed two properties on the helper class that handled the conversion to & fro. This is how the final code looks...

public class BaseDef
{
    public int Id { get; set; }

    public override string ToString()
    {
        return Id.ToString();
    }
}

public class BaseView
{
    public int Id { get; set; }

    public BaseView()
    {

    }

    public BaseView(BaseDef bd)
    {
        Id = bd.Id;
    }

    public BaseDef ToBaseDef()
    {
        return new BaseDef { Id = this.Id };
    }

    public static implicit operator BaseView(BaseDef bd)
    {
        return new BaseView(bd);
    }

    public static implicit operator BaseDef(BaseView bv)
    {
        return bv.ToBaseDef();
    }
}

public class DerivedDef : BaseDef
{
    public string Name { get; set; }

    public DerivedDef()
        : base()
    {

    }

    public DerivedDef(BaseDef bd)
    {
        this.Id = bd.Id;
    }
}

public class DerivedView : BaseView
{
    public string Name { get; set; }

    public DerivedView()
        : base()
    {

    }

    public DerivedView(DerivedDef dd)
        : base(dd)
    {
        Name = this.Name;
    }

    public DerivedDef ToDerivedDef()
    {
        return new DerivedDef((BaseView)this)
        {
            Name = this.Name,
        };
    }

    public static implicit operator DerivedView(DerivedDef dd)
    {
        return new DerivedView(dd);
    }

    public static implicit operator DerivedDef(DerivedView dv)
    {
        return dv.ToDerivedDef();
    }
}

public class SomeHelper<Tdef, Tview>
    where Tdef : BaseDef
    where Tview : BaseView
{
    public Func<Tview, Tdef> ConvertToDef { get; set; }
    public Func<Tdef, Tview> ConvertToView { get; set; }
    public Tdef Convert(Tview vm)
    {
        if (ConvertToDef == null)
        {
            throw new ArgumentNullException("ConvertToDef uninitialized");
        }
        return ConvertToDef(vm);
    }

    public Tview Convert(Tdef dm)
    {
        if (ConvertToView == null)
        {
            throw new ArgumentNullException("ConvertToView uninitialized");
        }
        return ConvertToView(dm);
    }
}

The consuming code looks like this...

    private static void TestCastWithGenerics()
    {
        BaseDef bd = new BaseDef()
        {
            Id = 1
        };

        DerivedView dv = new DerivedView()
        {
            Id = 1,
            Name = "DV",
        };
        var aClassD = new SomeHelper<DerivedDef, DerivedView>();
        aClassD.ConvertToDef = dv1 => dv1; // Behind scenes the implicit cast is being invoked...
        DerivedDef dd = aClassD.Convert(dv);

        var aClassB = new SomeHelper<BaseDef, BaseView>();
        aClassB.ConvertToView = bd1 => bd1; // Behind scenes the implicit cast is being invoked...
        BaseView bv = aClassB.Convert(bd);

        Console.WriteLine(dd);
        Console.WriteLine(bv);
    }