泛型约束,其中T : struct和T : class

68
我希望能够区分以下情况:
  1. 纯值类型(例如int
  2. 可空值类型(例如int?
  3. 引用类型(例如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

然而,如果我尝试像这样检测第三种情况,它将无法编译:

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()关键字来实例化对象。然而,在处理可空值类型时,这种方法会导致一些奇怪的行为。 - Adam Houldsworth
8个回答

81

约束条件不是签名的一部分,但参数是。而且在重载解析期间对参数中的约束条件进行强制执行。

因此,让我们将约束条件放在参数中。虽然有些难看,但它能够起作用。

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

(晚做总比不做好?)

2
哈,好主意!事实上,在第二个接受 T?Foo<T> 函数中,您不需要添加 ignore 参数。 - Pierre Arnaud
1
这给了我一个机会,在http://code.fitness/post/2016/04/generic-type-resolution.html上写关于这个主题的博客。 - Pierre Arnaud
1
我从Eric Lippert的一篇博客文章中得到了这个想法(https://blogs.msdn.microsoft.com/ericlippert/2009/12/10/constraints-are-not-part-of-the-signature/)。我一直喜欢恶作剧。至于T?,我需要它的情况只有1和3两种情况,并且我忘记测试是否需要它了。 - Alcaro
这很流畅。我喜欢使用"_"代替"ignore",就像函数式编程一样。 - Seth
1
更简单的方法,无需使用辅助类:不确定这是否仅适用于新版本语言或其他版本。我猜它可能会导致结构体的额外分配,但是我需要测试默认值的相等性。static void Foo<T>(T? value) where T : struct { }static void Foo<T>(T value, T defaultValue = default) where T : struct { }static void Foo<T>(T obj) where T : class { } - Jannes

22

很不幸,你无法仅根据限制条件来区分要调用的方法类型。

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


2
+1。当然,第一个和第二个工作是因为TT?是不同的参数。(TNullable<T> - Powerlord
1
+1 请参阅:http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx - Anthony Pegram
谢谢您的快速回复;如果我无法区分类型,是否有一些方法可以通过放宽某些限制来使我的最后一个示例编译? - Pierre Arnaud
啊,对于方法(1),只需删除“where T:struct”即可使我的示例编译。这对我来说已经足够了。 - Pierre Arnaud
2
实际上,如果一个人不介意在泛型引用类型的虚拟“可选”参数中具有其泛型参数约束,并且为该参数设置“null”默认值,则可以根据约束区分方法调用的类型。由于编译器将排除任何无法构造类型的重载,因此虚拟参数类型中的约束将有效地限制考虑哪些重载。当然,如果编译器可以在没有为虚拟参数提供值的调用站点执行该操作,它... - supercat
1
应该可以不需要调用者传递虚拟参数就能够做到这一点,但我不知道有什么方法能够实现。 - supercat

10

回复您在Marnix's answer中的评论,您可以通过使用一些反射来实现您想要的功能。

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

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<K,V>。)


1
通过使用类似于Comparer<T>.Default的方法,例如创建一个私有静态泛型类FooInvoker<T>,并具有类型为Action<T>的公共字段FooMethod,是否可以改善事情(由于FooInvoker<T>将无法在MyClass之外访问,因此不存在外部代码滥用公共字段的风险)?如果FooInvoker<T>的类构造函数适当地设置了FooMethod,我认为这可能避免了运行时需要进行字典查找的需要(我不知道.NET是否每次调用Foo<T>时都需要执行内部查找)。 - supercat
1
请查看我发布的答案,了解如何使用静态类的概要。由于我是凭记忆打字(并且大多数情况下使用vb.net编程),所以可能会有一些语法错误,但应该有足够的概要让您开始。 - supercat

5

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

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

2
@Maxim: 谢谢。我面临的问题是,在非可空方法中,我必须能够调用其他接受并返回T?的函数,而没有where T : struct约束条件这是无效的。 - Pierre Arnaud

3
扩展我对LukeH的评论,如果需要使用反射根据类型参数(而不是对象实例的类型)调用不同的操作,则有用的模式是创建一个私有泛型静态类,类似以下代码(此代码未经测试,但我之前做过这样的事情):
static class FooInvoker<T>
{
  public Action<Foo> theAction = configureAction;
  void ActionForOneKindOfThing<TT>(TT param) where TT:thatKindOfThing,T
  {
    ...
  }
  void ActionForAnotherKindOfThing<TT>(TT param) where TT:thatOtherKindOfThing,T
  {
    ...
  }
  void configureAction(T param)
  {
    ... 确定T是哪种类型的东西,并将“theAction”设置为上述方法之一。然后以…结束…
    theAction(param);
  }
}
请注意,当尝试为ActionForOneKindOfThing<TT>(TT param)创建委托时,反射将引发异常,如果TT与该方法的约束条件不符合。 因为在创建委托时系统验证了TT的类型,所以可以安全地调用theAction而无需进一步进行类型检查。 还要注意,如果外部代码执行以下操作:
  FooInvoker<T>.theAction(param);
只有第一次调用需要进行任何反射。 后续调用将直接调用委托。

2
幸运的是,在C#版本7.3中需要进行这种操作的次数更少了。
请参见C# 7.3的新功能 - 虽然不是很明确,但现在似乎在重载解析过程中在某种程度上使用“where”参数。

现在重载解析的模糊情况更少了。

此外,请参见在Visual Studio项目中选择C#版本
它仍会与以下内容发生冲突。
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'”。 - Pierre Arnaud
1
@PierreArnaud 看来我有点过于急躁了。我的情况略有不同,既然可以解决,我就认为你的情况也能够解决。我已经修改了回复以反映这一点……似乎微软改进了这个问题,但仍需要些许工作…… - Sprotty

1

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

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

0

使用最新的编译器,可以在不引入额外类型的情况下仅使用可空类型来实现RequireX方法(请参见sharplab.io):

using System;
using static Foos;

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

Foo(x); // OK, calls (1)
Foo(y); // OK, calls (2)
Foo(z); // OK, calls (3)
class Foos
{
    public static void Foo<T>(T a, T? _ = null) where T : struct => Console.WriteLine(1); // 1
    public static void Foo<T>(T? a) where T : struct => Console.WriteLine(2); // 2
    public static void Foo<T>(T a, T? _ = null) where T : class => Console.WriteLine(3); // 3
}

实际上,在第三个方法中删除第二个参数似乎也可以工作

class Foos
{
    public static void Foo<T>(T a, T? _ = null) where T : struct => Console.WriteLine(1); // 1
    public static void Foo<T>(T? a) where T : struct => Console.WriteLine(2); // 2
    public static void Foo<T>(T a) where T : class => Console.WriteLine(3); // 3
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接