web-dev-qa-db-ja.com

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

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

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

これを行うには、完了しているもののタイプを知る必要があります。文字列の場合、既知の可能なメソッドとプロパティのセットがあります。 Int32の場合、個別のセットなどがあります。

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

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

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

これまでのところ、これらのアイデアがあります:

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

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

    毎回新しいAppDomainを必要としないと思います。単一のAppDomainを複数の一時アセンブリに再利用し、複数の完了要求にわたって、セットアップと破棄のコストを償却できます。それは基本的な考え方の微調整です。

  2. iL をコンパイルして検査します。

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

より良いアイデアはありますか?コメント?提案?


[〜#〜] edit [〜#〜]-これについてさらに考えると、コンパイルと呼び出しは受け入れられません。エフェクト。したがって、最初のオプションは除外する必要があります。

また、.NET 4.0の存在を想定することはできないと思います。


[〜#〜] update [〜#〜]-上記ではないが、Eric Lippertによって丁寧に指摘された正しい答えは、完全な忠実度型推論システム。設計時にvarの型を確実に決定する唯一の方法です。しかし、それも簡単ではありません。私はそのようなものを構築しようとする幻想に苦しんでいないので、オプション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.? 

エンジンは適切なusing句を生成されたソースコードに追加するため、適切にコンパイルされ、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構文もありません。

間違いなく「限定設計」(ハックのための丁寧なWord)を介して対処することを検討する前に、それらの事柄がどれほど価値があるかを考えなければなりません。

メソッドの引数またはインスタンスメソッドへの依存関係の問題に対処するアプローチは、生成、コンパイル、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);
      }
  }
}

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

これを可能にするものは次のとおりです。

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

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

109
Cheeso

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

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

IDEメソッド本体内の特定の式のタイプを調べる必要がある場合-「foo。」と入力したとすると、fooのメンバーを把握する必要があります- -私たちは同じことをします;私たちは合理的にできる限り多くの仕事をスキップします。

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

これで、すべてのローカルのタイプを伝えることができる遅延構築されたデータベースができました。それで、その「foo」に戻ります。 -どの式がstatementにあるかを把握し、その文に対してセマンティックアナライザーを実行します。たとえば、メソッド本体があるとします:

_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のタイプです。

次に、ToCharArrayの型についてSystem.Stringのメタデータを照会すると、それがSystem.Char []であることが示されます。スーパー。したがって、yの型があります。

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

ここで、「OK、Where in scopeという名前の拡張メソッドが18個あります。そのうちの1つには、System.Char []と互換性のある最初の仮パラメーターがありますか?」そこで、互換性テストのラウンドを開始します。ただし、Where拡張メソッドはgenericです。つまり、型推論を行う必要があります。

最初の引数から拡張メソッドへの不完全な推論を処理できる特別な型推論エンジンを作成しました。型推論器を実行し、_IEnumerable<T>_をとるWhereメソッドがあり、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の型情報を表示します。

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

がんばろう!

202
Eric Lippert

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

Delphiのほとんどのセマンティック分析は、パーサー自体で実行されます。式は解析されるときに入力されますが、これは簡単ではない状況を除きます。この場合、先読み解析を使用して目的を判断し、解析でその決定を使用します。

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

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

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

次に、特定の質問に答えます。varで宣言された変数のタイプを推測することは、Pascalが定数のタイプを推測する方法と同等です。これは、初期化式のタイプに由来します。これらのタイプはボトムアップで構築されます。 xInteger型であり、yDouble型である場合、x + yDouble型になります。これは、これらが言語の規則であるためです。など。右側に完全な式の型ができるまでこれらの規則に従い、それが左側のシンボルに使用する型になります。

15
Barry Kelly

抽象構文ツリーを構築するために独自のパーサーを作成する必要がない場合は、 SharpDevelop または MonoDevelop のいずれかからパーサーを使用する方法を検討できます。オープンソースです。

7
Daniel Plaisted

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

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

戻り値の型はIEnumerable<Bar>ですが、これを解決するには以下を知っておく必要があります。

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

Emacsをターゲットにしているので、CEDETスイートから始めるのが最善かもしれません。 Eric Lippertのすべての詳細は、C++用のCEDET/Semanticツールのコードアナライザーで既に説明されています。また、C#パーサー(おそらく少しTLCが必要)があるため、不足している部分はC#に必要な部分の調整に関連する部分のみです。

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

Danielの答えは、MonoDevelopを使用して解析と分析を行うことを示唆しています。これは、既存のC#パーサーの代わりに代替メカニズムを使用したり、既存のパーサーを強化するために使用したりできます。

4
Eric

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

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

要するに、「低予算」バージョンはうまくやるのが非常に難しく、「実際の」バージョンは非常によく、veryうまくやるのは難しいと思います。 (ここで「難しい」とは「努力」と「技術的な困難」の両方を測定するものです。)

2
Brian

NRefactory がこれを行います。

2
erikkallen

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

0
Softlion