最近、ビジターパターンの実装を実験しました。ここでは、汎用インターフェイスを使用してAccept&Visitメソッドを適用しようとしました。
_public interface IVisitable<out TVisitable> where TVisitable : IVisitable<TVisitable>
{
TResult Accept<TResult>(IVisitor<TResult, TVisitable> visitor);
}
_
-その目的は、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);
}
_
-1)訪問者をTVisitableに「訪問可能」としてマークする必要があります2)このTVisitableの結果タイプ(TResult)はどのようにする必要がありますか3)各TVisitableごとに正しい署名のVisitメソッドを適用する訪問者の実装は「訪問可能」です、 そのようです:
_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>)'
の型引数を使用法から推測することはできません。」
誰かがそれがなぜであるかについて詳しく説明してもらえますか? CountVisitor
が実装する_IVisitor<T, Foo>
_インターフェースのバージョンは1つだけです。または、何らかの理由で_IVisitor<T, Bar>
_を削除できない場合は、両方とも同じT
--int
、=とにかく他のタイプはそこでは機能しません。適切な候補が複数あるとすぐに型推論はあきらめますか? (おもしろい事実:ReSharperは、theFoo.Accept<int>(...)
のint
は冗長であると考えています:P、それなしではコンパイルされませんが)
適切な候補が複数あるとすぐに型推論はあきらめますか?
はい、この場合はそうです。メソッドのジェネリック型パラメーター(TResult
)を推論しようとしているときに、型推論アルゴリズムは、型_IVisitor<TResult, TVisitable>
_に対して2つの推論を持つCountVisitor
で失敗したように見えます。
C#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
からTi
まで作成されます。それ以外の場合、
Ei
のタイプがU
で、xi
が値パラメーターの場合、下限推論が作成されますfromU
toTi
。
引数は1つしかないため、Ei
は式new CountVisitor()
のみです。これは明らかに匿名関数ではないため、2番目の箇条書きになります。この場合、U
のタイプがCountVisitor
であることを確認するのは簡単です。 「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
は1次元配列型_U1[]
_(または有効な基本型が_U1[]
_である型パラメーター)です。 )V
は、構築されたクラス、構造体、インターフェース、またはデリゲートタイプ_C<V1…Vk>
_であり、U
(または、U
がタイプパラメーターの場合は、その有効な基本クラスまたはそのメンバー)のような一意のタイプ_C<U1…Uk>
_があります。有効なインターフェースセット)は、_C<U1…Uk>
_と同一であるか、(直接的または間接的に)継承するか、(直接的または間接的に)実装します。(「一意性」の制限は、インターフェース_
C<T>{} class U: C<X>, C<Y>{}
_の場合、_C<T>
_がU
またはX
である可能性があるため、Y
から_U1
_に推論するときに推論が行われないことを意味します。)
最初の2つのケースは明らかに当てはまらないため、スキップできます。3番目のケースは私たちが該当するケースです。コンパイラは、CountVisitor
が実装するuniqueタイプ_C<U1…Uk>
_を見つけようとし、twoそのようなタイプ、_IVisitor<int, Foo>
_および_IVisitor<int, Bar>
_。仕様が示す例は、あなたの例とほぼ同じであることに注意してください。
一意性の制約があるため、このメソッド引数の推論は行われません。コンパイラーは引数から型情報を推測できないため、TResult
を推測しようとすることは何もないため、失敗します。
一意性の制約が存在する理由については、アルゴリズムが単純化され、コンパイラーの実装が単純化されると思います。興味がある場合は、 ここにリンクがあります Roslyn(最新のC#コンパイラ)がジェネリックメソッド型推論を実装するソースコードへ。
型推論は貪欲な方法で機能しているようです。最初にmethodジェネリック型を照合し、次にクラスジェネリック型を照合しようとします。だからあなたが言うなら
int count = theFoo.Accept<int>(new CountVisitor());
fooがクラスジェネリック型の唯一の候補であるため、これは奇妙なことに機能します。
まず、メソッドのジェネリック型を2番目のクラスのジェネリック型に置き換えると、次のように機能します。
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());
}
}
2番目(そしてこれは型推論がどのように機能するかを強調する奇妙な部分です)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>'
に変換できません
これは、コンパイラーが最初にmethodジェネリック型(この場合はTResult
)を調べ、さらに候補が見つかるとすぐに失敗することを示しています。クラスのジェネリック型については、これ以上詳しくは説明しません。
Microsoftから型推論の仕様を見つけようとしましたが、見つかりませんでした。
C#では、dynamic
キーワードを使用して「ダブルディスパッチ」を削除することで、Visitorパターンを簡略化できます。
次のようにVisitorを実装できます。
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
がオフコースで機能するための共通インターフェースを実装する必要があります。