Почему C # не выводит мои общие типы? - PullRequest
62 голосов
/ 15 декабря 2011

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

Может кто-нибудь сказать мне, является ли компилятор немного глупым в этом случае, или есть очень четкая причина, почему он не может вывести мои общие аргументы?

Вот код:

Классы и определения интерфейса:

interface IQuery<TResult> { }

interface IQueryProcessor
{
    TResult Process<TQuery, TResult>(TQuery query)
        where TQuery : IQuery<TResult>;
}

class SomeQuery : IQuery<string>
{
}

Некоторый код, который не компилируется:

class Test
{
    void Test(IQueryProcessor p)
    {
        var query = new SomeQuery();

        // Does not compile :-(
        p.Process(query);

        // Must explicitly write all arguments
        p.Process<SomeQuery, string>(query);
    }
}

Почему это? Что мне здесь не хватает?

Вот сообщение об ошибке компилятора (оно не оставляет много для нашего воображения):

Аргументы типа для метода IQueryProcessor.Process (TQuery) не могут быть выведены из использования. Попробуйте указать введите аргументы явно.

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

  1. Я предоставляю объект, который реализует IQuery<TResult>.
  2. Это только версия IQuery<TResult>, которую реализует тип: IQuery<string> и, следовательно, TResult должен быть string.
  3. С этой информацией у компилятора есть TResult и TQuery.

РЕШЕНИЕ

Для меня лучшим решением было изменить интерфейс IQueryProcessor и использовать динамическую типизацию в реализации:

public interface IQueryProcessor
{
    TResult Process<TResult>(IQuery<TResult> query);
}

// Implementation
sealed class QueryProcessor : IQueryProcessor {
    private readonly Container container;

    public QueryProcessor(Container container) {
        this.container = container;
    }

    public TResult Process<TResult>(IQuery<TResult> query) {
        var handlerType =
            typeof(IQueryHandler<,>).MakeGenericType(query.GetType(), typeof(TResult));
        dynamic handler = container.GetInstance(handlerType);
        return handler.Handle((dynamic)query);
    }
}

Интерфейс IQueryProcessor теперь принимает параметр IQuery<TResult>. Таким образом, он может вернуть TResult, и это решит проблемы с точки зрения потребителя. Нам нужно использовать отражение в реализации, чтобы получить реальную реализацию, поскольку нужны конкретные типы запросов (в моем случае). Но тут на помощь приходит динамическая типизация, которая сделает отражение для нас. Вы можете прочитать больше об этом в этой статье .

Ответы [ 7 ]

51 голосов
/ 15 декабря 2011

Группа людей указала, что C # не делает выводов на основе ограничений. Это правильно и актуально для вопроса. Выводы сделаны путем изучения аргументов и их соответствующих формальных типов параметров , и это единственный источник информации о выводах.

К этой статье присоединилась группа людей:

http://blogs.msdn.com/b/ericlippert/archive/2007/11/05/c-3-0-return-type-inference-does-not-work-on-member-groups.aspx

Эта статья устарела и не имеет отношения к вопросу. Он устарел, потому что описывает проектное решение, принятое нами в C # 3.0, которое мы затем изменили в C # 4.0, в основном на основе ответа на эту статью. Я только что добавил обновление на эту тему в статью.

Это не имеет значения, потому что статья о выводе типа возвращаемого значения из аргументов группы методов в общие формальные параметры делегата . Это не та ситуация, о которой спрашивает оригинальный плакат.

Соответствующая моя статья, которую я хочу прочитать, это:

http://blogs.msdn.com/b/ericlippert/archive/2009/12/10/constraints-are-not-part-of-the-signature.aspx

ОБНОВЛЕНИЕ: Я слышал новость о том, что C # 7.3 немного изменил правила применения ограничений, сделав вышеописанную статью десятилетней давности более не точной. Когда у меня будет время, я рассмотрю изменения, внесенные моими бывшими коллегами, и посмотрю, стоит ли публиковать исправления в моем новом блоге; до тех пор, будьте осторожны и посмотрите, что C # 7.3 делает на практике.

15 голосов
/ 15 декабря 2011

C # не будет выводить универсальные типы на основе возвращаемого типа универсального метода, только аргументы метода.

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

Подробнее см. Пост Эрика Липперта по теме .

11 голосов
/ 15 декабря 2011

Он не использует ограничения для вывода типов.Скорее он выводит типы (когда это возможно), а затем проверяет ограничения.

Поэтому, хотя единственно возможный TResult, который можно использовать с параметром SomeQuery, он этого не увидит.

Обратите также внимание, что для SomeQuery было бы вполне возможно также реализовать IQuery<int>, что является одной из причин, почему это ограничение для компилятора не может быть плохой идеей.

4 голосов
/ 15 декабря 2011

В спецификации это довольно четко изложено:

Раздел 7.4.2 Вывод типа

Если предоставленное количество аргументов отличается от количества параметров вметод, тогда вывод немедленно завершается неудачей. В противном случае предположим, что универсальный метод имеет следующую сигнатуру:

Tr M (T1 x1… Tm xm)

С вызовом метода видаM (E1… Em) задача вывода типа состоит в том, чтобы найти уникальные аргументы типа S1… Sn для каждого из параметров типа X1… Xn, чтобы вызов M (E1… Em) стал действительным.

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

Компилятор не просто предполагает, что вы хотели string в качестве аргумента TResult, но и не может.Представьте себе TResult, полученное из строки.Оба будут действительны, так что выбрать?Лучше быть явным.

2 голосов
/ 05 декабря 2018

На почему был получен хороший ответ, но есть альтернативное решение. Я регулярно сталкиваюсь с одними и теми же проблемами, однако dynamic или любое решение, использующее отражение или распределение данных, в моем случае не подлежит обсуждению (радость от видеоигр ...)

Таким образом, вместо этого я передаю возвращаемое значение в виде out параметров, которые затем правильно выводятся.

interface IQueryProcessor
{
     void Process<TQuery, TResult>(TQuery query, out TResult result)
         where TQuery : IQuery<TResult>;
}

class Test
{
    void Test(IQueryProcessor p)
    {
        var query = new SomeQuery();

        // Instead of
        // string result = p.Process<SomeQuery, string>(query);

        // You write
        string result;
        p.Process(query, out result);
    }
}

Единственный недостаток, который я могу вспомнить, это то, что он запрещает использование 'var'.

1 голос
/ 21 февраля 2019

Я больше не буду вдаваться в причину, у меня нет иллюзий, что я смогу объяснить лучше, чем Эрик Липперт.

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

Прежде всего, измените IQuery, чтобы сделать его самоссылочным:

public interface IQuery<TQuery, TResult> where TQuery: IQuery<TQuery, TResult>
{
}

Ваш IQueryProcessor будет выглядеть так:

public interface IQueryProcessor
{
    Task<TResult> ProcessAsync<TQuery, TResult>(IQuery<TQuery, TResult> query)
        where TQuery: IQuery<TQuery, TResult>;
}

Фактический тип запроса:

public class MyQuery: IQuery<MyQuery, MyResult>
{
    // Neccessary query parameters
}

Реализация процессора может выглядеть следующим образом:

public Task<TResult> ProcessAsync<TQuery, TResult>(IQuery<TQuery, TResult> query)
    where TQuery: IQuery<TQuery, TResult>
{
    var handler = serviceProvider.Resolve<QueryHandler<TQuery, TResult>>();
    // etc.
}
0 голосов
/ 18 февраля 2019

Другим обходным решением этой проблемы является добавление дополнительного параметра для разрешения типа. Например, мы можем добавить следующее расширение:

static class QueryProcessorExtension
{
    public static TResult Process<TQuery, TResult>(
        this IQueryProcessor processor, TQuery query,
        //Additional parameter for TQuery -> IQuery<TResult> type resolution:
        Func<TQuery, IQuery<TResult>> typeResolver)
        where TQuery : IQuery<TResult>
    {
        return processor.Process<TQuery, TResult>(query);
    }
}

Теперь мы можем использовать это расширение следующим образом:

void Test(IQueryProcessor p)
{
    var query = new SomeQuery();

    //You can now call it like this:
    p.Process(query, x => x);
    //Instead of
    p.Process<SomeQuery, string>(query);
}

Что далеко от идеала, но намного лучше, чем явное предоставление типов.

P.S. Ссылки по этой теме в репозитории dotnet:

https://github.com/dotnet/csharplang/issues/997

https://github.com/dotnet/roslyn/pull/7850

...