Search code examples
c#genericsgeneric-variance

Creating covariant generic types without violating empty interface rules


Background: I wanted to "expand" the .NET Lazy<> type to support implicit cast between Lazy<T> and the underlying T object to be able automatically unwrap the containing value. I was able to do so fairly easily:

public class ExtendedLazy<T> : Lazy<T>
{
    public ExtendedLazy() : base() {}
    public ExtendedLazy(bool isThreadSafe) : base(isThreadSafe) { }
    public ExtendedLazy(Func<T> valueFactory) : base(valueFactory) { }
    // other constructors

    public static implicit operator T(ExtendedLazy<T> obj)
    {
        return obj.Value;
    }
}

I wanted to take it a step further by making T covariant so I could assign an instance of ExtendedLazy<Derived> to ExtendedLazy<Base>. Since variance modifiers are not allowed in class definitions, I had to resort to an empty interface to achieve this:

public interface IExtendedLazy<out T>
{
}

And changed my class definition to

public class ExtendedLazy<T> : Lazy<T>, IExtendedLazy<T>

This works fine and I was able to make use of this covariant type:

ExtendedLazy<DerivedClass> derivedLazy = new ExtendedLazy<DerivedClass>();
IExtendedLazy<BaseClass> baseLazy = derivedLazy;

While this compiles and works fine, it goes against CA1040: Avoid empty interfaces which says using empty interfaces as contracts is a bad design and a code smell (and I'm sure most people agree). My question is, given the inability of the CLR to recognize variant generic types in class definitions, what other ways are around this to make it more consistent with acceptable OO practices? I'd imagine I'm not the only person facing this issue so am hoping to get some insight on this.


Solution

  • Your logic won't work as well as you think it will.

    ExtendedLazy<DerivedClass> derivedLazy = new ExtendedLazy<DerivedClass>();
    IExtendedLazy<BaseClass> baseLazy = derivedLazy;
    BaseClass v = baseLazy;
    

    This won't compile as there does not exist a conversion from IExtendedLazy<BaseClass> to BaseClass as the conversion operator is only defined for ExtendedLazy<T>.

    This will force you to do something else when using the interface. Adding T Value { get; } solves both the issue of CA1040 and gives you access to the underlying value.

    BTW the reason that Lazy<T> does not provide an implicit operator T is because the underlying Func<T> could throw which would be confusing since the line that throws may very well not have a function (or property) invocation on it.