代码之家  ›  专栏  ›  技术社区  ›  sergeb

如何可靠地确定在设计时使用var声明的变量的类型?

  •  109
  • sergeb  · 技术社区  · 5 年前

    我正在为Emacs的C开发一个完成(intellisense)工具。

    其思想是,如果用户键入一个片段,然后通过特定的按键组合请求完成,完成工具将使用.NET反射来确定可能的完成。

    这样做需要知道正在完成的事情的类型。如果它是一个字符串,那么有一组已知的可能的方法和属性;如果它是一个int32,那么它有一个单独的集合,依此类推。

    使用emacs中可用的代码lexer/parser包semantic,我可以找到变量声明及其类型。考虑到这一点,可以直接使用反射来获取类型上的方法和属性,然后将选项列表呈现给用户。(好,不完全 直截了当的 在内部 Emacs,但使用 the ability to run a powershell process inside emacs 更容易。我编写了一个自定义的.NET程序集来进行反射,将其加载到PowerShell中,然后在Emacs中运行的elisp可以通过ComInt向PowerShell发送命令并读取响应。因此,Emacs可以快速获得反射结果。)

    当代码使用 var 在完成的事情的声明中。这意味着没有显式地指定类型,完成将不起作用。

    当变量声明为 var 关键字?为了清楚起见,我不需要在运行时确定它。我想在“设计时”确定它。

    到目前为止,我有以下想法:

    1. 编译和调用:
      • 提取声明语句,例如'var foo=“a string value”;。`
      • 连接语句'foo.gettype();。`
      • 动态编译生成的C将其拆分为新程序集
      • 将程序集加载到新的AppDomain中,运行framment并获取返回类型。
      • 卸载并丢弃组件

      我知道怎么做这些。但对于编辑器中的每个完成请求来说,这听起来都是非常重要的。

      我想我不需要每次都有新的AppDomain。我可以为多个临时程序集重复使用一个AppDomain,并分摊设置它的成本。 在多个完成请求中向上和向下分解它。这更是对基本理念的一种调整。

    2. 编译和检查IL

      只需将声明编译到模块中,然后检查IL,以确定编译器推断出的实际类型。这怎么可能?我用什么检查白细胞介素?

    有什么好主意吗?评论?建议?


    编辑 -进一步考虑一下,编译和调用是不可接受的,因为调用可能有副作用。所以必须排除第一种选择。

    另外,我认为我不能假设.NET 4.0的存在。


    更新 -正确的答案,上面没有提到,但由埃里克利珀特轻轻指出,是要实现一个完全保真的类型推理系统。它是在设计时可靠地确定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检查必须找到第三个局部变量的类型,而不是第一个。

    而这:

    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语法。

    在我考虑通过“有限的设计”(礼貌地说是黑客)来解决这些问题之前,我必须先考虑这些问题的价值。

    解决方法参数或实例方法依赖性问题的一种方法是,在生成、编译和IL分析的代码片段中,用相同类型的“合成”局部变量替换对这些内容的引用。


    另一个更新 -依赖实例成员的var的完成现在可以工作了。

    我所做的是询问类型(通过语义),然后为所有现有成员生成合成的代理成员。对于这样的缓冲区:

    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的类型,如下所示:

    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);
          }
      }
    }
    

    所有实例和静态类型成员都可以在骨架代码中使用。它编译成功。在这一点上,通过反射直接确定局部VaR的类型。

    使这成为可能的是:

    • 在Emacs中运行PowerShell的能力
    • C编译器非常快。在我的机器上,编译内存中的程序集大约需要0.5秒。对于击键之间的分析,速度不够快,但足以支持按需生成完成列表。

    我还没看过林肯。
    这将是一个更大的问题,因为语义lexer/parser emacs对于c没有“做”linq。

    8 回复  |  直到 5 年前
        1
  •  202
  •   Cheeso    14 年前

    我可以为您描述我们如何在“真实”的C ide中有效地做到这一点。

    我们首先要做的是运行一个pass,它只分析源代码中的“顶级”内容。我们跳过所有的方法体。这允许我们快速建立一个数据库,其中包含关于程序源代码中的名称空间、类型和方法(以及构造函数等)的信息。如果您试图在击键之间进行分析,那么分析每个方法体中的每一行代码将花费太长的时间。

    当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建立了一个类型为determiners的数据库。我们分析了包含有趣表达式的语句。我们从语法上把它转换为

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

    为了确定foo的类型,我们首先必须知道y的类型,所以在这一点上,我们问类型决定者“y的类型是什么?”然后它启动一个表达式计算器,解析x.tochararray()并询问“x的类型是什么?”我们有一个类型限定符,上面写着“我需要在当前上下文中查找”string“。当前类型中没有类型字符串,因此我们查看命名空间。它也不存在,所以我们查看using指令,发现有一个“using system”,该系统有一个类型字符串。好的,这就是x的类型。

    然后,我们查询system.string的元数据以获取tochararray的类型,它说它是system.char[]。超级的。所以我们有一个Y型。

    现在我们问“System.char[]有方法在哪里?”不,所以我们查看using指令;我们已经预计算了一个数据库,其中包含了可能使用的扩展方法的所有元数据。

    现在我们说“好的,有十八个扩展方法在作用域的何处命名,它们中有没有第一个形式参数的类型与system.char[]兼容?”所以我们开始了一轮可兑换性测试。但是,扩展方法的位置 通用的 这意味着我们必须进行类型推断。

    我已经编写了一个特殊的类型推理引擎,它可以处理从第一个参数到扩展方法的不完全推理。我们运行类型推断器,发现有一个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的类型信息。

    除了“顶层”分析,我们什么都做 在击键之间 . 这才是真正棘手的问题。事实上,写下所有的分析并不难,它正在使它 足够快 你可以以打字的速度来完成,这才是真正棘手的一点。

    祝你好运!

        2
  •  15
  •   Barry Kelly    14 年前

    我可以大致告诉您DelphiIDE如何与Delphi编译器一起工作来实现IntelliSense(代码洞察是Delphi所称的代码洞察)。这并非100%适用于C,但这是一种值得考虑的有趣方法。

    Delphi中的大多数语义分析都是在解析器本身中完成的。表达式在解析时被类型化,除了不容易实现的情况——在这种情况下,使用先行分析来计算出预期的结果,然后在解析中使用该决策。

    除了使用运算符优先级解析的表达式外,解析主要是ll(2)递归下降。Delphi的一个独特之处是它是一种单通道语言,因此在使用之前需要声明结构,因此不需要顶级的通道来显示信息。

    这些特性的组合意味着,对于任何需要的地方,解析器都大致拥有代码洞察所需的所有信息。它的工作方式是这样的:IDE通知编译器的lexer光标的位置(需要代码洞察的地方),lexer将其转换为一个特殊的标记(称为kibitz标记)。每当解析器遇到这个令牌(可能在任何地方)时,它就知道这是一个将它拥有的所有信息发送回编辑器的信号。它使用longjmp来实现这一点,因为它是用C语言编写的;它所做的是通知最终调用方kibitz点所在的句法结构(即语法上下文)以及该点所需的所有符号表。例如,如果上下文在一个表达式中,该表达式是一个方法的参数,那么我们可以检查方法重载,查看参数类型,并将有效符号筛选为只能解析为该参数类型的符号(这会减少下拉列表中许多不相关的问题)。如果它在嵌套的作用域上下文中(例如“.”之后),解析器将返回对该作用域的引用,并且IDE可以枚举在该作用域中找到的所有符号。

    其他事情也可以做;例如,如果kibitz令牌不在其范围内,方法体将被跳过-这是乐观地完成的,如果跳过令牌,则回滚。相当于扩展方法(Delphi中的类助手)有一种版本化的缓存,因此它们的查找速度相当快。但是Delphi的一般类型推理比C弱得多。

    现在,具体问题是:推断用 var 相当于帕斯卡推断常量类型的方式。它来自初始化表达式的类型。这些类型是自下而上构建的。如果 x 属于类型 Integer y 属于类型 Double 然后 x + y 将是类型 双重的 ,因为这些是语言等的规则,所以您必须遵循这些规则,直到在右侧有完整表达式的类型,这是用于左侧符号的类型。

        3
  •  7
  •   Daniel Plaisted    14 年前

    如果您不想编写自己的解析器来构建抽象语法树,那么您可以从以下两个方面来看使用解析器: SharpDevelop MonoDevelop ,两者都是开源的。

        4
  •  4
  •   Dan Bryant    14 年前

    IntelliSense系统通常使用抽象语法树表示代码,这允许它们以与编译器相同的方式或多或少地解析分配给“var”变量的函数的返回类型。如果使用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. lambda表达式 foo => foo.Bar 具有foo类型的参数foo。这是由select的用法推断的,它采用 Func<TIn,TOut> 由于已知锡(foo),可以推断foo的类型。
    5. foo类型有一个属性栏,它是bar类型。我们知道Select返回 IEnumerable<TOut> 并且可以从lambda表达式的结果推断tout,因此生成的项类型必须是 IEnumerable<bar> .
        5
  •  4
  •   Eric    14 年前

    既然您的目标是Emacs,最好从cedet套件开始。Eric Lippert在CEDET/语义工具中的代码分析器中已经覆盖了所有的细节。还有一个C解析器(可能需要一点TLC),因此唯一缺少的部分与为C调优必要的部分有关。

    基本行为在核心算法中定义,这些核心算法依赖于基于每种语言定义的可重载函数。完成引擎的成功取决于完成了多少优化。以C++为指导,获得类似C++的支持不应该太糟糕。

    丹尼尔的回答建议使用MonoDevelop进行解析和分析。这可以是一种替代机制,而不是现有的C解析器,也可以用来扩充现有的解析器。

        6
  •  2
  •   Brian    14 年前

    做得好是个难题。基本上,您需要通过大多数的词法/解析/类型检查来为语言规范/编译器建模,并构建一个源代码的内部模型,然后您可以查询它。埃里克为C详细描述了这一点。您可以下载f编译器源代码(f ctp的一部分)并查看 service.fsi 要查看f语言服务为提供intellisense、推断类型的工具提示等而使用的f编译器中暴露的接口,它会给出一种可能的“接口”的感觉,如果您已经将编译器作为API调用。

    另一种方法是像描述的那样重新使用编译器,然后使用反射或查看生成的代码。从您需要“完整程序”从编译器获取编译输出的角度来看,这是有问题的,而在编辑器中编辑源代码时,您通常只有“部分程序”尚未解析,尚未实现所有方法等。

    总之,我认为“低预算”版本很难做得好,“真正”版本非常, 非常 很难做好。(这里“困难”衡量的是“努力”和“技术难度”。)

        7
  •  2
  •   erikkallen    12 年前

    NRefactory 会帮你的。

        8
  •  0
  •   Softlion    14 年前

    对于解决方案“1”,在.NET 4中有一个新的工具可以快速、轻松地执行此操作。 因此,如果您可以将程序转换为.NET 4,这将是您的最佳选择。