Общий метод обрабатывает 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;
    }
}

Две версии Counter отличаются только спецификацией универсального параметра. Один из них определяется как параметр универсального типа, другой — как универсальный аргумент. Оба ограничивают аргументы метода для реализации интерфейса 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);

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

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

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

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

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

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


person Daniel Leiszen    schedule 16.12.2015    source источник
comment
если вы явно укажете общий тип, например nonspecific.Count<CItem>(countables);, это тоже будет работать   -  person farid bekran    schedule 16.12.2015
comment
если вы определяете countables с IEnumerable<CItem> countables = new List<CItem>();, будет выбрана правильная перегрузка.   -  person SWeko    schedule 16.12.2015
comment
Две перегрузки метода Count() неоднозначны, обе могут принимать список в качестве аргумента. Однако один из них нарушает ограничение в конкретном случае, когда вы передаете список. Этого недостаточно, если вы будете использовать другой тип, который реализует как IEnumerable , так и ICountable, то это действительно неоднозначно. C# не позволяет вам объявлять общий тип/метод, который может случайно не скомпилироваться.   -  person Hans Passant    schedule 16.12.2015


Ответы (3)


Проблема с неспецифическим классом заключается в том, что компилятор не знает тип 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));
    }

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

person Fabjan    schedule 16.12.2015
comment
Да, я испытал то же самое. В моем сценарии у меня есть разные методы расширения для коллекций и отдельных экземпляров одного типа. Я ожидаю, что компилятор выберет подпись с параметром коллекции, когда я передаю коллекцию, и подпись с параметром экземпляра, когда я передаю экземпляр signle. Это нереальные ожидания? - person Daniel Leiszen; 16.12.2015
comment
@DanielLeiszen Да, потому что так работают общие параметры в C#. Компилятор не знает тип во время компиляции, только во время выполнения ... Однако, если вы устанавливаете ограничения типа, вы «сообщаете компилятору», какой тип он должен ожидать. - person Fabjan; 16.12.2015

Если я правильно помню (попробую найти ссылку в спецификации), метод 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), и первый выигрывает разрешение перегрузки.

person SWeko    schedule 16.12.2015
comment
Спасибо за сообщение. Я отметил решение Фабьяна как ответ, так как оно было первым. Но ваш пост также отвечает на вопрос. - person Daniel Leiszen; 16.12.2015
comment
@DanielLeiszen, нет проблем, это был интересный вопрос - теперь мне нужно покопаться в спецификации, чтобы понять, почему это работает именно во втором случае. - person SWeko; 16.12.2015

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

Я думаю, что лучше изменить имя метода, принимающего IEnumerble в качестве аргумента, на что-то вроде CountAll. То, что .NET framework делает для List.Remove и List.RemoveAll. Хорошей практикой является сделать ваш код более конкретным, а не позволять компилятору принимать все решения.

person fjch1997    schedule 16.12.2015