Общий метод обрабатывает IEnumerable иначе, чем общий тип

Пожалуйста, проверьте следующие сегменты кода:

public interface ICountable { }
public class Counter<T>
    where T : ICountable
{
    public int Count(IEnumerable<T> items)
    {
        return 0;
    }

    public int Count(T Item)
    {
        return 0;
    }
}

public class Counter
{
    public int Count<T>(IEnumerable<T> items)
        where T : ICountable
    {
        return 0;
    }

    public int Count<T>(T Item)
        where T : ICountable
    {
        return 0;
    }
}

Две версии счетчика отличаются только спецификацией общего параметра. Один из них определяет как типичный параметр типа, другой - как общий аргумент. Оба ограничивают аргументы метода для реализации интерфейса ICountable. Я буду называть их конкретными и неспецифическими соответственно.

Теперь я определяю класс, который реализует интерфейс ICountable, и набор экземпляров:

public class CItem : ICountable { }
var countables = new List<CItem>();

Затем я хотел бы использовать оба класса Counter в коллекции.

var specific = new Counter<CItem>();
var nonspecific = new Counter();

specific.Count(countables);
nonspecific.Count(countables);

Конкретный счетчик распознает, что коллекция счетчиков должна попадать в подпись int Count (IEnumerable), но неконкретная версия этого не делает. Я получаю сообщение об ошибке:

Тип 'System.Collections.Generic.List<CItem>' не может использоваться как тип типа T 'в общем типе или методе Counter.Count<T>(T). Нет никакого неявного преобразования ссылок из List<CItem> 'до ICountable.

Похоже, что неспецифическая версия использует неправильную подпись для коллекции.

Почему они ведут себя по-другому? Как указать конкретную версию, чтобы вести себя так же, как и другая?

Примечание. Я знаю, что этот пример нереалистичен. Однако я столкнулся с этой проблемой в довольно сложном сценарии с методами расширения. Я использую эти классы для простоты

Заранее спасибо

Ответ 1

Проблема с неспецифическим классом заключается в том, что компилятор не знает тип T во время компиляции, поэтому не может выбрать правильную перегрузку для метода Count<T>(). Однако, если вы задаете общие ограничения типов, компилятор теперь знает, какого типа ожидать...

Если вы закомментируете свой метод с подписью public int Count<T>(T Item), он скомпилируется, потому что он будет использовать метод с правильной подписью (которая public int Count<T>(IEnumerable<T> items))

Он также будет компилироваться и запускаться, если вы поможете компилятору вывести тип, внеся личный список в IEnumerable<CItem> явно:

nonspecific.Count(countables as IEnumerable<CItem>);

Взгляните на упрощенный сценарий:

    static string A<T>(IEnumerable<T> collection)
    {
        return "method for ienumerable";
    }

    static string A<T>(T item)
    {
        return "method for single element";
    }

    static void Main(string[] args)
    {
        List<int> numbers = new List<int>() { 5, 3, 7 };
        Console.WriteLine(A(numbers));
    }

Выход: "метод для одного элемента"

Ответ 2

Если я правильно помню (попытаюсь найти ссылку в спецификации), метод T выбирается, потому что это точное соответствие для типа.

Вывод типа правильно определяет, что оба общих метода применимы, как Count<CItem>(IEnumerable<CItem> items) и Count<List<CItem>>(List<CItem> items). Однако первая потеряет в разрешении перегрузки, так как вторая более конкретна. После этого ограничения вступают в игру, поэтому вы получаете ошибку времени компиляции.

Если вы объявляете свой countables с помощью

IEnumerable<CItem> countables = new List<CItem>();

тогда выбор становится Count<CItem>(IEnumerable<CItem> items) и Count<IEnumerable<CItem>>(IEnumerable<CItem> items), а первый выигрывает разрешение перегрузки.

Ответ 3

По моему мнению, причина, по которой компилятор считает, что вы вызываете Counter.Count(T) вместо Counter.Count <T> (IEnumerable <T> ) объясняется тем, что для более позднего требуется преобразование из списка в IEnumerable. И это имеет приоритет меньше, чем использование прежней сигнатуры Counter.Count(T), которая приводит к ошибке.

Я думаю, что лучше изменить имя метода того, что принимает IEnumerble в качестве аргумента в нечто вроде CountAll. Что-то, что .NET Framework делает для List.Remove и List.RemoveAll. Это хорошая практика, чтобы сделать ваш код более конкретным, а не позволять компилятору выполнять все решения.