当传递值类型时,编译器没有调用适当的通用重载函数。

9

我有类似以下公共函数:

public static T Get<T>(this Mango m, T defaultValue = default(T)) where T : class
{
    //do something; return something;
}

public static T? Get<T>(this Mango m, T? defaultValue = default(T?)) where T : struct
{
    //do something; return something;
}

我希望能够单独处理引用类型和可空类型。它可以编译,直到我调用值类型。对于引用类型,它可以编译。

mango.Get<string>(); // compiles..
mango.Get(""); // compiles..

mango.Get<int>(); // The type 'int' must be a reference type in order to use it as 
                  // parameter 'T' in the generic type or method Get<T>(Mango, T)
//also            // The call is ambiguous between the following methods or properties: 
                  // Get<int>(Mango, int) and Get<int>(Mango, int?)

这里真正的歧义在哪里?当Tint时,它不能适当地调用结构体重载吗? 另外:

mango.Get<int>(0);  // The type 'int' must be a reference type in order to use it as 
                    // parameter 'T' in the generic type or method Get<T>(Mango, T)

为什么编译器只检测引用类型的重载?我尝试着创建了两个不同的重载:

public static T Get<T>(this Mango m) where T : class
{
    return default(T);
}

public static T? Get<T>(this Mango m) where T : struct
{
    return default(T);
}

public static T Get<T>(this Mango m, T def) where T : class
{
    return default(T);
}

public static T? Get<T>(this Mango m, T? def) where T : struct
{
    return default(T);
}

问题仍然存在。显然,前两种方法在这里不能编译,因为重载仅仅基于约束条件是行不通的。
我尝试通过移除具有“class”约束的重载,只保留具有“struct”约束的那个,就像这样:
public static T? Get<T>(this Mango m, T? defaultValue = default(T?)) where T : struct
{
    //do something; return something;
}

mango.Get<int>(); // voila compiles!
mango.Get<int>(0); // no problem at all..
// but now I can't have mango.Get<string>() for instance :(

我只需要更改这两个函数的名字吗?我认为有一个统一的名称是恰当的,这样调用者只需调用Get而不必担心实现细节。

更新:如果我要避免可选参数,则 Marc 的解决方案无法使用。

mango.Get<int>(); // still wouldnt work!!

但是还有更多的魔法 :(:(
public static bool IsIt<T>(this T? obj) where T : struct
{
    return who knows;
}

public static bool IsIt<T>(this T obj) where T : class
{
    return perhaps;
}

我本来以为同样的编译器错误(在我看来)会再次困扰我。但是这一次它却能够正常工作。

Guid? g = null;
g.IsIt(); //just fine, and calls the struct constrained overload
"abcd".IsIt(); //just fine, and calls the class constrained overload

所以如果 Marc 说过载决策在约束检查之前进行,那么这次不应该也得到同样的错误吗?但是没有。为什么呢? 究竟发生了什么? :x


1
在你的帖子中间,我本来想建议只尝试结构体版本,但你已经这样做了并且编译通过了。在我看来,这是一个真正的 bug,应该报告给相关人员。 - John Willemse
1
@JohnWillemse:没有bug,请看Marc的答案。 - Daniel Hilgarth
1
我只会重命名。 - leppie
当我尝试引入这个重载时:public static V? Get<V>(this Mango m, V defaultValue) where V : struct,其中最后一个参数 defaultValue 是一个非可空值类型,那么它将无法编译,因为我的新重载被认为具有与第一个重载相同的签名(即带有 class 约束的那个)。 - Jeppe Stig Nielsen
@JeppeStigNielsen 没问题,因为重载与约束无关。它仅取决于方法名称和参数签名。 - nawfal
显示剩余2条评论
3个回答

6

在进行过载解析之后,约束检查会被执行,如果我没记错的话;重载解析似乎更偏向于第一个版本。但你可以强制使用另一个版本:

mango.Get<int>((int?)0);

甚至包括:

mango.Get((int?)0);

就我个人而言,我可能只会更改名称以避免歧义。


测试一段时间后会再联系您。 - nawfal
@MarcGravell 无论如何,也许问题是...为什么你需要一个非空的重载?非空可以隐式转换为可空。或者使用可选参数而不是重载?如果可空整数不为空,则执行某些操作,否则执行其他操作并使用非空整数。 - Matías Fidemraizer
1
有趣的是,为什么它更喜欢在mango.Get(0)上使用第一个重载。现在将方法变成非泛型并强制参数,因此编写这些重载:static void M(int? i) { }static void M(object o) { },然后调用M(0)可以将int装箱并调用object重载,或将int包装到可空类型中并调用该重载。但由于每个Nullable<int>都是一个Object,反之则不成立,因此可空重载最为“专业化”且必须优先考虑。所以M(0)转到M((int?)0),而不是M((object)0)。那么mango.Get(0)呢? - Jeppe Stig Nielsen
Marc,很遗憾你的解决方案对于mango.Get<int>()无效。你能在问题上找到我的更新吗? - nawfal

1
有趣的是,编译器会检查在方法签名中使用的泛型类型中指定的约束条件,但不会检查签名本身中的约束条件。因此,如果一个方法接受两个参数,一个是T where T : struct类型,另一个是Nullable<T>[],那么编译器将不考虑任何不是结构体的T。对于T的指定struct约束不会在评估重载时被考虑,但Nullable<T>T约束为结构体的事实会被考虑。
我真的觉得无法考虑约束条件来评估重载非常奇怪,因为可以为Nullable<T>[]参数指定默认空值,并假装该参数不存在。然而,当涉及到什么是模棱两可和什么被接受时,vb.net编译器和C#编译器似乎存在差异。

编译器将检查在方法签名中使用的泛型类型中指定的约束,但不会检查签名本身中的约束。这句话很令人困惑,但是“The method's specified struct constraint for T is not considered in evaluating overloads, but the fact that Nullable<T> constrains T to struct, is.”这句话有所帮助 :) 在我的情况下,VB是否可以正确推断重载? - nawfal
@nawfal:它并不会,但是添加一个虚拟参数,其类型包含一个类约束的泛型类型参数(就像Nullable<T>包含一个结构体约束的泛型类型参数一样)可以帮助解决问题;在两个签名除了默认为null的虚拟参数外完全相同的情况下,vb.net仍然会感到歧义,但是如果您不需要函数内的泛型类型参数,则可以将其作为Object类型的参数传递。 - supercat

0

让我试着自己回答一下。

正如马克所说,约束检查是在重载解析之后进行的,并且在之间进行

public static T Get<T>(this Mango m, T defaultValue = default(T)) where T : class
{
    //do something; return something;
}

并且

public static T? Get<T>(this Mango m, T? defaultValue = default(T?)) where T : struct
{
    //do something; return something;
}

重载决议更喜欢 class 版本。但这只有在编译器在两个类似的重载之间有选择时才会发生(没有可选参数的情况下,两个重载变得相同,忽略约束条件)。现在当应用约束条件时,对于 Get<int> 的调用将失败,因为 int 不是 class

当提供默认参数时,事情会有所改变。如果我调用

mango.Get(0);

编译器足够聪明,可以调用正确的重载函数,但是现在哪个重载函数接受 int 或者 T where T: struct?并没有这样的函数。在给定的例子中,第二个参数应该是 T? 而不是 T编译器不会自动通过为每个参数类型应用所有可用的转换来解决重载问题。这不是一个错误,只是少了一个功能而已。如果我这样做:
int? i = 0;
mango.Get(i);

它可以工作,正确的重载被调用了。这也是第二个例子中发生的事情。它能够工作是因为我提供了正确的参数。

当我调用时:

Guid? g = null;
g.IsIt();

obj 已知为 g,因此 T 等于 Guid。但如果我调用

Guid g = Guid.NewGuid();
g.IsIt();

这个不起作用,因为g现在是Guid而不是Guid?,编译器不会自动进行转换,而是必须明确告诉编译器。

我对编译器不自动进行转换的事实感到满意,因为这将对每种可能的类型进行太多计算,但C#中似乎存在一个缺陷,即约束检查未涉及重载决策。 即使我像mango.Get<int>()mango.Get<int>(0)这样提供类型,重载决策也不会优先选择struct版本并使用default(int?)作为参数defaultValue。 对我来说看起来很奇怪。


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