Che*_*eso 109 c# reflection type-inference appdomain
我正在为emacs中的C#完成(intellisense)工具.
这个想法是,如果用户键入片段,然后通过特定的击键组合要求完成,则完成工具将使用.NET反射来确定可能的完成.
这样做需要知道正在完成的事物的类型.如果它是一个字符串,那么有一组已知的可能方法和属性; 如果它是一个Int32,它有一个单独的集,依此类推.
使用语义,emacs中提供的代码词法分析器/解析器包,我可以找到变量声明及其类型.鉴于此,可以直接使用反射来获取类型上的方法和属性,然后向用户显示选项列表.(好吧,在 emacs中做的不是那么简单,但是使用在 emacs 中运行PowerShell进程的能力,它变得容易得多.我编写了一个自定义.NET程序集来进行反射,将其加载到powershell中,然后在内部运行elisp emacs可以通过comint向powershell发送命令并读取响应.因此,emacs可以快速获得反射结果.)
当代码var在声明正在完成的事物中使用时,问题就会到来.这意味着未明确指定类型,并且完成将不起作用.
当使用var关键字声明变量时,如何可靠地确定实际使用的类型?为了清楚起见,我不需要在运行时确定它.我想在"设计时"确定它.
到目前为止,我有这些想法:
我知道怎么做这一切.但对于编辑器中的每个完成请求,它听起来非常重量级.
我想我每次都不需要新的AppDomain.我可以为单个AppDomain重复使用多个临时程序集,并在多个完成请求中分摊设置和拆除它的成本.这更像是对基本理念的调整.
只需将声明编译到模块中,然后检查IL,以确定编译器推断出的实际类型.这怎么可能?我会用什么来检查IL?
还有更好的想法吗?评论?建议?
编辑 - 进一步考虑这一点,编译和调用是不可接受的,因为调用可能有副作用.因此必须排除第一种选择.
此外,我认为我不能假设.NET 4.0的存在.
更新 - 上面没有提到的正确答案,但Eric Lippert温和地指出,是实现完全保真的类型推理系统.它是在设计时可靠地确定var类型的唯一方法.但是,这也不容易.因为我苦于没有幻想,我要试图建立这样的事情,我把选项2的快捷方式-提取相关声明代码,并编译它,然后检查所产生的IL.
这实际上适用于完成方案的公平子集.
例如,假设在下面的代码片段中,?是用户要求完成的位置.这有效:
var x = "hello there";
x.?
Run Code Online (Sandbox Code Playgroud)
完成意识到x是一个String,并提供适当的选项.它通过生成并编译以下源代码来实现:
namespace N1 {
static class dmriiann5he { // randomly-generated class name
static void M1 () {
var x = "hello there";
}
}
}
Run Code Online (Sandbox Code Playgroud)
......然后用简单的反射检查IL.
这也有效:
var x = new XmlDocument();
x.?
Run Code Online (Sandbox Code Playgroud)
引擎将相应的using子句添加到生成的源代码中,以便正确编译,然后IL检查是相同的.
这也有效:
var x = "hello";
var y = x.ToCharArray();
var z = y.?
Run Code Online (Sandbox Code Playgroud)
它只是意味着IL检查必须找到第三个局部变量的类型,而不是第一个.
还有这个:
var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var x = z.?
Run Code Online (Sandbox Code Playgroud)
......比前面的例子更深一层.
但是,什么不起作用是完成任何局部变量,其初始化取决于实例成员或本地方法参数的任何点.喜欢:
var foo = this.InstanceMethod();
foo.?
Run Code Online (Sandbox Code Playgroud)
也不是LINQ语法.
在我考虑通过什么绝对是"有限的设计"(黑客的礼貌用语)来完成它之前,我将不得不考虑这些东西是多么有价值.
解决依赖于方法参数或实例方法的问题的方法是在生成,编译然后进行IL分析的代码片段中替换对具有相同类型的"合成"本地变量的那些事物的引用.
另一个更新 - 依赖于实例成员的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...
Run Code Online (Sandbox Code Playgroud)
...生成的代码被编译,以便我可以从输出IL中学习本地var 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);
}
}
}
Run Code Online (Sandbox Code Playgroud)
骨架代码中提供了所有实例和静态类型成员.它编译成功.此时,通过Reflection可以直接确定局部变量的类型.
使这成为可能的是:
我还没有看过LINQ.
这将是一个更大的问题,因为语义词法分析器/解析器emacs具有C#,不"做"LINQ.
Eri*_*ert 202
我可以为您描述我们如何在"真正的"C#IDE中有效地完成这项工作.
我们要做的第一件事是运行一个只分析源代码中"顶级"内容的传递.我们跳过所有方法体.这使我们能够快速建立一个信息数据库,该数据库包含程序源代码中的命名空间,类型和方法(以及构造函数等).如果你试图在击键之间进行分析,那么分析每个方法体中的每一行代码都会花费太长时间.
当IDE需要计算出方法体内特定表达式的类型时 - 比如说你输入了"foo".我们需要弄清楚foo的成员是什么 - 我们做同样的事情; 我们可以合理地跳过尽可能多的工作.
我们从一个传递开始,该传递仅分析该方法中的局部变量声明.当我们运行该传递时,我们从一对"范围"和"名称"到"类型确定器"进行映射."类型确定器"是表示"如果需要,我可以计算出本地类型"这一概念的对象.计算出本地的类型可能很昂贵,所以如果需要,我们希望推迟这项工作.
我们现在有一个懒惰的数据库,可以告诉我们每个本地的类型.所以,回到那个"foo".- 我们找出相关表达式所在的语句,然后针对该语句运行语义分析器.例如,假设您有方法体:
String x = "hello";
var y = x.ToCharArray();
var z = from foo in y where foo.
Run Code Online (Sandbox Code Playgroud)
现在我们需要弄清楚foo是char类型.我们构建了一个包含所有元数据,扩展方法,源代码类型等的数据库.我们构建了一个具有x,y和z类型确定器的数据库.我们分析包含有趣表达式的语句.我们首先将其语法转换为
var z = y.Where(foo=>foo.
Run Code Online (Sandbox Code Playgroud)
为了计算foo的类型,我们必须首先知道y的类型.所以在这一点上我们问类型确定器"y的类型是什么"?然后它启动一个表达式求值程序,它解析x.ToCharArray()并询问"x的类型是什么"?我们有一个类型确定器,它说"我需要在当前上下文中查找"字符串".当前类型中没有类型String,因此我们查看命名空间.它不存在,所以我们查看using指令并发现有一个"使用系统"并且System有一个String类型.好的,这就是x的类型.
然后,我们查询System.String的ToCharArray类型的元数据,并说它是System.Char [].超.所以我们有一个y类型.
现在我们问"System.Char []有一个方法在哪里?" 不,所以我们查看使用指令; 我们已经预先计算了一个数据库,其中包含可能使用的扩展方法的所有元数据.
现在我们说"好了,有十八个扩展方法名为Where in scope,是否有任何第一个形式参数,其类型与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.我们也知道扩展方法括号内的第一个参数是lambda.所以我们启动一个lambda表达式类型推导器,它说"形式参数foo被假定为System.Char",在分析lambda的其余部分时使用这个事实.
我们现在拥有分析lambda体的所有信息,即"foo".我们查找foo的类型,我们发现根据lambda绑定器它是System.Char,我们已经完成了; 我们显示System.Char的类型信息.
除了按键之间的"顶级"分析,我们会做所有事情.这真是棘手的一点.实际上写所有分析并不难; 它的速度足够快,你可以在打字速度上做到这一点,这是真正棘手的一点.
祝好运!
Bar*_*lly 15
我可以大致告诉你Delphi IDE如何使用Delphi编译器来进行智能感知(代码洞察是Delphi所称的).它不是100%适用于C#,但它是一个值得考虑的有趣方法.
Delphi中的大多数语义分析都是在解析器本身中完成的.表达式在解析时被键入,除非这种情况不容易 - 在这种情况下,使用预读解析来计算出预期的内容,然后在解析中使用该决策.
除了使用运算符优先级解析的表达式之外,解析主要是LL(2)递归下降.Delphi的一个独特之处在于它是单通道语言,因此构造需要在使用之前声明,因此不需要顶层传递来传递信息.
这些功能组合意味着解析器具有代码洞察所需的大致所有信息,可用于需要它的任何点.它的工作方式是:IDE通知编译器的光标位置(需要代码洞察的点),并且词法分析器将其转换为特殊标记(称为kibitz标记).每当解析器遇到此令牌(可能在任何地方)时,它就知道这是将它返回给编辑器的所有信息发回的信号.它使用longjmp执行此操作,因为它是用C语言编写的; 它的作用是通知终极调用者所发现的kibitz点的句法结构(即语法上下文),以及该点所需的所有符号表.因此,例如,如果上下文位于表达式中,该表达式是方法的参数,我们可以检查方法重载,查看参数类型,并将有效符号过滤为只能解析为该参数类型的符号(这在下拉列表中减少了许多无关紧要的事情.如果它位于嵌套的作用域上下文中(例如,在"."之后),则解析器将返回对作用域的引用,IDE可以枚举该作用域中找到的所有符号.
其他的事情也做了; 例如,如果kibitz令牌不在其范围内,则跳过方法体 - 这是乐观地完成的,并且如果它跳过令牌则回滚.相当于扩展方法 - Delphi中的类助手 - 有一种版本化缓存,因此它们的查找速度相当快.但是Delphi的泛型类型推断比C#弱得多.
现在,针对具体问题:推断声明的变量类型var等同于Pascal推断常量类型的方式.它来自初始化表达式的类型.这些类型是自下而上构建的.如果x是类型Integer,并且y是类型Double,那么x + y将是类型Double,因为那些是语言的规则; 您可以遵循这些规则,直到右侧有完整表达式的类型,这就是您在左侧用于符号的类型.
| 归档时间: |
|
| 查看次数: |
29426 次 |
| 最近记录: |