Неверное преобразование завершается с ошибкой при изменении структуры структуры на класс

Рассматриваемый класс/класс:

public struct HttpMethod
{
    public static readonly HttpMethod Get = new HttpMethod("GET");
    public static readonly HttpMethod Post = new HttpMethod("POST");
    public static readonly HttpMethod Put = new HttpMethod("PUT");
    public static readonly HttpMethod Patch = new HttpMethod("PATCH");
    public static readonly HttpMethod Delete = new HttpMethod("DELETE");

    private string _name;

    public HttpMethod(string name)
    {
        // validation of name
        _name = name.ToUpper();
    }

    public static implicit operator string(HttpMethod method)
    {
        return method._name;
    }

    public static implicit operator HttpMethod(string method)
    {
        return new HttpMethod(method);
    }

    public static bool IsValidHttpMethod(string method)
    {
        // ...
    }

    public override bool Equals(object obj)
    {
        // ...
    }

    public override int GetHashCode()
    {
        return _name.GetHashCode();
    }

    public override string ToString()
    {
        return _name;
    }
}

Следующий код вызывает проблему:

public class HttpRoute
{
    public string Prefix { get; }
    public HttpMethod[] Methods { get; }

    public HttpRoute(string pattern, params HttpMethod[] methods)
    {
        if (pattern == null) throw new ArgumentNullException(nameof(pattern));
        Prefix = pattern;
        Methods = methods ?? new HttpMethod[0];
    }

    public bool CanAccept(HttpListenerRequest request)
    {
        return Methods.Contains(request.HttpMethod) && request.Url.AbsolutePath.StartsWith(Prefix);
    }
}

Ошибка компилятора создается путем изменения структуры HttpMethod в закрытом классе. Об ошибке сообщается для return Methods.Contains(request.HttpMethod), обратите внимание: request.HttpMethod в этом случае есть string. Который производит следующее:

Error   CS1929  'HttpMethod[]' does not contain a definition for 'Contains' and the best extension method overload 'Queryable.Contains<string>(IQueryable<string>, string)' requires a receiver of type 'IQueryable<string>'

Мой вопрос - почему? Я могу перепроектировать код, чтобы заставить его работать, но я хочу знать, почему переход от структуры к закрытому классу создает эту странную ошибку.

Изменить: добавление упрощенного набора кода примера (доступно здесь: https://dotnetfiddle.net/IZ9OXg). Обратите внимание, что комментирование неявного оператора для строки второго класса позволяет компилировать код:

public static void Main()
{
    HttpMethod1[] Methods1 = new HttpMethod1[10];
    HttpMethod2[] Methods2 = new HttpMethod2[10];

    var res1 = Methods1.Contains("blah"); //works
    var res2 = Methods2.Contains("blah"); //doesn't work
}

public struct HttpMethod1
{
    public static implicit operator HttpMethod1(string method)
    {
        return new HttpMethod1();
    }

    public static implicit operator string (HttpMethod1 method)
    {
        return "";
    }

}

public class HttpMethod2
{
    public static implicit operator HttpMethod2(string method)
    {
        return new HttpMethod2();
    }

    //Comment out this method and it works fine
    public static implicit operator string (HttpMethod2 method)
    {
        return "";
    }

}

Ответ 1

Вещи, которые я знаю:

  • Обычно проблема заключается в выводе типа.
  • В первом случае T выводится как HttpMethod1.
  • В случае структуры нет преобразования от HttpMethod1[] до IEnumerable<string>, потому что ковариация работает только с ссылочными типами.
  • В случае класса нет преобразования от HttpMethod2[] до IEnumerable<string>, потому что ковариация работает только с ссылочными преобразованиями, и это определяемое пользователем преобразование.

Вещи, которые я подозреваю, но необходимо подтвердить:

  • Что-то о небольшой разнице между моими последними двумя точками сбивает с толку алгоритм вывода типа.

ОБНОВЛЕНИЕ:

  • Это не имеет никакого отношения к ковариантным преобразованиям массива. Проблема повторяется даже без преобразования массива.
  • Однако он имеет отношение к ковариантным преобразованиям интерфейсов.
  • Это не имеет ничего общего со строками. (Строки часто немного странные, потому что они имеют трудно запоминающееся преобразование в IEnumerable<char>, которое иногда испортило вывод типа.)

Здесь фрагмент программы, который отображает проблему; обновите свои преобразования для преобразования в C вместо строки:

public interface IFoo<out T> {}
public class C {}
public class Program
{
    public static bool Contains<T>(IFoo<T> items, T item) 
    {
        System.Console.WriteLine(typeof(T));
        return true; 
    }
    public static void Main()
    {
        IFoo<HttpMethod1> m1 = null;
        IFoo<HttpMethod2> m2 = null;
        var res1 = Contains(m1, new C()); //works
        var res2 = Contains(m2, new C()); //doesn't work
    }
    }

Это выглядит как возможная ошибка в выводе типа, и если это так, это моя ошибка; много извинений, если это так. К сожалению, у меня нет времени заглядывать в нее сегодня. Возможно, вы захотите открыть проблему в github и попросить кого-то, кто все еще делает это, заглянуть в нее. Мне было бы интересно узнать, что получилось, и если это окажется ошибкой в ​​дизайне или реализации алгоритма вывода.

Ответ 2

Во-первых, это наблюдаемая поведенческая разница между структурами и классами. Тот факт, что вы "запечатали" ваш класс, не влияет на результат в этом сценарии.

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

string method = HttpMethods[0];

Работа с массивами вводит некоторые менее понятные нюансы компилятора.

ковариация

Когда HttpMethod является классом (ссылочным типом), с таким массивом, как HttpRoute.HttpMethods Array covariance (12.5 С# 5.0 Language Spec) вступает в игру, что позволяет обрабатывать HttpMethod [x] как объект. Ковариация будет уважать встроенные неявные ссылочные преобразования (например, наследование типа или преобразование в объект), и оно будет уважать явные операторы, но оно не будет уважать или искать пользовательские неявные операторы. (Хотя немного двусмысленно, в фактических спецификациях перечислены специфические по умолчанию неявные операторы и явные операторы, он не упоминает определяемые пользователем операторы, но, поскольку все остальное так сильно указано, вы можете сделать вывод, что определенные пользователем операторы не поддерживаются.)

В основном ковариация имеет приоритет над многими оценками общего типа. Подробнее об этом через мгновение.

Ковариация массивов конкретно не распространяется на массивы типов значений. Например, никакого преобразования не существует, что позволяет использовать int [] как объект [].

Поэтому, когда HttpMethod является структурой (тип значения), ковариация больше не является проблемой и будет применяться следующее общее расширение из пространства имен System.Linq:

public static bool Contains<TSource>(this IEnumerable<TSource> source, TSource value);

Поскольку вы передали в компаратор строк, оператор Contains будет оцениваться следующим образом:

public static bool Contains<string>(this IEnumerable<string> source, string value);

Когда HttpMethod является классом (ссылочным типом), благодаря ковариации, HttpMethod [] в его текущей форме сопоставляется только с Object [] и, следовательно, IEnumerable, но не IEnumerable <T> , Почему бы и нет? потому что компилятор должен иметь возможность определять тип для генерации общей реализации IEnumerable <T> и определить, может ли он выполнять явное преобразование из объекта в T. Другими словами, компилятор не может определить, может ли T определенно быть String или нет, поэтому он не находит совпадения в методах расширения Linq, которые мы ожидали.

И что вы можете с этим поделать? (! Не это!) Первой обычной попыткой может быть попытка использования .Cast <string> (), чтобы отбросить экземпляры HttpMethod к строкам для сравнения:

return HttpMethods.Cast<string>().Contains(request.Method) && request.Url.AbsolutePath.StartsWith(Prefix);

Вы обнаружите, что это не сработает. Несмотря на то, что параметр для Cast <T> имеет тип IEnumerable, а не IEnumerable <T> . Он предоставляется, чтобы вы могли использовать старые коллекции, которые не реализуют общую версию IEnumerable с LINQ. Литой <T> предназначен только для преобразования не общих объектов в их "истинный" тип посредством процесса оценки общих истоков для ссылочных типов или Un-Boxing для типов значений. Если Бокс и Unboxing (Руководство по программированию на С#) применяется только к типам значений (structs), и поскольку наш тип HttpMethod является ссылочным типом (классом), только обычное происхождение между HttpMethod и String является объектом. В HttpMethod нет неявного или даже явного оператора, который принимает Object и, поскольку он не является типом значения, нет встроенного оператора un-box, который может использовать компилятор.

Обратите внимание, что этот Cast < > не работает во время выполнения в этом сценарии, когда HttpMethod является типом значения (класса), который компилятор будет рад позволить ему создавать.

Окончательное решение

Вместо Cast <T> или полагаясь на неявные преобразования, нам нужно будет принудительно включить элементы в массиве HttpMethods в строку (это все равно будет использовать неявный оператор!), Но Linq снова делает эту тривиальную, но необходимую задачу:

return HttpMethods.Select(c => (string)c).Contains(request.Method) && request.Url.AbsolutePath.StartsWith(Prefix);