Аргумент типа универсального типа С#, не выводимый из использования
Недавно я экспериментировал с реализацией шаблона посетителя, где я пытался применять методы Accept & Visit с помощью общих интерфейсов:
public interface IVisitable<out TVisitable> where TVisitable : IVisitable<TVisitable>
{
TResult Accept<TResult>(IVisitor<TResult, TVisitable> visitor);
}
Цель -whose состоит в том, чтобы 1) отметить определенный тип "Foo" как посетимый таким посетителем, который, в свою очередь, является "посетителем такого типа Foo" и 2) применяет метод Accept правильной подписи к типу реализации, доступному для посещения, например так:
public class Foo : IVisitable<Foo>
{
public TResult Accept<TResult>(IVisitor<TResult, Foo> visitor) => visitor.Visit(this);
}
Пока что так хорошо, интерфейс посетителя:
public interface IVisitor<out TResult, in TVisitable> where TVisitable : IVisitable<TVisitable>
{
TResult Visit(TVisitable visitable);
}
-should 1) отметьте посетителя как "способного посетить" TVisitable 2), какой тип результата (TResult) для этого TVisitable должен быть 3) принудительно использовать метод Visit для правильной подписи для каждого TVisitable. ", вот так:
public class CountVisitor : IVisitor<int, Foo>
{
public int Visit(Foo visitable) => 42;
}
public class NameVisitor : IVisitor<string, Foo>
{
public string Visit(Foo visitable) => "Chewie";
}
Довольно приятно и красиво, это позволяет мне написать:
var theFoo = new Foo();
int count = theFoo.Accept(new CountVisitor());
string name = theFoo.Accept(new NameVisitor());
Отлично.
Теперь начинаются печальные времена, когда я добавляю еще один доступный тип, например:
public class Bar : IVisitable<Bar>
{
public TResult Accept<TResult>(IVisitor<TResult, Bar> visitor) => visitor.Visit(this);
}
который можно посетить, скажем, только CountVisitor
:
public class CountVisitor : IVisitor<int, Foo>, IVisitor<int, Bar>
{
public int Visit(Foo visitable) => 42;
public int Visit(Bar visitable) => 7;
}
который внезапно прерывает вывод типа в методе Accept! (это разрушает весь дизайн)
var theFoo = new Foo();
int count = theFoo.Accept(new CountVisitor());
давая мне:
"Аргументы типа для метода 'Foo.Accept<TResult>(IVisitor<TResult, Foo>)'
не могут быть выведены из использования."
Может ли кто-нибудь объяснить, почему это так? Существует только одна версия IVisitor<T, Foo>
которую реализует CountVisitor
или, если IVisitor<T, Bar>
не может быть устранена по какой-либо причине, у обоих из них есть один и тот же T
int
, = no other тип все равно будет работать. Выдает ли вывод типа, как только есть только один подходящий кандидат? (Забавный факт: ReSharper считает int
в theFoo.Accept<int>(...)
избыточным: P, хотя он не будет компилироваться без него)
Ответы
Ответ 1
Кажется, что вывод типа работает жадным способом, сначала пытаясь сопоставить методу generic types, а затем классу generic types. Так что если вы скажете
int count = theFoo.Accept<int>(new CountVisitor());
это работает, что странно, так как Foo является единственным кандидатом для класса generic type.
Во-первых, если вы замените тип generic типа на тип второго класса, он работает:
public interface IVisitable<R, out T> where T: IVisitable<int, T>
{
R Accept(IVisitor<R, T> visitor);
}
public class Foo : IVisitable<int, Foo>
{
public int Accept(IVisitor<int, Foo> visitor) => visitor.Visit(this);
}
public class Bar : IVisitable<int, Bar>
{
public int Accept(IVisitor<int, Bar> visitor) => visitor.Visit(this);
}
public interface IVisitor<out TResult, in T> where T: IVisitable<int, T>
{
TResult Visit(T visitable);
}
public class CountVisitor : IVisitor<int, Foo>, IVisitor<int, Bar>
{
public int Visit(Foo visitable) => 42;
public int Visit(Bar visitable) => 7;
}
class Program {
static void Main(string[] args) {
var theFoo = new Foo();
int count = theFoo.Accept(new CountVisitor());
}
}
Во-вторых (и это странная часть, которая подчеркивает, как работает вывод типа), посмотрите, что произойдет, если вы замените int
на string
в посетителе Bar
:
public class CountVisitor : IVisitor<int, Foo> , IVisitor<string, Bar>
{
public int Visit(Foo visitable) => 42;
public string Visit(Bar visitable) => "42";
}
Во-первых, вы получаете ту же ошибку, но смотрите, что произойдет, если вы нажмете строку:
int count = theFoo.Accept<string>(new CountVisitor());
ошибка CS1503: аргумент 1: невозможно преобразовать из 'CountVisitor'
в 'IVisitor<string, Foo>'
Это говорит о том, что компилятор сначала рассматривает методы общих типов (TResult
в вашем случае) и не работает сразу, если находит больше кандидатов. Он даже не смотрит дальше, на классы общих типов.
Я попытался найти спецификацию вывода типа из Microsoft, но не смог найти ее.
Ответ 2
Выдает ли вывод типа, как только есть только один подходящий кандидат?
Да, в этом случае. При попытке вывести параметр общего типа метода (TResult
), алгоритм вывода типа оказывается неудачным на CountVisitor
имеющем два вывода типа IVisitor<TResult, TVisitable>
.
Из спецификации С# 5 (самое последнее, что я мог найти), §7.5.2:
Tr M<X1…Xn>(T1 x1 … Tm xm)
При вызове метода формы M(E1 …Em)
задачей вывода типа является поиск уникальных аргументов типа S1…Sn
для каждого из параметров типа X1…Xn
так что вызов M<S1…Sn>(E1…Em)
становится действительным.
Самый первый шаг, который принимает компилятор, выглядит следующим образом (§7.5.2.1):
Для каждого из аргументов метода Ei
:
-
Если Ei
- анонимная функция, то вывод о явном параметрическом параметре (§7.5.2.7) производится из Ei
to Ti
-
В противном случае, если Ei
имеет тип U
а xi
- параметр значения, то вывод с нижней границей производится от U
до Ti
.
У вас есть только один аргумент, поэтому мы имеем, что единственным Ei
является выражение new CountVisitor()
. Это явно не анонимная функция, поэтому мы находимся во втором пункте. Тривиально видеть, что в нашем случае U
имеет тип CountVisitor
. xi
" xi
- это параметр значения" в основном означает, что это не переменная out
, in
, ref
и т.д., Которая здесь имеет место.
На этом этапе нам нужно сделать вывод с нижней границей от CountVisitor
к IVisitor<TResult, TVisitable>
Соответствующая часть §7.5.2.9 (где из-за переменной переменной мы имеем V
= IVisitor<TResult, TVisitable>
в наш случай):
- В противном случае значения
U1…Uk
и V1…Vk
определяются путем проверки того, применяется ли одно из следующих случаев: -
V
- тип массива V1[…]
а U
- тип массива U1[…]
(или параметр типа, эффективный базовый тип которого является U1[…]
) того же ранга -
V
является одним из IEnumerable<V1>
, ICollection<V1>
или IList<V1>
а U
является одномерным типом массива U1[]
(или параметром типа, эффективным базовым типом которого является U1[]
) -
V
- построенный класс, структура, интерфейс или тип делегата C<V1…Vk>
и существует уникальный тип C<U1…Uk>
такой, что U
(или, если U
является параметром типа, его эффективным базовым классом или любым членом его эффективного набора интерфейсов) идентичен, наследуется от (прямо или косвенно) или реализует (прямо или косвенно) C<U1…Uk>
.
(Ограничение "единственности" означает, что в случае интерфейса C<T>{} class U: C<X>, C<Y>{}
, то никаких выводов не делается, если вывести из U
в C<T>
поскольку U1
может X
или Y
).
Мы можем пропустить первые два случая, поскольку они явно не применимы, третий случай - тот, в который мы попадаем. Компилятор пытается найти уникальный тип C<U1…Uk>
который CountVisitor
реализует и находит два таких типа: IVisitor<int, Foo>
и IVisitor<int, Bar>
. Обратите внимание, что пример, который дает spec, почти идентичен вашему примеру.
Из-за ограничения уникальности для этого аргумента метода не делается никаких выводов. Поскольку компилятор не может вывести информацию о типе из аргумента, ему нечего делать, чтобы попытаться вывести TResult
и, таким образом, выйти из строя.
Что касается того, почему существует ограничение единственности, я предполагаю, что он упрощает алгоритм и, следовательно, реализацию компилятора. Если вам интересно, здесь ссылка на исходный код, где Roslyn (современный компилятор С#) реализует типичный вывод типа метода.
Ответ 3
В С# вы можете упростить шаблон посетителя, удалив "двойную отправку", используя dynamic
ключевое слово.
Вы можете реализовать своего посетителя следующим образом:
public class CountVisitor : IVisitor<int, IVisitable>
{
public int Visit( IVisitable v )
{
dynamic d = v;
Visit(d);
}
private int Visit( Foo f )
{
return 42;
}
private int Visit( Bar b )
{
return 7;
}
}
Делая это, вам не нужно иметь метод Accept, реализованный в Foo
и Bar
хотя они все равно должны реализовать общий интерфейс для работы Visitor
для работы в отключенном режиме.