Почему общее ограничение не помогает компилятору выбирать среди полиморфных методов с необязательным аргументом? - PullRequest
2 голосов
/ 18 апреля 2019

Имеется 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: тип 'строка' должен иметь тип значения, не допускающий значения NULL, чтобы использовать его в качестве параметра 'T' в универсальном типе или методе 'ExtensionMethods.NullIf (T, T)'

Он компилируется, если необязательный аргумент ignoreCase предоставлен вызывающей стороной или удален из метода или если удаленный ограниченный универсальный метод удален.

Почему компилятор не использует ограничение where для исключения универсального из рассмотрения, так как он распознает несовместимость?

1 Ответ

1 голос
/ 18 апреля 2019

И Джон Скит, и Эрик Липперт подробно рассказывают о том, как ведет себя компилятор, почему он работает так, как он работает, и так далее, но я не могу точно сказать, есть ли решение для этого варианта использования во всем этом .

У меня очень похожие методы расширения в одной из моих собственных библиотек классов (и тем не менее, я редко использую их). Одна вещь, которую я сделал по-другому, состояла в том, чтобы различать , обнуляемый и обнуляемый (NullIf), и , не обнуляемый и обнуляемый (ToNullIf). Ваш NullIf для типов значений - это то, что я назвал ToNullIf.

Допустим, вы хотите начать с 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;
    }
}

Компилятор выберет правильный метод для ссылочных типов и типов значений, допускающих значение NULL, так, как это описывают Джон Скит и Эрик Липперт в своих соответствующих блогах.

Различие, которое я упомянул выше, включает метод расширения 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
    }
}

Если вас не интересует соотношение между ссылочными типами и типами значений, допускающими значение NULL, в названии методов для случая от Nullable до Nullable , нет никаких причин, по которым вы не можете удалить расширение ExtensionMethodsForValueTypes.NullIf Способ выше и переименуйте ToNullIf в NullIf. В конечном счете, именно разделение на различные классы решает исходную проблему.

Последнее замечание: Ничто из этого не учитывает обнуляемые и ненулевые ссылочные типы в C # 8.0, отчасти потому, что оно новое, а отчасти потому, что различие просто может не может быть сделано или, если это возможно, требует совершенно другой техники.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...