Подтвердить что ты не робот

Вывод типа с классом, реализующим несколько интерфейсов иерархии

В качестве примера можно использовать нечто вроде калькулятора с элементами разных типов, функции, которые оценивают для разных типов элементов, и контекст для хранения элементов и запуска функций. Интерфейсы выглядят примерно так:

public interface IElement {
}
public interface IChildElement : IElement {
    double Score { get; }
}
public interface IGrandchildElement : IChildElement {
    int Rank { get; }
}

public interface IFunction<Tout, in Tin> where Tin : IElement {
    Tout Evaluate(Tin x, Tin y);
}

public interface IContext<Tin> where Tin : IElement {
    Tout Evaluate<Tout>(string x, string y, IFunction<Tout, Tin> eval);
}

Обратите внимание, что функции могут возвращать произвольные типы. Ниже приведена фиктивная реализация, где у меня есть функция с именем Foo, которая может использоваться как для IChildElement, так и для IGrandchildElement, и возвращает double в обоих случаях:

public class ChildElement : IChildElement {
    public double Score { get; internal set; }
}
public class GrandchildElement : ChildElement, IGrandchildElement {
    public int Rank { get; internal set; }
}

public class Foo : IFunction<double, IChildElement>, IFunction<double, IGrandchildElement> {
    public double Evaluate(IChildElement x, IChildElement y) {
        return x.Score / y.Score;
    }
    public double Evaluate(IGrandchildElement x, IGrandchildElement y) {
        return x.Score * x.Rank / y.Score / y.Rank;
    }
}

public class Context<T> : IContext<T> where T : IElement {
    protected Dictionary<string, T> Results { get; set; }

    public Context() {
        this.Results = new Dictionary<string, T>();
    }

    public void AddElement(string key, T e) {
        this.Results[key] = e;
    }
    public Tout Evaluate<Tout>(string x, string y, IFunction<Tout, T> eval) {
        return eval.Evaluate(this.Results[x], this.Results[y]);
    }
}

Пример выполнения примера:

Context<IChildElement> cont = new Context<IChildElement>();
cont.AddElement("x", new ChildElement() { Score = 1.0 });
cont.AddElement("y", new ChildElement() { Score = 2.0 });
Foo f = new Foo();
double res1 = cont.Evaluate("x", "y", f); // This does not compile
double res2 = cont.Evaluate<double>("x", "y", f); // This does

Как вы можете видеть, моя проблема в том, что мне, похоже, нужно жестко набрать вызов Context.Evaluate. Если я этого не сделаю, компилятор говорит, что не может вывести тип аргументов. Это особенно поразительно для меня, поскольку в обоих случаях функция Foo возвращает double.

Если Foo реализует только IFunction<double, IChildElement> или IFunction<double, IGrandchildElement>, у меня нет этой проблемы. Но это так.

Я не понимаю. Я имею в виду, что добавление <double> не различает IFunction<double, IGrandchildElement> и IFunction<double, IChildElement>, потому что они оба возвращают double. Насколько я понимаю, он не дает компилятору никакой дополнительной информации, чтобы различать.

В любом случае, есть ли способ избежать жесткого ввода всех вызовов на Task.Evaluate? В реальном мире у меня есть несколько функций, поэтому быть в состоянии избежать этого было бы здорово.

Bounty для объяснения причины, почему добавление <double> помогает компилятору. Это проблема с компилятором, слишком ленивым, так сказать?

Старое обновление: использование делегатов

Опцией может быть использование делегатов вместо IFunction в IContext.Evaluate:

public interface IContext<Tin> where Tin : IElement {
    Tout Evaluate<Tout>(string x, string y, Func<Tin, Tin, Tout> eval);
}
public class Context<T> : IContext<T> where T : IElement {
    // ...
    public Tout Evaluate<Tout>(string x, string y, Func<T, T, Tout> eval) {
        return eval(this.Results[x], this.Results[y]);
    }
}

Таким образом, при вызове IContext.Evaluate нам не нужно печатать <double>:

Foo f = new Foo();
double res1 = cont.Evaluate("x", "y", f.Evaluate); // This does compile now
double res2 = cont.Evaluate<double>("x", "y", f.Evaluate); // This still compiles

Итак, компилятор работает так, как ожидалось. Мы избегаем необходимости жесткого типа, но мне не нравится тот факт, что мы используем IFunction.Evaluate вместо самого объекта IFunction.

4b9b3361

Ответ 1

(Я не прошел через версию делегатов. Я решил, что этот ответ уже достаточно длинный...)

Давайте начнем с упрощения кода. Вот короткий, но полный пример, который все еще демонстрирует проблему, но устраняет все неуместное. Я также изменил порядок аргументов типа в IFunction только для соответствия более нормальным соглашениям (например, Func<T, TResult>):

// We could even simplify further to only have IElement and IChildElement...
public interface IElement {}
public interface IChildElement : IElement {}
public interface IGrandchildElement : IChildElement {}

public interface IFunction<in T, TResult> where T : IElement
{
    TResult Evaluate(T x);
}

public class Foo : IFunction<IChildElement, double>,
                   IFunction<IGrandchildElement, double>
{
    public double Evaluate(IChildElement x) { return 0; }
    public double Evaluate(IGrandchildElement x) { return 1; }
}

class Test
{
    static TResult Evaluate<TResult>(IFunction<IChildElement, TResult> function)
    {
        return function.Evaluate(null);
    }

    static void Main()
    {
        Foo f = new Foo();
        double res1 = Evaluate(f);
        double res2 = Evaluate<double>(f);
    }
}

У этой проблемы все еще есть та же проблема:

Test.cs(27,23): error CS0411: The type arguments for method
        'Test.Evaluate<TResult>(IFunction<IChildElement,TResult>)' cannot be
        inferred from the usage. Try specifying the type arguments explicitly.

Теперь о том, почему это происходит... проблема - это вывод типа, как говорили другие. Механизм вывода типа в С# (как на С# 3) довольно хорош, но он не настолько мощный, насколько это возможно.

Посмотрите, что происходит в части вызова метода, со ссылкой на спецификацию языка С# 5.

7.6.5.1 (вызовы метода) - важная часть здесь. Первый шаг:

Создан набор методов-кандидатов для вызова метода. Для каждого метода F, связанного с группой методов M:

  • Если F не является общим, F является кандидатом, когда:
    • M не имеет списка аргументов типа и
    • F применимо относительно A (§7.5.3.1).
  • Если F является общим и M не имеет списка аргументов типа, F является кандидатом, когда:
    • Вывод типа (§7.5.2) преуспевает, выводя список аргументов типа для вызова и
    • Когда аргументы inferred type заменяются соответствующими параметрами типа метода, все построенные типы в списке параметров F удовлетворяют их ограничениям (§4.4.4), а список параметров F применим относительно A (§ 7.5.3.1).
  • Если F является общим и M содержит список аргументов типа, F является кандидатом, когда:
    • F имеет такое же количество параметров типа метода, как и в списке аргументов типа, и
    • После того, как аргументы типа заменяются соответствующими параметрами типа метода, все построенные типы в списке параметров F удовлетворяют их ограничениям (§4.4.4), а список параметров F применим относительно A (§ 7.5.3.1).

Теперь здесь группа методов M представляет собой набор с единственным методом (Test.Evaluate) - к счастью, раздел 7.4 (поиск элемента) прост. Поэтому у нас есть только один метод F.

Это общий, и M не имеет списка аргументов типа, поэтому мы закончим прямо в разделе 7.5.2 - вывод типа. Обратите внимание, что если список аргументов отсутствует, это полностью пропущено, и третья основная отметка выше, поэтому вызов Evaluate<double>(f) завершается успешно.

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

7.5.2 сама по себе в основном просто описание, включая тот факт, что вывод типа происходит поэтапно.

Общий метод, который мы пытаемся вызвать, описывается как:

Tr M<X1...Xn>(T1 x1 ... Tm xm)

и вызов метода описывается как:

M(E1 ... Em)

Итак, в нашем случае мы имеем:

  • T r is TResult, который совпадает с X 1.
  • T 1 is IFunction<IChildElement, TResult>
  • x 1 is function, параметр значения
  • E 1 is F, который имеет тип Foo

Теперь попробуйте применить это для остального вывода типа...

7.5.2.1 Первая фаза
Для каждого из аргументов метода E i:

  • Если E i является анонимной функцией, то явный вывод типа параметра (§7.5.2.7) производится из E i в T i
  • В противном случае, если E i имеет тип U и x i - это параметр значения, то вывод с нижней границей делается из U в T iсуб > .
  • В противном случае, если E i имеет тип U и x i является параметром ref или out, то точный вывод делается из U в T iсуб > .
  • В противном случае для этого аргумента не делается никаких указаний.

Здесь важна вторая маркерная точка: E 1 не является анонимной функцией, E 1 имеет тип Foo и x 1 - параметр значения. Таким образом, мы получаем вывод с нижней границей от Foo до T 1. Этот вывод с нижней границей описан в 7.5.2.9. Важная часть здесь:

В противном случае устанавливает U 1... U k и V 1... V k являются определяется путем проверки того, применяется ли какой-либо из следующих случаев:

  • [...]
  • V - построенный класс, структура, интерфейс или делегат типа C < V 1... V k > и существует уникальный тип C < U 1... U k > gt; такой, что U (или, если U является параметром типа, его эффективным базовым классом или любым членом его эффективного набора интерфейсов) идентичен, наследуется от (прямо или косвенно) или реализует (прямо или косвенно) C < U 1суб > ... U <суб > ксуб → . (Ограничение "единственности" означает, что в случае интерфейса C <T> {} класс U: C <X> , C <Y> {}, то не делается никаких выводов при выводе из U в C <T> поскольку U 1 может быть X или Y.)

Для целей этой части U есть Foo, а V - IFunction<IChildElement, TResult>. Однако Foo реализует как IFunction<IChildElement, double>, так и IFunction<IGrandchildelement, double>. Поэтому, хотя в обоих случаях мы закончили с U 2 как double, этот пункт не выполняется.

Одна вещь, которая меня удивляет, заключается в том, что она не полагается на контравариантность T in IFunction<in T, TResult>. Мы получаем ту же проблему, если удалим часть in. Я бы ожидал, что он будет работать в этом случае, так как не было бы перехода от IFunction<IGrandchildElement, TResult> к IFunction<IChildElement, TResult>. Возможно, эта часть является ошибкой компилятора, но, скорее всего, я неправильно понимаю спецификацию. Однако в случае, если это действительно дано, это не имеет значения - из-за контравариантности T существует такое преобразование, поэтому оба интерфейса действительно значительны.

В любом случае, это означает, что на самом деле мы не получаем вывода какого-либо типа из этого аргумента!

Это вся первая фаза.

Вторая фаза описывается следующим образом:

7.5.2.2 Вторая фаза

Вторая фаза выполняется следующим образом:

  • Все незафиксированные переменные типа Xi, не зависящие от (п. 7.5.5.5), фиксированы (X7.2.2.10).
  • Если таких переменных типа не существует, все незафиксированные переменные типа Xi фиксированы, для которых выполняются все следующие условия:
    • Существует хотя бы одна переменная типа Xj, которая зависит от Xi
    • Xi имеет непустое множество оценок
  • Если такие переменные типа не существуют и существуют все еще нефиксированные переменные типа, запрос типа терпит неудачу.
  • В противном случае, если никаких дополнительных переменных нефиксированного типа не существует, вывод типа завершается успешно.
  • В противном случае для всех аргументов Ei с соответствующим типом параметра Ti, где выходные типы (§7.5.2.4) содержат незафиксированные переменные типа Xj, но входные типы (§7.5.2.3) не являются выходным типом вывода (§7.5. 2.6) производится из Ei в Ti. Затем повторяется вторая фаза.

Я не буду копировать все подпункты, но в нашем случае...

  • Переменная типа X 1 не зависит от каких-либо других переменных типа, поскольку нет никаких других переменных типа. Поэтому нам нужно исправить X 1. (Ссылка здесь неверна - на самом деле это должно быть 7.5.2.11. Я позволю Мадзу знать.)

У нас нет границ для X 1 (потому что предыдущий выше вывод не помог), поэтому мы заканчиваем неудачный вывод типа в этой точке. Взрыва. Все это зависит от части уникальности в 7.5.2.9.

Конечно, это можно исправить. Часть вывода типа спецификации может быть сделана более мощной - проблема в том, что это также усложнило бы ее, в результате чего:

  • Разработчикам сложнее рассуждать о типе вывода (это достаточно сложно, как есть!)
  • Трудно правильно указать без пробелов.
  • Сложнее реализовать правильно
  • Вполне возможно, что он имеет худшую производительность во время компиляции (что может быть проблемой в интерактивных редакторах, таких как Visual Studio, которым необходимо выполнить вывод того же типа для таких вещей, как Intellisense).

Это все балансирующий акт. Я думаю, что команда С# сделала очень хорошо - тот факт, что он не работает в угловых случаях, например, это не слишком большая проблема, IMO.

Ответ 2

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

Существует двусмысленность между IFunction<double, IChildElement> и IFunction<double, IGrandChildElement>, потому что они оба являются связываемыми.

При ошибке ввода типа вы должны явно указать аргументы типа. Это соответствует спецификации языка С#.

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

После того, как вы явно указали, что T привязан к double, больше не существует двусмысленности, так как Tin привязан к IChildElement через ваше объявление Context<IChildElement> и Tout привязан к double через аргумент явного типа.

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

Однако спецификация говорит:

Вывод типа происходит как часть обработки времени привязки вызова метода (§7.6.5.1) и имеет место перед перегрузкой шаг разрешения вызова

... поэтому, я думаю, они хотели разделить эти вещи. Причина этого выше меня. Я предполагаю, что это могла быть простота спецификации или поддержки будущих расширений или просто они не думали об этом: -)

Ответ 3

Причина, по которой это происходит, заключается в том, что Foo() реализует IFunction для IChildElement и IGrandchildElement. Поскольку ваш тип использования имеет тип IChildElement, он может ссылаться либо на IChildElement, либо на IGrandchildElement, поэтому вызов неоднозначен, так как IFunction<double, IGrandchildElement> является IFunction<double, IChildElement>. Обратите внимание, что проблема не вызвана из-за IChildElement и IGrandchildElement, но поскольку она реализует два потенциальных типа IFunction, она даже не рассматривает тип возврата double.

// f is both an IFunction<double, IGrandchildElement>
// and an IFunction<double, IChildElement>
Foo f = new Foo();
double res1 = cont.Evaluate("x", "y", f); // This does not compile
double res2 = cont.Evaluate<double>("x", "y", f); // This does

Поэтому вам нужно сделать это более конкретным образом, есть два способа сделать это, используя приведения:

double res3 = cont.Evaluate<double>("x", "y", f);
double res4 = cont.Evaluate("x", "y", (IFunction<double, IChildElement>)f);

Вы не хотите делать это каждый раз, как вы сказали, но последний метод литья показывает потенциальное решение вашей проблемы; приведение Foo к требуемому интерфейсу в переменную и использование этой переменной при вызове cont.Evaluate().

IFunction<double, IChildElement> iFunc = f;
double res5 = cont.Evaluate("x", "y", iFunc);