为什么泛型约束不能帮助编译器在具有可选参数的多态方法之间做出决定?



给定 2 个静态重载方法,一个泛型方法,一个不是:

public static T? NullIf<T>(this T value, T equalsThis) 
where T : struct  // value types including enums
{                        
return EqualityComparer<T>.Default.Equals(value, equalsThis)
? (T?)null
: value;
}
public static string NullIf(this string value, string equalsThis, bool ignoreCase = false)
{
return String.Compare(value, equalsThis, ignoreCase) == 0 ? null : value;
}

测试代码:

string s = "None";
string result = s.NullIf("None");

生成编译错误,因为它首选泛型:

错误 CS0453:类型"字符串"必须是 中的不可为空的值类型 为了在泛型类型或方法中将其用作参数"T" 'ExtensionMethods.NullIf(T, T)'

如果可选的 ignoreCase 参数由调用方提供或从方法中删除,或者如果删除了受约束的泛型方法,则它确实会编译。

为什么编译器不使用 where 约束来消除泛型,因为它识别不兼容?

Jon Skeet 和 Eric Lippert 都详细介绍了编译器的行为方式、为什么它以这种方式工作等等,但我真的无法判断是否有针对这个用例的解决方案。

我在自己的一个类库中有非常相似的扩展方法(然而,我很少使用它们)。 我做的一件事不同是区分可空到可空(NullIf)与不可空到可空(ToNullIf)。 您对值类型的NullIf就是我所说的ToNullIf

假设你想要从一个通常适用于任何可为 null 类型的NullIf开始。 不能在同一类中同时使用两者,因为约束不是方法签名的一部分。 为了解决这个问题,您可以将它们放在单独的类中。

public static partial class ExtensionMethodsForValueTypes
{
// Nullable to nullable
public static T? NullIf<T>(this T? value, T? other)
where T : struct
{
return value == null || EqualityComparer<T>.Default.Equals((T)value, other) ? null : value;
}
}
public static partial class ExtensionMethodsForReferenceTypes
{
// Nullable to nullable
public static T NullIf<T>(this T value, T other)
where T : class
{
return EqualityComparer<T>.Default.Equals(value, other) ? null : value;
}
}

编译器将为引用类型和可为空值类型选择正确的方法,就像 Jon Skeet 和 Eric Lippert 在各自的博客中描述的那样。

我上面提到的区别包括一个ToNullIf扩展方法,该方法采用(不可为空的)值类型。 它可以与采用可为 null值类型的NullIf位于同一类中。 但是,它也不能称为NullIf。 出于原因,我将再次服从大师赛。

不过,幸运的是,通过不同的方法名称指示提升到可为空实际上对于更清楚地传达意图非常有用,以及在 IDE 中向您传达提示,例如当 IntelliSense 不显示纯值类型的NullIf或可空值类型的ToNullIf时。 然而,由于IntelliSense在VS 2017中的部分匹配,键入"NullIf"将显示ToNullIf是否可用。

partial class ExtensionMethodsForValueTypes
{
// Non-nullable to nullable
public static T? ToNullIf<T>(this T value, T other)
where T : struct
{
return EqualityComparer<T>.Default.Equals(value, other) ? (T?)null : value;
}
}

如果要在采用引用类型的NullIf之上添加字符串专用化,则可以,但如果没有至少一个非默认参数来将其与调用站点上的泛型版本区分开来,则不能使用默认参数。 在您的情况下,您需要提供两个重载。 没有ignoreCase参数的重载会阻止选择NullIf<string>,因为前者是更具体的类型匹配。带有ignoreCase参数的参数可为您提供所需的不区分大小写。

partial class ExtensionMethodsForReferenceTypes
{
public static string NullIf(this string value, string other) => NullIf(value, other, false);
public static string NullIf(this string value, string other, bool ignoreCase)
{
return String.Compare(value, equalsThis, ignoreCase) == 0 ? null : value
}
}

如果您对可空到可为空情况的方法名称中的引用类型和可为空值类型之间的奇偶校验不感兴趣,则没有理由不能删除上面的ExtensionMethodsForValueTypes.NullIf扩展方法并将ToNullIf重命名为NullIf。 最终,是分成不同的类解决了原始问题。

最后一点:在 C# 8.0 中,所有这些都没有考虑可为空和不可为空的引用类型,部分原因是它是新的,部分原因是根本无法区分,或者,如果可以,它需要完全不同的技术。

最新更新