首页 > 解决方案 > 为什么泛型约束不能帮助编译器在具有可选参数的多态方法中做出决定?

问题描述

给定 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:类型“字符串”必须是不可为空的值类型,才能将其用作泛型类型或方法“ExtensionMethods.NullIf(T, T)”中的参数“T”

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

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

标签: c#polymorphismgeneric-constraints

解决方案


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

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

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

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扩展方法,它采用(不可为空的)值类型。它可以与NullIf采用可为值类型的类在同一个类中。但是,它也不能称为NullIf. 出于这个原因,我将再次推迟到大师赛。

不过幸运的是,通过不同的方法名称指示提升为 nullable 实际上对于更清楚地传达意图以及在 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上面的扩展方法并重命名ToNullIfNullIf. 最终,解决原始问题的是分成不同的类。

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


推荐阅读