C# 在不违反空接口规则的情况下创建协变泛型类型

C# 在不违反空接口规则的情况下创建协变泛型类型,c#,generics,generic-variance,C#,Generics,Generic Variance,背景:我想“扩展”.NETLazy类型,以支持Lazy和底层T对象之间的隐式转换,从而能够自动打开包含的值。我很容易做到这一点: public class ExtendedLazy<T> : Lazy<T> { public ExtendedLazy() : base() {} public ExtendedLazy(bool isThreadSafe) : base(isThreadSafe) { } public ExtendedLazy(Fu

背景:我想“扩展”.NET
Lazy
类型,以支持
Lazy
和底层
T
对象之间的隐式转换,从而能够自动打开包含的值。我很容易做到这一点:

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;
    }
}
并将我的类定义更改为

public类ExtendedLazy:Lazy,IExtendedLazy

这很好,我能够利用这种协变类型:

ExtendedLazy<DerivedClass> derivedLazy = new ExtendedLazy<DerivedClass>();
IExtendedLazy<BaseClass> baseLazy = derivedLazy;
ExtendedLazy-derivedLazy=new ExtendedLazy();
iextendelazy baseLazy=派生的lazy;

虽然这可以很好地编译和工作,但与此相反,使用空接口作为契约是一种糟糕的设计和代码味道(我相信大多数人都同意)。我的问题是,鉴于CLR无法识别类定义中的变量泛型类型,有什么其他方法可以使它更符合可接受的OO实践?我想我不是唯一一个面对这个问题的人,所以我希望能对此有所了解。

你的逻辑不会像你想象的那样有效

ExtendedLazy<DerivedClass> derivedLazy = new ExtendedLazy<DerivedClass>();
IExtendedLazy<BaseClass> baseLazy = derivedLazy;
BaseClass v = baseLazy;
ExtendedLazy-derivedLazy=new ExtendedLazy();
iextendelazy baseLazy=派生的lazy;
BaseClass v=baseLazy;
这不会编译,因为不存在从
IExtendedLazy
BaseClass
的转换,因为转换运算符仅为
ExtendedLazy
定义

这将迫使您在使用该接口时执行其他操作。添加
T值{get;}
解决了CA1040的问题,并允许您访问基础值


顺便说一句,
Lazy
没有提供
隐式运算符T
的原因是,底层的
Func
可能会抛出,这会令人困惑,因为抛出的行可能没有函数(或属性)调用。

CA1040是一个错误的规则。它告诉您使用属性而不是标记接口,除非这样的检查在运行时慢几个数量级。在您的情况下,无论如何都必须向它添加一个
T值{get;}
成员。@Lucastzesniewski:如果缓存结果,那么标记接口和属性之间的差异对于大多数用例来说是一个清洗,因为与调用的数量相比,您通常使用的类型数量相对较少。CA1040无疑是一个较弱且更值得怀疑的建议,尽管许多人都同意,对于小型项目,标记不太灵活,但对于这些实例来说已经足够了。@Guvante当然,但为什么要强迫我在反射调用中使用类似于
静态ConcurrentDictionary
,当我可以用
在单个表达式中实现相同的操作时?它仍然比缓存查找更快。@Lucastzesniewski:CA1040说“如果你没有一个方法,你就没有一个真正的接口,你就有一个标记接口,标记接口不是最好的主意”。老实说,标记接口不是问题的正确解决方案,这往往是正确的,而不是错误的。有一些问题是一个很好的解决方案,我同意在这些情况下可以忽略CA1040。然而,大多数想要使用标记接口的东西都需要使用几个标记接口,并且很快就变得很难维护,这就是为什么他们建议不要使用它的原因。@Guvante是的,我同意在大多数情况下使用属性更好。我想我只是不喜欢规则:)谢谢你的回答。我明白你的意思,但是,我仍然认为在其他情况下,接口可能不一定要有任何成员,但它只是作为一个变体类型声明器使用。我们还在一个非常特定的单元测试场景中使用Lazy,所以这里不需要担心。但您关于没有默认隐式运算符的观点是有道理的。
ExtendedLazy<DerivedClass> derivedLazy = new ExtendedLazy<DerivedClass>();
IExtendedLazy<BaseClass> baseLazy = derivedLazy;
BaseClass v = baseLazy;