設計時にvarを使用して宣言された変数の型を確実に判断するにはどうすればよいですか?


109

私はemacsでC#の補完(インテリセンス)機能に取り組んでいます。

ユーザーがフラグメントを入力し、特定のキーストロークの組み合わせを介して補完を要求すると、補完機能は.NETリフレクションを使用して可能な補完を決定するという考え方です。

これを行うには、完成するものの種類を知っている必要があります。文字列の場合、考えられるメソッドとプロパティの既知のセットがあります。それがInt32の場合は、別個のセットがあります。

emacsで利用可能なコードレクサー/パーサーパッケージのセマンティックを使用して、変数宣言とその型を見つけることができます。そのため、リフレクションを使用して型のメソッドとプロパティを取得し、オプションのリストをユーザーに提示するのは簡単です。(OK、emacs 内で実行するのは簡単ではありませんが、 emacs 内でpowershellプロセスを実行する機能を使用すると、はるかに簡単になります。リフレクションを実行するためのカスタム.NETアセンブリを記述し、それをpowershellにロードしてから、elisp内で実行しますemacsはコマンドをpowershellに送信し、comintを介して応答を読み取ることができます。その結果、emacsはリフレクションの結果をすばやく取得できます。)

問題varは、完了したものの宣言でコードが使用するときに発生します。つまり、タイプが明示的に指定されておらず、補完が機能しません。

変数がvarキーワードで宣言されている場合、実際に使用されているタイプを確実に判断するにはどうすればよいですか?明確にするために、実行時にそれを決定する必要はありません。「設計時」に決定したい。

これまでのところ、私はこれらのアイデアを持っています:

  1. コンパイルして呼び出す:
    • 宣言ステートメントを抽出します。例: `var foo =" a string value ";`
    • ステートメント `foo.GetType();`を連結します
    • 結果のC#フラグメントを動的にコンパイルして新しいアセンブリに
    • アセンブリを新しいAppDomainに読み込み、フレームを実行して戻り値の型を取得します。
    • アセンブリをアンロードして破棄する

    私はこれをすべて行う方法を知っています。しかし、エディターでの完了要求ごとに、それはひどく重いように聞こえます。

    私は毎回新しいAppDomainを必要としないと思います。単一のAppDomainを複数の一時的なアセンブリに再利用し、複数の完了リクエストにまたがって、それを設定および破棄するコストを償却できます。これは、基本的な考え方の微調整です。

  2. ILのコンパイルと検査

    宣言をモジュールにコンパイルし、ILを検査して、コンパイラーによって推論された実際のタイプを判別します。これはどのようにして可能でしょうか?ILの検査には何を使用しますか?

そこにもっと良いアイデアはありますか?コメント?提案?


編集 -これについてさらに考えると、コンパイルと呼び出しは受け入れられません。呼び出しには副作用がある可能性があるためです。したがって、最初のオプションは除外する必要があります。

また、.NET 4.0の存在は想定できません。


更新 -正解は、上記には言及されていませんが、エリックリッパートによって穏やかに指摘されていますが、完全な忠実度の型推論システムを実装することです。これは、設計時に変数のタイプを確実に決定する唯一の方法です。しかし、それも簡単ではありません。私はそのようなものを構築したいという幻想に苦しんでいないので、オプション2のショートカットをとりました-関連する宣言コードを抽出してコンパイルし、結果のILを検査します。

これは、完了シナリオのかなりのサブセットに対して実際に機能します。

たとえば、次のコードフラグメントで、?ユーザーが完了を要求する位置です。これは機能します:

var x = "hello there"; 
x.?

補完により、xは文字列であることがわかり、適切なオプションが提供されます。これは、次のソースコードを生成してコンパイルすることによって行われます。

namespace N1 {
  static class dmriiann5he { // randomly-generated class name
    static void M1 () {
       var x = "hello there"; 
    }
  }
}

...そして単純な反射でILを検査します。

これも機能します:

var x = new XmlDocument();
x.? 

エンジンは適切に使用する句を生成されたソースコードに追加するため、適切にコンパイルされ、IL検査は同じになります。

これも機能します:

var x = "hello"; 
var y = x.ToCharArray();    
var z = y.?

これは、IL検査が最初のローカル変数ではなく、3番目のローカル変数のタイプを検出する必要があることを意味します。

この:

var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
    {
        foo,
        foo.Length.ToString()
    };
var z = fred.Count;
var x = z.?

...これは前の例よりも1レベルだけ深いです。

しかし、機能しないのは、初期化がインスタンスメンバーの任意の時点で依存するローカル変数、またはローカルメソッド引数の完了です。お気に入り:

var foo = this.InstanceMethod();
foo.?

LINQ構文もありません。

完成させるために間違いなく「限定デザイン」(ハックの礼儀正しい言葉)であるものを使ってそれらに対処することを検討する前に、それらがどれほど価値があるかについて考えなければなりません。

メソッド引数またはインスタンスメソッドへの依存性の問題に対処するアプローチは、生成され、コンパイルされ、IL分析されるコードのフラグメントで、それらへの参照を同じタイプの「合成」ローカル変数で置き換えることです。


別の更新 -インスタンスメンバーに依存する変数の補完が機能するようになりました。

私が行ったのは、(セマンティックを介して)タイプを問い合わせてから、既存のすべてのメンバーの合成代用メンバーを生成することでした。このようなC#バッファーの場合:

public class CsharpCompletion
{
    private static int PrivateStaticField1 = 17;

    string InstanceMethod1(int index)
    {
        ...lots of code here...
        return result;
    }

    public void Run(int count)
    {
        var foo = "this is a string";
        var fred = new System.Collections.Generic.List<String>
        {
            foo,
            foo.Length.ToString()
        };
        var z = fred.Count;
        var mmm = count + z + CsharpCompletion.PrivateStaticField1;
        var nnn = this.InstanceMethod1(mmm);
        var fff = nnn.?

        ...more code here...

...コンパイルされて生成されたコードで、出力ILからローカル変数nnnのタイプを知ることができるように、次のようになります。

namespace Nsbwhi0rdami {
  class CsharpCompletion {
    private static int PrivateStaticField1 = default(int);
    string InstanceMethod1(int index) { return default(string); }

    void M0zpstti30f4 (int count) {
       var foo = "this is a string";
       var fred = new System.Collections.Generic.List<String> { foo, foo.Length.ToString() };
       var z = fred.Count;
       var mmm = count + z + CsharpCompletion.PrivateStaticField1;
       var nnn = this.InstanceMethod1(mmm);
      }
  }
}

すべてのインスタンスおよび静的型メンバーは、スケルトンコードで使用できます。正常にコンパイルされます。その時点で、ローカル変数のタイプを決定することは、リフレクションを介して簡単です。

これを可能にするのは:

  • emacsでPowerShellを実行する機能
  • C#コンパイラは本当に高速です。私のマシンでは、インメモリアセンブリをコンパイルするのに約0.5秒かかります。キーストローク間分析には十分高速ではありませんが、完了リストのオンデマンド生成をサポートするには十分高速です。

私はまだLINQを調べていません。
C#のセマンティックレクサー/パーサーemacsはLINQを「実行」しないため、これははるかに大きな問題になります。


4
fooの型は、型推論を通じてコン​​パイラーによって計算され、埋められます。メカニズムはまったく異なると思います。おそらく型推論エンジンはフックを持っていますか?少なくとも、「型推論」をタグとして使用します。
George Mauer、

3
すべてのタイプを持ち、実際のオブジェクトのセマンティクスをまったく持たない「偽の」オブジェクトモデルを作成する手法は、良いものです。そのようにして、当時Visual InterDevでIntelliSense for JScriptを実行しました。すべてのメソッドと型を持ち、副作用のないIEオブジェクトモデルの "偽の"バージョンを作成し、コンパイル時に解析済みコードに対して小さなインタープリターを実行して、どの型が返されるかを確認します。
Eric Lippert、

回答:


202

「実際の」C#IDEでそれを効率的に行う方法を説明します。

最初に行うことは、ソースコードの「トップレベル」のものだけを分析するパスを実行することです。メソッド本体はすべてスキップします。これにより、プログラムのソースコード内にある名前空間、型、メソッド(およびコンストラクターなど)に関する情報のデータベースをすばやく構築できます。キーストローク間で実行しようとすると、すべてのメソッド本体のすべてのコード行を分析するのに時間がかかりすぎます。

IDEがメソッド本体内の特定の式のタイプを計算する必要がある場合-「foo」と入力したとしましょう。そして、fooのメンバーが何であるかを理解する必要があります-同じことを行います。できる限り多くの作業をスキップします。

まず、そのメソッド内のローカル変数宣言のみを分析するパスから始めます。そのパスを実行すると、「スコープ」と「名前」のペアから「タイプ決定子」へのマッピングが作成されます。「タイプ決定子」は、「必要に応じて、このローカルのタイプを計算できる」という概念を表すオブジェクトです。ローカルのタイプを調べることはコストがかかる可能性があるため、必要に応じてその作業を延期したいと考えています。

これで、すべてのローカルのタイプを知ることができる、遅延ビルドされたデータベースができました。それで、その「フー」に戻りましょう。-私たちはどの把握声明関連する式がであり、その後、ちょうどその文に対して意味解析を実行します。たとえば、次のメソッド本体があるとします。

String x = "hello";
var y = x.ToCharArray();
var z = from foo in y where foo.

そして今、私たちはfooがchar型であることを理解する必要があります。すべてのメタデータ、拡張メソッド、ソースコードタイプなどを含むデータベースを構築します。x、y、zの型判別子を持つデータベースを構築します。興味深い表現を含むステートメントを分析します。まず構文的に変換します

var z = y.Where(foo=>foo.

fooのタイプを計算するには、まずyのタイプを知る必要があります。したがって、この時点で、型判別子に「yの型は何ですか」と質問します。次に、x.ToCharArray()を解析して「xのタイプは何ですか」と尋ねる式エバリュエーターを起動します。「現在のコンテキストで「文字列」を検索する必要がある」というタイプの判別子があります。現在の型にはString型がないため、名前空間を調べます。それもないので、usingディレクティブを調べて、「using System」があり、そのSystemにString型があることを発見します。OK、それがxのタイプです。

次に、System.Stringのメタデータに対してToCharArrayのタイプをクエリし、それがSystem.Char []であると表示します。素晴らしい。したがって、yの型があります。

次に、「System.Char []にはメソッドWhereがありますか?」いいえ。それでは、usingディレクティブを調べます。使用できる可能性のある拡張メソッドのすべてのメタデータを含むデータベースをすでに計算済みです。

ここで、「OK、スコープ内に名前が付けられた18の拡張メソッドがあります。それらのいずれかに、System.Char []と互換性のある型の最初の仮パラメーターがありますか?」そこで、一連の変換テストを開始します。ただし、Where拡張メソッドはジェネリックです。つまり、型推論を行う必要があります。

最初の引数から拡張メソッドへの不完全な推論を処理できる特別なタイプの推論エンジンを作成しました。型推論を実行して、を受け取るWhereメソッドがありIEnumerable<T>、System.Char []からに推論できることを発見しましIEnumerable<System.Char>た。TはSystem.Charです。

このメソッドのシグネチャはWhere<T>(this IEnumerable<T> items, Func<T, bool> predicate)であり、TがSystem.Charであることはわかっています。また、拡張メソッドの括弧内の最初の引数がラムダであることもわかっています。したがって、「仮パラメーターfooはSystem.Charであると想定される」と言うラムダ式タイプの推論を開始し、残りのラムダを分析するときにこの事実を使用します。

これで、ラムダの本体「foo。」を分析するために必要なすべての情報が得られました。fooのタイプを調べて、ラムダバインダーによると、それがSystem.Charであることがわかりました。System.Charのタイプ情報を表示します。

そして、キーストローク間の「トップレベル」分析以外のすべてを行います。それは本当のトリッキーなビットです。実際にすべての分析を書くことは難しくありません。それはあなたが本当のトリッキーなビットであるタイピング速度でそれを行うことができるのに十分速くなっています。

幸運を!


8
エリック、完全な返事をありがとう。あなたは私の目をかなり開きました。emacsの場合、ユーザーエクスペリエンスの品質の点でVisual Studioと競合する動的なキーストロークエンジンを作成することは望んでいませんでした。1つは設計に0.5秒程度のレイテンシがあるため、emacsベースの機能はオンデマンドのみであり、今後もオンデマンドのままです。先行入力の提案はありません。もう1つ-var localsの基本的なサポートを実装しますが、物事が困難になったとき、または依存関係グラフが特定の制限を超えたときは喜んでパントします。その限界はまだわからない。再度、感謝します。
Cheeso

13
特にラムダ式やジェネリック型の推論では、これらすべてが非常に迅速かつ確実に機能することを正直に思い知らされます。ステートメントがまだ完了しておらず、拡張メソッドのジェネリックパラメーターを明示的に指定していなかったとしても、ラムダ式を初めて書いたときは本当に驚きました。魔法を少し覗いてくれてありがとう。
ダンブライアント

21
@Dan:ソースコードを見た(または書いた)ので、それがうまく機能していることに心を揺さぶられます。:-)そこにはいくつかの毛むくじゃらのものがあります。
エリックリッペルト2010年

11
彼らはあるので、Eclipseの人は、おそらくより良いそれを行うより素晴らしい C#コンパイラとIDEチームより。
Eric Lippert、

23
私はこの愚かなコメントをしたことをまったく覚えていません。それも意味がありません。酔っていたに違いない。ごめんなさい。
Tomas Andrle 2010年

15

Delphi IDEがDelphiコンパイラと連携してインテリセンスを実行する方法を大まかに説明できます(コードインサイトはDelphiがそれを呼び出すものです)。C#に100%適用できるわけではありませんが、検討に値する興味深いアプローチです。

Delphiのほとんどの意味分析は、パーサー自体で行われます。式が解析されるときに型が型付けされますが、これが容易ではない場合を除きます。この場合、先読み解析を使用して意図された結果が得られ、その決定が解析で使用されます。

解析は、演算子の優先順位を使用して解析される式を除いて、主にLL(2)再帰降下です。Delphiの特徴の1つは、それがシングルパス言語であることです。そのため、コンストラクトを使用する前に宣言する必要があるため、その情報を引き出すためにトップレベルのパスは必要ありません。

この機能の組み合わせにより、パーサーは、コードインサイトに必要なあらゆるポイントで必要なほぼすべての情報を取得できます。動作方法は次のとおりです。IDEは、コンパイラのレクサーにカーソルの位置(コードインサイトが必要なポイント)を通知し、レクサーはこれを特別なトークン(kibitzトークンと呼ばれます)に変換します。パーサーがこのトークン(どこにでもある可能性があります)に出会うたびに、これがすべての情報をエディターに送り返すための信号であることを認識します。Cで記述されているため、これはlongjmpを使用して行われます。それが行うことは、最終的な呼び出し元に、kibitzポイントが見つかった構文構文(つまり文法上のコンテキスト)の種類と、そのポイントに必要なすべてのシンボリックテーブルを通知することです。たとえば コンテキストがメソッドの引数である式内にある場合、メソッドのオーバーロードをチェックし、引数の型を確認し、有効なシンボルをその引数の型に解決できるものだけにフィルタリングできます(これにより、ドロップダウンに無関係なくだらないことがたくさんあります)。ネストされたスコープコンテキスト内にある場合( "。"の後など)、パーサーはスコープへの参照を返し、IDEはそのスコープ内にあるすべてのシンボルを列挙できます。

他のことも行われます。たとえば、kibitzトークンが範囲内にない場合、メソッド本体はスキップされます。これは楽観的に行われ、トークンをスキップした場合はロールバックされます。拡張メソッドに相当するもの-Delphiのクラスヘルパー-には、バージョン付きのキャッシュがあるので、検索はかなり高速です。しかし、Delphiのジェネリック型の推論は、C#よりもはるかに弱いです。

さて、特定の質問:で宣言された変数varの型を推論することは、Pascalが定数の型を推論する方法と同等です。これは、初期化式のタイプに由来します。これらのタイプは、ボトムアップで構築されています。場合xタイプのものでありInteger、かつy型であるDouble場合、x + yタイプのものであろうDoubleものは、言語のルールがあるので、。右辺に完全な式の型ができるまでこれらの規則に従います。それが左側のシンボルに使用する型です。



4

Intellisenseシステムは、通常、抽象構文ツリーを使用してコードを表します。これにより、コンパイラーとほぼ同じ方法で、変数に割り当てられている関数の戻り値の型を解決できます。VS Intellisenseを使用する場合、有効な(解決可能な)割り当て式の入力が完了するまで、varのタイプが得られないことがあります。式がまだあいまいな場合(たとえば、式のジェネリック引数を完全に推測できない場合)、var型は解決されません。タイプを解決するためにかなり深くツリーを歩く必要がある場合があるため、これはかなり複雑なプロセスになる可能性があります。例えば:

var items = myList.OfType<Foo>().Select(foo => foo.Bar);

戻り値の型はですがIEnumerable<Bar>、これを解決するには次の知識が必要です。

  1. myListはを実装するタイプですIEnumerable
  2. 拡張方法があります OfType<T>IEnumerableに適用されるがあります。
  3. 結果の値はIEnumerable<Foo>であり、Selectこれに適用される拡張メソッドがあります。
  4. ラムダ式にfoo => foo.Barは、タイプFooのパラメーターfooがあります。これはSelectの使用によって推測されます。これはaを受け取り、Func<TIn,TOut>TInは既知(Foo)であるため、fooのタイプを推測できます。
  5. タイプFooには、タイプBarのプロパティBarがあります。Selectは戻り値IEnumerable<TOut>とTOutをラムダ式の結果から推測できることを知っているため、結果の項目の型はでなければなりませんIEnumerable<Bar>

そう、それはかなり深くなる可能性があります。私はすべての依存関係を解決することに慣れています。これについて考えると、最初に説明したオプション(コンパイルと呼び出し)は絶対に受け入れられません。コードを呼び出すと、データベースの更新などの副作用が生じる可能性があり、エディターが行うべきことではないからです。コンパイルは問題ありませんが、呼び出しは問題です。ASTの構築に関しては、私はそれを実行したくないと思います。本当に私はその仕事をコンパイラーに任せたいと思っています。コンパイラーはすでにそれを行う方法を知っています。知りたいことをコンパイラに教えてもらえるようにしたい。簡単な答えが欲しいだけです。
Cheeso

コンパイルから検査する際の課題は、依存関係が任意に深くなる可能性があることです。つまり、コンパイラーがコードを生成するためにすべてをビルドする必要がある場合があります。そうした場合、生成されたILでデバッガーシンボルを使用し、各ローカルのタイプをそのシンボルと一致させることができると思います。
ダンブライアント

1
@Cheeso:コンパイラーは、そのような型分析をサービスとして提供していません。将来的にはそれが約束されることはないが期待しています。
Eric Lippert

はい、私はそれが行く方法かもしれないと思います-すべての依存関係を解決してから、ILをコンパイルして検査します。@エリック、知っておくと良い。今のところ、完全なAST分析を行うことを望んでいない場合、既存のツールを使用してこのサービスを作成するには、ダーティハックに頼らなければなりません。たとえば、インテリジェントに構築されたコードのフラグメントをコンパイルしてから、ILDASM(または同様の)をプログラムで使用して、私が求めている答えを得ます。
Cheeso

4

Emacsをターゲットにしているので、CEDETスイートから始めるのが最善の場合があります。Eric Lippertのすべての詳細は、C ++のCEDET /セマンティックツールのコードアナライザーですでに説明されています。C#パーサー(おそらく少しTLCを必要とする)もあるので、欠けている部分はC#に必要な部分のチューニングに関連しています。

基本的な動作は、言語ごとに定義されるオーバーロード可能な関数に依存するコアアルゴリズムで定義されます。コンプリーションエンジンの成功は、どれだけチューニングが行われたかに依存します。c ++をガイドとして使用すれば、C ++と同様のサポートが得られることは悪くありません。

ダニエルの答えは、MonoDevelopを使用して解析と分析を行うことを提案しています。これは、既存のC#パーサーの代わりの代替メカニズムである場合や、既存のパーサーを拡張するために使用される場合があります。


そう、私はCEDETについて知っており、セマンティックのためにcontribディレクトリのC#サポートを使用しています。セマンティックは、ローカル変数とそのタイプのリストを提供します。補完エンジンはそのリストをスキャンして、ユーザーに適切な選択肢を提供できます。問題は、変数がの場合ですvar。セマンティックはそれをvarとして正しく識別しますが、型推論を提供しません。私の質問は、具体的にどのようにそれに対処するでした。また、既存のCEDETコンプリーションにプラグインすることも検討しましたが、その方法がわかりませんでした。CEDETのドキュメントは...完全ではありません...
Cheeso

サイドコメント-CEDETは見事に野心的ですが、使用および拡張が難しいと感じました。現在、パーサーは「名前空間」をC#のクラスインジケーターとして扱います。「名前空間」を明確な構文要素として追加する方法すら理解できませんでした。これを行うと、他のすべての構文分析が妨げられ、その理由を理解できませんでした。以前、補完フレームワークで私が抱えていた難しさを説明しました。これらの問題以外にも、継ぎ目や断片間の重なりがあります。一例として、ナビゲーションはセマンティックと上院の両方の一部です。CEDETは魅力的のようですが、結局のところ、それを実行するにはあまりに扱いにくいです。
Cheeso

Cheeso、あまり文書化されていないCEDETの部分を最大限に活用したい場合、最善の策はメーリングリストを試すことです。質問がまだ十分に開発されていない領域を掘り下げるのは簡単です。そのため、適切な解決策を作成したり、既存の解決策を説明したりするには、数回の反復が必要です。特にC#については、私はそれについて何も知らないので、単純な1回限りの答えはありません。
エリック

2

うまくやるのは難しい問題です。基本的に、字句解析/構文解析/型チェックの大部分を介して言語仕様/コンパイラをモデル化し、クエリできるソースコードの内部モデルを構築する必要があります。EricがC#について詳しく説明しています。F#コンパイラのソースコード(F#CTPの一部)をいつでもダウンロードして、service.fsiて、F#言語サービスがインテリセンスや推論された型のツールチップなどを提供するために消費するF#コンパイラーから公開されたインターフェイスを確認できます。呼び出し先のAPIとしてすでにコンパイラーが利用可能である場合に考えられる「インターフェース」の感覚。

もう1つの方法は、コンパイラを現状のまま再利用し、リフレクションを使用するか、生成されたコードを確認することです。これは、コンパイラからコンパイル出力を取得するために「完全なプログラム」が必要であるという観点からは問題がありますが、エディタでソースコードを編集するときは、まだ解析されていない「部分的なプログラム」しかなく、すべてのメソッドがまだ実装されているなど

要するに、私は「低予算」バージョンはうまくやるのが非常に難しいと思います、そして「本物」バージョンはうまくやるのが非常に非常に難しいと思います。(ここで「ハード」とは、「努力」と「技術的困難」の両方を測定します。)


はい、「低予算」バージョンにはいくつかの明確な制限があります。私は「十分に良い」とは何か、そしてそのバーに会えるかどうかを決定しようとしています。私自身のこれまでの経験でドッグフーディングを行った経験から、emacs内でのC#の記述ははるかに優れています。
Cheeso


0

ソリューション「1」の場合、.NET 4に新しい機能があり、これをすばやく簡単に実行できます。したがって、プログラムを.NET 4に変換できる場合は、それが最良の選択です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.