如何可靠地确定在devise时使用var声明的variables的types?
我正在为emacs中的C#完成(intellisense)工具。
这个想法是,如果用户键入一个片段,然后通过特定的按键组合来请求完成,完成工具将使用.NETreflection来确定可能的完成。
这样做需要知道正在完成的事物的types。 如果它是一个string,则有一组已知的可能的方法和属性; 如果它是一个Int32,它有一个单独的集合,依此类推。
使用语义,emacs中可用的代码词法分析器/parsing器包,我可以findvariables声明及其types。 鉴于此,使用reflection来获取types的方法和属性是很简单的,然后向用户提供选项列表。 (好吧, 在 emacs 里面做的不是很简单 ,但是使用在 emacs 里面运行powershell的过程变得容易多了,我写了一个自定义的.NET程序集来做reflection,把它加载到powershell中,然后在内部运行elisp emacs可以通过comint向powershell发送命令并读取响应,因此emacs可以快速得到reflection结果。)
当代码在正在完成的事情的声明中使用var
时,问题就到了。 这意味着types没有明确指定,完成将不起作用。
如何用var
关键字声明variables时,如何可靠地确定实际使用的types? 只是要清楚,我不需要在运行时确定它。 我想在“devise时间”确定它。
到目前为止,我有这些想法:
- 编译和调用:
- 提取声明语句,例如`var foo =“一个string值”;`
- 连接一个语句`foo.GetType();`
- 将生成的C#片段dynamic编译成一个新的程序集
- 将程序集加载到一个新的AppDomain中,运行framgment并获取返回types。
- 卸载并丢弃程序集
我知道如何做到这一切。 但是,对于编辑器中的每个完成请求,它听起来都非常重量级。
我想我每次都不需要一个新的AppDomain。 我可以重复使用一个AppDomain来处理多个临时程序集,并在多个完成请求中分摊设置和分解的成本。 这更多的是对基本想法的调整。
- 编译和检查IL
简单地将声明编译成模块,然后检查IL,以确定编译器推断的实际types。 这怎么可能? 我会用什么来检查IL?
有更好的想法吗? 注释? build议?
编辑 – 进一步思考,编译和调用是不可接受的,因为调用可能有副作用。 所以必须排除第一个选项。
另外,我想我不能假定.NET 4.0的存在。
更新 – 上面没有提到的正确答案,但Eric Lippert轻声指出,是实现一个完全保真types推断系统。 它是在devise时可靠地确定variablestypes的唯一方法。 但是,这也不容易。 因为我不想有任何幻想想要尝试构build这样的事情,所以我select了选项2的快捷方式 – 提取相关的声明代码,并编译它,然后检查生成的IL。
对于完成情况的公平子集,这实际上是有效的。
例如,假设在下面的代码段中, 是用户要求完成的位置。 这工作:
var x = "hello there"; x.?
完成意识到x是一个string,并提供了相应的选项。 它通过生成并编译以下源代码来完成此操作:
namespace N1 { static class dmriiann5he { // randomly-generated class name static void M1 () { var x = "hello there"; } } }
…然后用简单的reflection来检查IL。
这也适用:
var x = new XmlDocument(); x.?
引擎在生成的源代码中添加适当的使用条款,以便编译正确,然后IL检查是相同的。
这也适用于:
var x = "hello"; var y = x.ToCharArray(); var z = y.?
这只是意味着IL检查必须find第三个局部variables的types,而不是第一个。
和这个:
var foo = "Tra la la"; var fred = new System.Collections.Generic.List<String> { foo, foo.Length.ToString() }; var z = fred.Count; var x = z.?
…这只是前一个例子的一个更深层次。
但是,不起作用的是任何局部variables的完成,它们的初始化依赖于实例成员上的任何一点,或者局部方法参数。 喜欢:
var foo = this.InstanceMethod(); foo.?
也不是LINQ语法。
在我考虑通过绝对是“有限的devise”(有礼貌的言辞来完成)来解决它们之前,我将不得不考虑这些东西的价值。
通过依赖于方法参数或实例方法来解决问题的一种方法是在生成,编译和IL分析的代码片段中replace对具有相同types的“合成”局部variables的引用。
另一个更新 – 完成取决于实例成员的variables现在可以工作。
我所做的是询问types(通过语义),然后为所有现有成员生成合成替代成员。 对于像这样的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中学习本地var nnn的types,如下所示:
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); } } }
所有的实例和静态types成员在骨架代码中都可用。 它编译成功。 此时,通过reflection来确定局部variables的types是非常简单的。
是什么使这成为可能的是:
- 在emacs中运行powershell的能力
- C#编译器非常快。 在我的机器上,大约需要0.5s来编译内存中的程序集。 对于键击分析来说速度不够快,但速度足以支持按需生成完成列表。
我还没有看过LINQ。
这将是一个更大的问题,因为语义词法分析器/parsing器emacs已经为C#,不“做”LINQ。
我可以为你描述我们如何在“真正的”C#IDE中有效地做到这一点。
我们做的第一件事就是运行一个只分析源代码中“顶级”东西的通行证。 我们跳过所有的方法体。 这使我们能够快速build立一个关于程序源代码中的名称空间,types和方法(以及构造函数等)的信息的数据库。 分析每个方法体中的每一行代码,如果要在击键之间执行,将会花费太长的时间。
当IDE需要计算方法体内特定expression式的types时 – 比如说你已经input了“foo”。 我们需要弄清楚foo的成员是什么 – 我们也是这样做的; 我们可以跳过尽可能多的工作。
我们从一个分析开始,只分析该方法中的局部variables声明。 当我们运行该通道时,我们将一对“范围”和“名称”映射到“types确定器”。 “types确定器”是一个对象,表示“我可以制定出本地的types,如果我需要”的概念。 制定一个地方的types可能是昂贵的,所以我们希望推迟这项工作,如果我们需要的话。
我们现在有一个懒散的数据库,可以告诉我们每个本地的types。 所以,回到那个“foo”。 – 我们计算出相关expression式所在的语句 ,然后针对该语句运行语义分析器。 例如,假设你有方法体:
String x = "hello"; var y = x.ToCharArray(); var z = from foo in y where foo.
现在我们需要弄清楚foo是chartypes的。 我们build立一个包含所有元数据,扩展方法,源代码types等的数据库。 我们build立一个数据库,它有x,y和ztypes的确定器。 我们分析包含有趣expression的陈述。 我们首先将它从语法上转换为
var z = y.Where(foo=>foo.
为了弄出foo的types,我们首先要知道y的types。 所以在这一点上,我们要求types确定器“y是什么types”? 然后启动一个expression式求值器来分析x.ToCharArray()并询问“x的types是什么”? 我们有一个types确定器,用于在当前上下文中显示“我需要查找”string。 在当前types中没有stringtypes,所以我们查看命名空间。 它不在那里,所以我们看使用指令,发现有一个“使用系统”,该系统有一个stringtypes。 好的,这就是x的types。
然后,我们查询System.String的元数据的ToCharArray的types,它说这是一个System.Char []。 超。 所以我们有一个types的y。
现在我们问“System.Char []有一个方法在哪里? 不,所以我们看看使用指令; 我们已经预先计算了一个包含所有可能使用的扩展方法的元数据的数据库。
现在我们说“OK,有十八个名为Where的扩展方法,它们中的任何一个都有第一个types与System.Char []?兼容的forms参数” 所以我们开始了一轮可兑换testing。 但是,Where扩展方法是通用的 ,这意味着我们必须进行types推断。
我写了一个特殊types的推断引擎,可以处理从第一个参数到扩展方法的不完全推理。 我们运行types推理器,发现有一个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。 因此,我们启动一个lambdaexpression式推理器,它说“forms参数foo被假定为System.Char”,在分析其余的lambda时使用这个事实。
现在我们有了所有的信息来分析lambda的实体,即“foo”。 我们查看foo的types,我们发现根据lambda结合体是System.Char,我们完成了; 我们显示System.Char的types信息。
除了按键之间的“顶级”分析之外,我们还可以做所有的事情。 这是真正的棘手的一点。 其实写出所有的分析并不难; 它使得速度足够快 ,你可以在打字速度这是真正棘手的一点。
祝你好运!
我可以粗略地告诉你,Delphi IDE如何与Delphi编译器一起工作来实现intellisense(代码洞察是Delphi所称的)。 这不是100%适用于C#,但它是一个值得考虑的有趣的方法。
Delphi中的大多数语义分析都是在parsing器本身完成的。 expression式在被parsing时被键入,除非这不是一件容易的事情 – 在这种情况下,先行parsing被用来计算出什么意图,然后在parsing中使用这个决定。
parsing主要是LL(2)recursion下降,除了使用运算符优先级parsing的expression式。 Delphi的一个独特之处在于它是一种单通道语言,所以构造需要在使用之前进行声明,所以不需要顶层通道来将这些信息输出。
这些function的组合意味着parsing器几乎包含了代码洞察所需的全部信息。 它的工作方式是这样的:IDE通知编译器的词法分析器(需要代码洞察的地方),词法分析器把它变成一个特殊的标记(它被称为kibitz标记)。 每当parsing器遇到这个令牌(可能在任何地方),它就知道这是将所有返回给编辑器的信息发送回去的信号。 它使用longjmp来完成,因为它是用C语言编写的; 它所发挥的作用是通知最终的调用者(即语法上下文)的kibitz点以及这个点所需要的所有符号表。 因此,例如,如果上下文在作为方法参数的expression式中,我们可以检查方法重载,查看参数types,并将有效符号过滤为只能parsing为该参数types的那些符号(this在下拉菜单中减less了很多不相干的东西)。 如果它在嵌套的作用域上下文中(例如,在“。”之后),parsing器将把对该作用域的引用交还给IDE,并且IDE可以枚举在该作用域中find的所有符号。
其他的事情也完成了; 例如,如果kibitz标记不在其范围内,方法体将被跳过 – 这是乐观的,如果跳过标记则回滚。 Delphi中的扩展方法(类助手)相当于拥有一种版本caching,所以他们的查找速度相当快。 但是Delphi的genericstypes推断比C#弱得多。
现在,针对具体问题:推断用var
声明的variablestypes与Pascal推断常量types的方式相同。 它来自初始化expression式的types。 这些types是从下到上构build的。 如果x
的types是Integer
, y
的types是Double
,那么x + y
将是Double
types的,因为这些是语言的规则; 等等。按照这些规则,直到右边有完整expression式的types,这就是左边符号的types。
如果您不想编写自己的parsing器来构build抽象语法树,则可以使用来自SharpDevelop或MonoDevelop的parsing器,这两个parsing器都是开源的。
智能感知系统通常使用抽象语法树(Abstract Syntax Tree)来表示代码,这使得他们能够以与编译程序相同的方式来parsing被分配给“var”variables的函数的返回types。 如果您使用VS智能感知,您可能会注意到,只有在input有效的(可parsing的)赋值expression式之后,它才会给出var的types。 如果expression式仍然不明确(例如,它不能完全推断expression式的generics参数),那么vartypes将无法parsing。 这可能是一个相当复杂的过程,因为您可能需要深入到树中才能parsingtypes。 例如:
var items = myList.OfType<Foo>().Select(foo => foo.Bar);
返回types是IEnumerable<Bar>
,但解决这个需要知道:
- myList是实现
IEnumerable
的types。 - 有一个适用于IEnumerable的扩展方法
OfType<T>
。 - 得到的值是
IEnumerable<Foo>
并有一个适用于此的扩展方法Select
。 - lambdaexpression式
foo => foo.Bar
具有Footypes的参数foo。 这是通过Select的使用来推断的,该Select使用Func<TIn,TOut>
并且由于TIn是已知的(Foo),所以可以推断出foo的types。 - typesFoo有一个属性Bar,它是Bar的types。 我们知道Select返回
IEnumerable<TOut>
,并且可以从lambdaexpression式的结果中推断出TOut,所以得到的项目types必须是IEnumerable<Bar>
。
由于您的目标是Emacs,因此最好从CEDET套件开始。 所有Eric Lippert在CEDET / Semantic工具中的代码分析器中已经涵盖了所有的细节。 还有一个C#parsing器(可能需要一点TLC),所以缺less的部分与调整C#所需的部分有关。
基本行为是在核心algorithm中定义的,这些核心algorithm依赖于以每种语言为基础定义的可重载函数。 完成引擎的成功取决于已经完成了多less调整。 以c ++为指导,获得类似于C ++的支持应该不会太糟糕。
Daniel的答案build议使用MonoDevelop进行parsing和分析。 这可能是替代现有C#parsing器的替代机制,也可能用于扩充现有的parsing器。
要做好,这是一个难题。 基本上你需要通过大部分的lexing / parsing / typechecking来build立语言规范/编译器的模型,然后build立一个你可以查询的源代码的内部模型。 Eric详细描述了C#。 您可以随时下载F#编译器源代码(F#CTP的一部分),并查看service.fsi
以查看F#编译器暴露的接口,F#语言服务用于提供智能感知,推断types的工具提示等。如果你已经有编译器作为一个API来调用,它给出了一个可能的“接口”的意义。
另一种方法是按照原样重新使用编译器,然后使用reflection或查看生成的代码。 从编译器需要“完整的程序”获得编译输出的观点来看,这是有问题的,而在编辑器中编辑源代码时,通常只有“部分程序”尚未parsing,还有所有的方法等等
总之,我认为“低预算”的版本很难做好,“真正的”版本很难做好。 (这里'硬'在这里既是'努力'也是'技术难度')
NRefactory会为你做这个。
对于解决scheme“1”,您可以在.NET 4中使用新的工具来快速轻松地完成此操作。 所以,如果你可以把你的程序转换成.NET 4,那将是你最好的select。