通用约束,其中 T : struct 和 where T : class

Posted

技术标签:

【中文标题】通用约束,其中 T : struct 和 where T : class【英文标题】:Generic constraints, where T : struct and where T : class 【发布时间】:2011-02-27 19:20:55 【问题描述】:

我想区分以下情况:

    普通值类型(例如int) 可以为空的值类型(例如int?) 引用类型(例如string) - 可选,我不关心它是否映射到上面的 (1) 或 (2)

我想出了以下代码,适用于情况(1)和(2):

static void Foo<T>(T a) where T : struct   // 1

static void Foo<T>(T? a) where T : struct   // 2

但是,如果我尝试像这样检测案例 (3),它不会编译:

static void Foo<T>(T a) where T : class   // 3

错误消息是类型“X”已经定义了一个名为“Foo”的成员,具有相同的参数类型。好吧,不知何故,我无法区分 where T : structwhere T : class

如果我删除第三个函数(3),下面的代码也不会编译:

int x = 1;
int? y = 2;
string z = "a";

Foo (x); // OK, calls (1)
Foo (y); // OK, calls (2)
Foo (z); // error: the type 'string' must be a non-nullable value type ...

如何让Foo(z) 进行编译,将其映射到上述函数之一(或第三个具有另一个约束的函数,我没有想到)?

【问题讨论】:

对于引用类型有:new(),但是对于可为空的值类型,这有奇怪的行为。 【参考方案1】:

约束不是签名的一部分,但参数是。并且在重载决议期间强制执行参数中的约束。

所以让我们把约束放在一个参数中。这很丑陋,但它有效。

class RequireStruct<T> where T : struct  
class RequireClass<T> where T : class  

static void Foo<T>(T a, RequireStruct<T> ignore = null) where T : struct   // 1
static void Foo<T>(T? a) where T : struct   // 2
static void Foo<T>(T a, RequireClass<T> ignore = null) where T : class   // 3

(迟到六年总比没有好?)

【讨论】:

哈,好主意!实际上,您不需要将ignore 参数添加到第二个采用T?Foo&lt;T&gt; 函数中。 这让我有机会在code.fitness/post/2016/04/generic-type-resolution.html上发表关于该主题的博客 我的想法来自one of Eric Lippert's blog posts。我一直喜欢恶作剧。至于T?,我需要它的情况只有案例1和3,我忘了测试是否需要。 这很光滑。我喜欢使用“_”而不是“忽略”函数式编程。 更简单的方式,不需要辅助类:不确定这是否只是较新的语言版本或其他。我想这可能会导致结构的额外分配,但无论如何我都需要测试是否等于默认值。 static void Foo&lt;T&gt;(T? value) where T : struct static void Foo&lt;T&gt;(T value, T defaultValue = default) where T : struct static void Foo&lt;T&gt;(T obj) where T : class 【参考方案2】:

很遗憾,您无法仅根据约束来区分要调用的方法类型。

因此,您需要在不同的类中定义一个方法,或者使用不同的名称。

【讨论】:

+1。当然,第一个和第二个工作,因为TT? 是不同的论点。 (TNullable&lt;T&gt; +1 见:blogs.msdn.com/b/ericlippert/archive/2009/12/10/… 感谢您的快速回复;如果我无法区分类型,有没有办法通过放松一些约束来编译我的最后一个示例? 啊,只需删除方法 (1) 的 where T : struct,我的示例就会编译。这对我来说已经足够了。 实际上,如果不介意泛型引用类型的虚拟“可选”参数对其泛型参数有约束,则可以根据约束来区分方法调用的类型,并具有该参数的“null”默认值。由于编译器将排除任何类型无法构造的重载,因此虚拟参数类型中的约束将有效限制考虑的重载。当然,如果编译器可以在不为虚拟参数提供值的调用站点上执行此操作,它...【参考方案3】:

除了你对Marnix's answer的评论,你可以通过一点反思来实现你想要的。

在下面的示例中,不受约束的 Foo&lt;T&gt; 方法使用反射将调用转移到适当的受约束方法 - FooWithStruct&lt;T&gt;FooWithClass&lt;T&gt;。出于性能原因,我们将创建并缓存一个强类型委托,而不是每次调用 Foo&lt;T&gt; 方法时都使用普通反射。

int x = 42;
MyClass.Foo(x);    // displays "Non-Nullable Struct"

int? y = 123;
MyClass.Foo(y);    // displays "Nullable Struct"

string z = "Test";
MyClass.Foo(z);    // displays "Class"

// ...

public static class MyClass

    public static void Foo<T>(T? a) where T : struct
    
        Console.WriteLine("Nullable Struct");
    

    public static void Foo<T>(T a)
    
        Type t = typeof(T);

        Delegate action;
        if (!FooDelegateCache.TryGetValue(t, out action))
        
            MethodInfo mi = t.IsValueType ? FooWithStructInfo : FooWithClassInfo;
            action = Delegate.CreateDelegate(typeof(Action<T>), mi.MakeGenericMethod(t));
            FooDelegateCache.Add(t, action);
        
        ((Action<T>)action)(a);
    

    private static void FooWithStruct<T>(T a) where T : struct
    
        Console.WriteLine("Non-Nullable Struct");
    

    private static void FooWithClass<T>(T a) where T : class
    
        Console.WriteLine("Class");
    

    private static readonly MethodInfo FooWithStructInfo = typeof(MyClass).GetMethod("FooWithStruct", BindingFlags.NonPublic | BindingFlags.Static);
    private static readonly MethodInfo FooWithClassInfo = typeof(MyClass).GetMethod("FooWithClass", BindingFlags.NonPublic | BindingFlags.Static);
    private static readonly Dictionary<Type, Delegate> FooDelegateCache = new Dictionary<Type, Delegate>();

(请注意,此示例不是线程安全的。如果您需要线程安全,那么您将需要对所有对缓存字典的访问使用某种锁定,或者 -- 如果您'能够以 .NET4 为目标——改用 ConcurrentDictionary&lt;K,V&gt;。)

【讨论】:

可以通过使用类似于Comparer&lt;T&gt;.Default 的方法来改进事情,例如使用Action&lt;T&gt; 类型的公共字段FooMethod 创建一个私有静态泛型类FooInvoker&lt;T&gt;(因为FooInvoker&lt;T&gt;MyClass 之外无法访问,就不会有外部代码滥用公共字段的风险)?如果FooInvoker&lt;T&gt; 的类构造函数适当地设置FooMethod,我认为这可能会避免在运行时查找字典的需要(我不知道.net 是否需要在每次Foo&lt;T&gt; 是称为)。 查看我发布的答案,了解如何使用静态类。我可能犯了一些语法错误,因为我是从记忆中输入的(主要是 vb.net 中的程序),但应该有足够的大纲让你继续前进。【参考方案4】:

删除第一个方法的结构约束。如果您需要区分值类型和类,可以使用参数的类型来做到这一点。

      static void Foo( T? a ) where T : struct
      
         // nullable stuff here
      

      static void Foo( T a )
      
         if( a is ValueType )
         
            // ValueType stuff here
         
         else
         
            // class stuff
         
      

【讨论】:

@Maxim:谢谢。我面临的问题是,在不可为空的方法中,我必须能够调用其他接受并返回T? 的函数,而如果没有where T : struct 约束,这是无效的。【参考方案5】:

放大我对 LukeH 的评论,如果需要使用反射来调用基于类型参数的不同操作(与对象实例的类型不同),一种有用的模式是创建一个私有的通用静态类,类似于以下(这个确切的代码未经测试,但我以前做过这种事情):

静态类 FooInvoker public Action theAction = configureAction; void ActionForOneKindOfThing(TT param) where TT:thatKindOfThing,T ... void ActionForAnotherKindOfThing(TT param) where TT:thatOtherKindOfThing,T ... 无效配置操作(T参数) ... 确定 T 是哪种事物,并将 `theAction` 设置为 ...上述方法。然后以...结束 动作(参数);

请注意,如果在 TT 不符合该方法的约束时尝试为 ActionForOneKindOfThing&lt;TT&gt;(TT param) 创建委托,反射将引发异常。因为系统在创建委托时验证了TT 的类型,所以可以安全地调用theAction 而无需进一步的类型检查。另请注意,如果外部代码这样做:

FooInvoker.theAction(param);

只有第一次调用需要任何反射。后续调用将直接调用委托。

【讨论】:

【参考方案6】:

谢天谢地,从 C# 版本 7.3 开始,这种乱七八糟的事情就更少了

参见Whats new in C# 7.3 - 它不是很明确,但它现在似乎在重载解析期间在某种程度上使用了“where”参数。

重载解析现在有更少的模棱两可的情况

另请参阅您的 Visual Studio 项目中的 Selecting C# Version

它仍然会与以下内容发生冲突

Foo(x);
...
static void Foo<T>(T a) where T : class   // 3
static void Foo<T>(T a) where T : struct   // 3

但会正确解决

Foo(x);
...
static void Foo<T>(T a, bool b = false) where T : class   // 3
static void Foo<T>(T a) where T : struct   // 3

【讨论】:

我尝试了 C# 7.3,它并没有改变我原来问题中方法 (1) 和 (3) 之间的冲突。我仍然收到错误类型'X'已经定义了一个名为'Foo'的成员具有相同的参数类型 @PierreArnaud 似乎我有点过分了。我的情况略有不同,因此我认为它会是您的情况。我已经修改了回复以反映这一点......似乎MS已经改进了这一点,但他们还有一些工作要做......【参考方案7】:

如果您不需要泛型参数并且只想在编译时区分这三种情况,您可以使用以下代码。

static void Foo(object a)   // reference type
static void Foo<T>(T? a) where T : struct   // nullable
static void Foo(ValueType a)   // valuetype

【讨论】:

以上是关于通用约束,其中 T : struct 和 where T : class的主要内容,如果未能解决你的问题,请参考以下文章

包含值类型和字符串的 C# 通用约束

`<T extends E>` 形式的通用约束,其中`E` 是`enum`?

通用方法,其中T是type1或type2

c#泛型中非托管和结构约束之间的区别

为啥 Nullable<T> 不匹配作为通用约束的引用类型 [重复]

非 Nullable 类型的通用约束