1. ホーム
  2. c#

[解決済み] 設計時にvarで宣言された変数の型を確実に決定する方法を教えてください。

2022-11-14 20:21:49

質問

emacsでC#の補完(intellisense)機能を作っています。

このアイデアは、ユーザーが断片を入力し、特定のキーストロークの組み合わせで補完を要求した場合、補完機能は.NETリフレクションを使用して可能な補完を決定します。

これを行うには、補完されるもののタイプがわかっていることが必要です。 文字列であれば、可能なメソッドとプロパティの既知のセットがあり、Int32であれば、別のセットがある、というように。

emacsで利用可能なコードレキサー/パーサーパッケージであるsemanticを使用して、私は変数の宣言とその型を見つけることができます。 それを考えると、型のメソッドとプロパティを取得するためにリフレクションを使用するのは簡単で、その後、ユーザーにオプションのリストを提示します。(OK、かなり ストレート を行うには の中で を使用していますが、emacsでは emacsの中でpowershellプロセスを実行する機能 を使えば、もっと簡単になります。私はリフレクションを行うためのカスタム.NETアセンブリを書き、それをpowershellにロードし、emacs内で実行されているelispはpowershellにコマンドを送り、comint経由でレスポンスを読み取ることができます。 その結果、emacsはリフレクションの結果を素早く得ることができるのです)。

問題は、コードが var を使用している場合です。これは型が明示的に指定されていないことを意味し、補完はうまくいきません。

で宣言された変数が実際に使用される型を確実に判断するにはどうしたらよいでしょうか。 var キーワードで宣言されている場合、実際に使用される型を確実に決定するにはどうしたらよいでしょうか。 はっきりさせておきたいのですが、私は実行時にそれを決定する必要はありません。 私は、quot;Design time"でそれを決定したいのです。

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

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

    これをすべて行う方法は知っています。 しかし、エディターでの補完要求のたびに、ひどく重苦しい感じがします。

    私は、毎回新しい AppDomain を必要としないと思います。 複数の一時的なアセンブリに 1 つの AppDomain を再利用し、設定と削除のコストを分散させることができます。 複数の完了要求にまたがって、セットアップとテアダウンのコストを償却することができます。 これは、基本的なアイデアの微調整です。

  2. IL をコンパイルして検査する

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

何か良いアイデアはありませんか?コメントや提案はありますか?


EDIT - このことについてさらに考えると、コンパイルと起動は、起動が副作用を持つ可能性があるため、受け入れられません。したがって、最初の選択肢は除外されなければなりません。

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


アップデイト - 正しい答えは、上記には書かれていませんが、Eric Lippertによって優しく指摘されたように、完全な忠実度の型推論システムを実装することです。 これは、設計時にバーの型を確実に決定する唯一の方法です。 しかし、これを実行するのは簡単なことではありません。 私はそのようなものを構築しようとする幻想を抱いていないので、私は選択肢 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.?

...これは、前の例より一段階深いだけです。

しかし、何 はそうではなく が働かないのは、初期化がインスタンスメンバーやローカルメソッドの引数に依存しているローカル変数に対する補完です。のようなものです。

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

LINQの構文でもありません。

完成のための "limited design" (ハックのための丁寧な言葉) であることは間違いありませんが、それらに取り組むことを考える前に、それらがどれほど価値があるかについて考えなければならないでしょう。

メソッド引数またはインスタンスメソッドへの依存の問題に対処するためのアプローチは、生成、コンパイル、および IL 分析されるコードの断片において、それらへの参照を同じ型の "synthetic"local vars で置き換えることでしょう。


もう一つの更新 - インスタンスメンバに依存するバーの補完が動作するようになりました。

私が行ったのは、型を (セマンティック経由で) 尋問し、次にすべての既存のメンバーに対して合成スタンドインメンバーを生成することでした。 このような 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 でパワーシェルを実行する機能
  • C# コンパイラは本当に速いです。私のマシンでは、インメモリアセンブリをコンパイルするのに約 0.5 秒かかります。キーストローク間の解析には十分な速度ではありませんが、補完リストのオンデマンド生成をサポートするのには十分な速度です。

LINQはまだ調べていないのですが。

emacs が C# のために持っているセマンティック・レキサ/パーサは、LINQ を "do" しないので、それはより大きな問題になるでしょう。

どのように解決するのですか?

C#のIDEで効率的に行う方法を説明します。

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

IDE がメソッド本体内の特定の式の型を調べる必要がある場合、たとえば "foo." と入力し、foo のメンバが何であるかを把握する必要がある場合、同じように、合理的にできる限り作業をスキップします。

を分析するパスから始めます。 ローカル変数 宣言のみを分析するパスから始めます。 このパスを実行すると、"scope" と "name" のペアから "type determiner" にマッピングが作成されます。この型決定器は、「必要であれば、このローカルの型を調べることができる」という概念を表すオブジェクトです。ローカルの型を調べるのは高くつくので、必要ならその作業を先延ばしにしたいのです。

これで、すべてのローカルのタイプを知ることができる、簡単に構築されたデータベースができました。そこで、"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() を解析し、 "what is the type of x"と尋ねます。 現在のコンテキストで "String"を検索する必要があります」と言う型決定子があります。現在の型にStringという型はないので、名前空間を探します。そこで、usingディレクティブを調べると、"using System"があり、SystemにStringの型があることがわかります。OK、それはxの型です。

次に、System.String のメタデータに ToCharArray の型を問い合わせると、それは System.Char[] であると言います。スーパーです。 ということで、yの型ができました。

ここで、quot;System.Char[]にWhereメソッドがあるか?" いいえ。そこで、usingディレクティブを調べます。

ここで、quot;OK, Where という名前の拡張メソッドがスコープ内に 1812 個ありますが、その中に System.Char[] と互換性のある型を持つ最初の形式パラメータがありますか?" そこで、変換性テストのラウンドを開始します。しかし、Where 拡張メソッドは 一般的な であるため、型推論を行う必要があります。

私は、拡張メソッドの最初の引数から不完全な推論を行うことを扱うことができる特別な型推論エンジンを書きました。型推論エンジンを実行し、Whereメソッドに IEnumerable<T> を受け取る Where メソッドがあること、そして System.Char[] から IEnumerable<System.Char> という推論ができるので、TはSystem.Charです。

このメソッドのシグネチャは Where<T>(this IEnumerable<T> items, Func<T, bool> predicate) であり、T が System.Char であることを知っています。 また、拡張メソッドの括弧内の最初の引数はラムダであることも分かっています。そこで、quot;形式パラメータfooはSystem.Charであると仮定する、というラムダ式型推論を起動し、ラムダの残りの部分を分析するときにこの事実を使用します。

これで、ラムダの本体である "foo." を解析するために必要な情報はすべて揃いました。 fooの型を調べると、ラムダバインダによるとSystem.Charであることがわかり、System.Charの型情報を表示して完了です。

そして、"トップレベル"の分析以外のすべてを行います。 キーストロークの間 . これが本当に厄介なところです。実際にすべての解析を書くのは難しいことではありません。 を十分に速くすることです。 にすることです。

がんばってください。