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

发布于 2024-09-01 12:01:22 字数 4325 浏览 2 评论 0原文

我正在 emacs 中为 C# 开发一个完成(智能感知)工具。

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

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

使用语义(emacs 中提供的代码词法分析器/解析器包),我可以找到变量声明及其类型。鉴于此,使用反射来获取类型的方法和属性,然后向用户呈现选项列表是很简单的。 (好吧,在 emacs 中执行起来不太简单,但是使用 在 emacs 中运行 powershell 进程的能力,它变得更加容易。我编写了一个自定义的 .NET 程序集来进行反射,将其加载到powershell中,然后emacs中运行的elisp可以通过comint向powershell发送命令并读取响应,因此emacs可以快速获得反射的结果。)

当代码使用var 在正在完成的事情的声明中。这意味着未明确指定类型,并且完成将不起作用。

当使用 var 关键字声明变量时,如何可靠地确定所使用的实际类型?需要明确的是,我不需要在运行时确定它。我想在“设计时”确定它。

到目前为止我有这些想法:

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

    我知道如何做到这一切。但对于编辑器中的每个完成请求来说,这听起来非常重量级。

    我想我不需要每次都需要一个全新的 AppDomain。我可以将单个 AppDomain 重新用于多个临时程序集,并分摊设置它的成本 跨多个完成请求将其向上和向下拆卸。这更多的是对基本想法的调整。

  2. 编译并检查 IL

    只需将声明编译到模块中,然后检查 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 分析的代码片段中,替换对具有相同类型的“合成”局部变量的引用。


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

我所做的是询问类型(通过语义),然后为所有现有成员生成合成替代成员。对于这样的 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 的类型,如下所示:

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

所有实例和静态类型成员都可以在骨架代码。它编译成功。此时,通过反射确定局部变量的类型就很简单了。

使这一切成为可能的是:

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

我还没有研究过 LINQ。
这将是一个更大的问题,因为 emacs 的语义词法分析器/解析器适用于 C#,不能“执行”LINQ。

I'm working on a completion (intellisense) facility for C# in emacs.

The idea is, if a user types a fragment, then asks for completion via a particular keystroke combination, the completion facility will use .NET reflection to determine the possible completions.

Doing this requires that the type of the thing being completed, be known. If it's a string, there's a known set of possible methods and properties; if it's an Int32, it has a separate set, and so on.

Using semantic, a code lexer/parser package available in emacs, I can locate the variable declarations, and their types. Given that, it's straightforward to use reflection to get the methods and properties on the type, and then present the list of options to the user. (Ok, not quite straightforward to do within emacs, but using the ability to run a powershell process inside emacs, it becomes much easier. I write a custom .NET assembly to do reflection, load it into the powershell, and then elisp running within emacs can send commands to powershell and read responses, via comint. As a result emacs can get the results of reflection quickly.)

The problem arrives when the code uses var in the declaration of the thing being completed. That means the type is not explicitly specified, and completion won't work.

How can I reliably determine the actual type used, when the variable is declared with the var keyword? Just to be clear, I don't need to determine it at runtime. I want to determine it at "Design time".

So far I have these ideas:

  1. compile and invoke:
    • extract the declaration statement, eg `var foo = "a string value";`
    • concatenate a statement `foo.GetType();`
    • dynamically compile the resulting C# fragment it into a new assembly
    • load the assembly into a new AppDomain, run the framgment and get the return type.
    • unload and discard the assembly

    I know how to do all this. But it sounds awfully heavyweight, for each completion request in the editor.

    I suppose I don't need a fresh new AppDomain every time. I could re-use a single AppDomain for multiple temporary assemblies, and amortize the cost of setting it
    up and tearing it down, across multiple completion requests. That's more a tweak of the basic idea.

  2. compile and inspect IL

    Simply compile the declaration into a module, and then inspect the IL, to determine the actual type that was inferred by the compiler. How would this be possible? What would I use to examine the IL?

Any better ideas out there? Comments? suggestions?


EDIT - thinking about this further, compile-and-invoke is not acceptable, because the invoke may have side effects. So the first option must be ruled out.

Also, I think I cannot assume the presence of .NET 4.0.


UPDATE - The correct answer, unmentioned above, but gently pointed out by Eric Lippert, is to implement a full fidelity type inference system. It;s the only way to reliably determine the type of a var at design time. But, it's also not easy to do. Because I suffer no illusions that I want to attempt to build such a thing, I took the shortcut of option 2 - extract the relevant declaration code, and compile it, then inspect the resulting IL.

This actually works, for a fair subset of the completion scenarios.

For example, suppose in the following code fragments, the ? is the position at which the user asks for completion. This works:

var x = "hello there"; 
x.?

The completion realizes that x is a String, and provides the appropriate options. It does this by generating and then compiling the following source code:

namespace N1 {
  static class dmriiann5he { // randomly-generated class name
    static void M1 () {
       var x = "hello there"; 
    }
  }
}

...and then inspecting the IL with simple reflection.

This also works:

var x = new XmlDocument();
x.? 

The engine adds the appropriate using clauses to the generated source code, so that it compiles properly, and then the IL inspection is the same.

This works, too:

var x = "hello"; 
var y = x.ToCharArray();    
var z = y.?

It just means the IL inspection has to find the type of the third local variable, instead of the first.

And this:

var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
    {
        foo,
        foo.Length.ToString()
    };
var z = fred.Count;
var x = z.?

...which is just one level deeper that the prior example.

But, what doesn't work is completion on any local variable whose initialization depends at any point on an instance member, or local method argument. Like:

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

Nor LINQ syntax.

I'll have to think about how valuable those things are before I consider addressing them via what is definitely a "limited design" (polite word for hack) for completion.

An approach to addressing the issue with dependencies on method arguments or instance methods would be to replace, in the fragment of code that gets generated, compiled and then IL analyzed, the references to those things with "synthetic" local vars of the same type.


Another Update - completion on vars that depend on instance members, now works.

What I did was interrogate the type (via semantic), and then generate synthetic stand-in members for all existing members. For a C# buffer like this:

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...

...the generated code that gets compiled, so that I can learn from the output IL the type of the local var nnn, looks like this:

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

All of the instance and static type members are available in the skeleton code. It compiles successfully. At that point, determining the type of the local var is straightforward via Reflection.

What makes this possible is:

  • the ability to run powershell in emacs
  • the C# compiler is really fast. On my machine, it takes about 0.5s to compile an in-memory assembly. Not fast enough for between-keystrokes analysis, but fast enough to support the on-demand generation of completion lists.

I haven't looked into LINQ yet.
That will be a much bigger problem because the semantic lexer/parser emacs has for C#, doesn't "do" LINQ.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

乙白 2024-09-08 12:01:22

我可以为您描述我们如何在“真正的”C# IDE 中高效地做到这一点。

我们要做的第一件事是运行一个过程,仅分析源代码中的“顶级”内容。我们跳过所有方法体。这使我们能够快速建立一个数据库,其中包含有关程序源代码中的名称空间、类型和方法(以及构造函数等)的信息。如果您尝试在击键之间进行分析,则分析每个方法主体中的每一行代码将花费太长时间。

当 IDE 需要计算方法体内特定表达式的类型时,假设您输入了“foo”。我们需要弄清楚 foo 的成员是什么——我们做同样的事情;我们会尽可能多地跳过工作。

我们从一个仅分析该方法中的局部变量声明的过程开始。当我们运行该过程时,我们会从一对“范围”和“名称”到“类型确定器”进行映射。 “类型确定器”是一个对象,表示“如果需要,我可以计算出此本地的类型”的概念。确定当地人的类型可能会很昂贵,因此如果需要的话我们希望推迟这项工作。

我们现在有了一个延迟构建的数据库,可以告诉我们每个本地的类型。那么,回到那个“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() 并询问“x 的类型是什么”?我们有一个类型确定器,它表示“我需要在当前上下文中查找“String””。当前类型中没有String类型,所以我们在命名空间中查找。它也不存在,所以我们查看 using 指令,发现有一个“using System”,并且 System 具有 String 类型。好的,这就是 x 的类型。

然后,我们在 System.String 的元数据中查询 ToCharArray 的类型,它表明它是一个 System.Char[]。极好的。所以我们有一个 y 的类型。

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

现在我们说“好吧,有十八打扩展方法,名为Where in range,它们中的任何一个都有第一个形式参数,其类型与System.Char[]兼容吗?”因此我们开始了一轮可兑换性测试。然而,Where 扩展方法是通用的,这意味着我们必须进行类型推断。

我编写了一个特殊的类型推理引擎,可以处理从第一个参数到扩展方法的不完整推理。我们运行类型推断器,发现有一个Where方法接受IEnumerable,并且我们可以从System.Char[]到IEnumerable进行推断;,所以 T 是 System.Char。

该方法的签名是 Where(this IEnumerableitems, Funcpredicate),我们知道 T 是 System.Char。我们还知道扩展方法括号内的第一个参数是 lambda。因此,我们启动一个 lambda 表达式类型推断器,表示“形式参数 foo 假定为 System.Char”,在分析 lambda 的其余部分时使用这一事实。

我们现在拥有分析 lambda 主体所需的所有信息,即“foo.”。我们查找 foo 的类型,根据 lambda 绑定器发现它是 System.Char,我们就完成了;我们显示 System.Char 的类型信息。

除了击键之间的“顶级”分析之外,我们会做所有事情。这才是真正棘手的地方。实际上写出所有的分析并不难;它使它足够快,以便您可以以打字速度完成操作,这是真正棘手的一点。

I can describe for you how we do that efficiently in the "real" C# IDE.

The first thing we do is run a pass which analyzes only the "top level" stuff in the source code. We skip all the method bodies. That allows us to quickly build up a database of information about what namespace, types and methods (and constructors, etc) are in the source code of the program. Analyzing every single line of code in every method body would take way too long if you're trying to do it between keystrokes.

When the IDE needs to work out the type of a particular expression inside a method body -- say you've typed "foo." and we need to figure out what are the members of foo -- we do the same thing; we skip as much work as we reasonably can.

We start with a pass which analyzes only the local variable declarations within that method. When we run that pass we make a mapping from a pair of "scope" and "name" to a "type determiner". The "type determiner" is an object that represents the notion of "I can work out the type of this local if I need to". Working out the type of a local can be expensive so we want to defer that work if we need to.

We now have a lazily-built database that can tell us the type of every local. So, getting back to that "foo." -- we figure out which statement the relevant expression is in and then run the semantic analyzer against just that statement. For example, suppose you have the method body:

String x = "hello";
var y = x.ToCharArray();
var z = from foo in y where foo.

and now we need to work out that foo is of type char. We build a database that has all the metadata, extension methods, source code types, and so on. We build a database that has type determiners for x, y and z. We analyze the statement containing the interesting expression. We start by transforming it syntactically to

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

In order to work out the type of foo we must first know the type of y. So at this point we ask the type determiner "what is the type of y"? It then starts up an expression evaluator which parses x.ToCharArray() and asks "what's the type of x"? We have a type determiner for that which says "I need to look up "String" in the current context". There is no type String in the current type, so we look in the namespace. It's not there either so we look in the using directives and discover that there's a "using System" and that System has a type String. OK, so that's the type of x.

We then query System.String's metadata for the type of ToCharArray and it says that it's a System.Char[]. Super. So we have a type for y.

Now we ask "does System.Char[] have a method Where?" No. So we look in the using directives; we have already precomputed a database containing all of the metadata for extension methods that could possibly be used.

Now we say "OK, there are eighteen dozen extension methods named Where in scope, do any of them have a first formal parameter whose type is compatible with System.Char[]?" So we start a round of convertibility testing. However, the Where extension methods are generic, which means we have to do type inference.

I've written a special type infererencing engine that can handle making incomplete inferences from the first argument to an extension method. We run the type inferrer and discover that there is a Where method that takes an IEnumerable<T>, and that we can make an inference from System.Char[] to IEnumerable<System.Char>, so T is System.Char.

The signature of this method is Where<T>(this IEnumerable<T> items, Func<T, bool> predicate), and we know that T is System.Char. Also we know that the first argument inside the parentheses to the extension method is a lambda. So we start up a lambda expression type inferrer that says "the formal parameter foo is assumed to be System.Char", use this fact when analyzing the rest of the lambda.

We now have all the information we need to analyze the body of the lambda, which is "foo.". We look up the type of foo, we discover that according to the lambda binder it is System.Char, and we're done; we display type information for System.Char.

And we do everything except the "top level" analysis between keystrokes. That's the real tricky bit. Actually writing all the analysis is not hard; it's making it fast enough that you can do it at typing speed that is the real tricky bit.

弥枳 2024-09-08 12:01:22

我可以大致告诉你Delphi IDE如何与Delphi编译器一起工作来进行智能感知(Delphi称之为代码洞察)。它并不 100% 适用于 C#,但它是一种值得考虑的有趣方法。

Delphi 中的大多数语义分析都是在解析器本身中完成的。表达式在解析时被键入,除非这并不容易的情况 - 在这种情况下,使用前瞻解析来计算出想要的内容,然后在解析中使用该决策。

解析主要是 LL(2) 递归下降,但表达式除外,表达式是使用运算符优先级进行解析的。 Delphi 的一个独特之处在于它是一种单遍语言,因此需要在使用之前声明构造,因此不需要顶级传递来带出该信息。

这种功能组合意味着解析器几乎拥有任何需要的地方的代码洞察所需的所有信息。它的工作方式是这样的:IDE 通知编译器的词法分析器光标的位置(需要代码洞察的点),词法分析器将其转换为特殊标记(称为 kibitz 标记)。每当解析器遇到这个标记(可能在任何地方)时,它就知道这是将其拥有的所有信息发送回编辑器的信号。它使用 longjmp 来完成此操作,因为它是用 C 编写的;它的作用是通知最终调用者基比兹点所在的语法结构(即语法上下文)类型,以及该点所需的所有符号表。例如,如果上下文位于作为方法参数的表达式中,我们可以检查方法重载,查看参数类型,并将有效符号过滤为仅那些可以解析为该参数类型的符号(此减少了下拉菜单中许多不相关的内容)。如果它位于嵌套作用域上下文中(例如,在“.”之后),则解析器将返回对该作用域的引用,并且 IDE 可以枚举在该作用域中找到的所有符号。

其他的事情也都做了;例如,如果 kibitz 标记不在其范围内,则方法体将被跳过 - 这是乐观地完成的,如果跳过标记则回滚。扩展方法的等价物——Delphi 中的类帮助器——有一种版本化缓存,因此它们的查找速度相当快。但Delphi 的泛型类型推断比C# 弱得多。

现在,具体问题是:推断用 var 声明的变量的类型相当于 Pascal 推断常量类型的方式。它来自初始化表达式的类型。这些类型是自下而上构建的。如果 x 的类型为 Integer,并且 y 的类型为 Double,则 x + y< /code> 将是 Double 类型,因为这些是语言的规则;等等。您遵循这些规则,直到您拥有右侧完整表达式的类型,这就是您用于左侧符号的类型。

I can tell you roughly how the Delphi IDE works with the Delphi compiler to do intellisense (code insight is what Delphi calls it). It's not 100% applicable to C#, but it's an interesting approach which deserves consideration.

Most semantic analysis in Delphi is done in the parser itself. Expressions are typed as they are parsed, except for situations where this is not easy - in which case look-ahead parsing is used to work out what's intended, and then that decision is used in the parse.

The parse is largely LL(2) recursive descent, except for expressions, which are parsed using operator precedence. One of the distinct things about Delphi is that it's a single-pass language, so constructs need to be declared before being used, so no top-level pass is needed to bring that information out.

This combination of features means that the parser has roughly all the information needed for code insight for any point where it's needed. The way it works is this: the IDE informs the compiler's lexer of the position of the cursor (the point where code insight is desired) and the lexer turns this into a special token (it's called the kibitz token). Whenever the parser meets this token (which could be anywhere) it knows that this is the signal to send back all the information it has back to the editor. It does this using a longjmp because it's written in C; what it does is it notifies the ultimate caller of the kind of syntactic construct (i.e. grammatical context) the kibitz point was found in, as well as all the symbolic tables necessary for that point. So for example, if the context is in an expression which is an argument to a method, the we can check the method overloads, look at the argument types, and filter the valid symbols to only those which can resolve to that argument type (this cuts down in a lot of irrelevant cruft in the drop-down). If it's in a nested scope context (e.g. after a "."), the parser will have handed back a reference to the scope, and the IDE can enumerate all the symbols found in that scope.

Other things are also done; for example, method bodies are skipped if the kibitz token does not lie in their range - this is done optimistically, and rolled back if it skipped over the token. The equivalent of extension methods - class helpers in Delphi - have a kind of versioned cache, so their lookup is reasonably fast. But Delphi's generic type inference is much weaker than C#'s.

Now, to the specific question: inferring the types of variables declared with var is equivalent to the way Pascal infers the type of constants. It comes from the type of the initialization expression. These types are built from the bottom up. If x is of type Integer, and y is of type Double, then x + y will be of type Double, because those are the rules of the language; etc. You follow these rules until you have a type for the full expression on the right hand side, and that's the type you use for the symbol on the left.

等风来 2024-09-08 12:01:22

如果您不想编写自己的解析器来构建抽象语法树,您可以考虑使用 SharpDevelopMonoDevelop,两者都是开源的。

If you don't want to have to write your own parser to build the abstract syntax tree, you could look at using the parsers from either SharpDevelop or MonoDevelop, both of which are open source.

晌融 2024-09-08 12:01:22

智能感知系统通常使用抽象语法树来表示代码,这使得它们能够以与编译器大致相同的方式解析分配给“var”变量的函数的返回类型。如果您使用 VS Intellisense,您可能会注意到,在您完成输入有效(可解析)的赋值表达式之前,它不会为您提供 var 的类型。如果表达式仍然不明确(例如,它无法完全推断表达式的通用参数),则 var 类型将无法解析。这可能是一个相当复杂的过程,因为您可能需要深入树才能解析类型。例如:

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

返回类型是 IEnumerable,但解决这个问题需要知道:

  1. myList 是实现 IEnumerable 的类型。
  2. 有一个适用于 IEnumerable 的扩展方法 OfType
  3. 结果值为 IEnumerable 并且有一个适用于此的扩展方法 Select
  4. lambda 表达式 foo =>; foo.Bar 具有 Foo 类型的参数 foo。这是通过使用 Select 来推断的,它采用 Func 并且由于 TIn 已知 (Foo),因此可以推断 foo 的类型。
  5. 类型 Foo 有一个属性 Bar,其类型为 Bar。我们知道 Select 返回 IEnumerable,并且 TOut 可以从 lambda 表达式的结果推断出来,因此结果项的类型必须是 IEnumerable

Intellisense systems typically represent the code using an Abstract Syntax Tree, which allows them to resolve the return type of the function being assigned to the 'var' variable in more or less the same way as the compiler will. If you use the VS Intellisense, you may notice that it won't give you the type of var until you've finished entering a valid (resolvable) assignment expression. If the expression is still ambiguous (for instance, it can't fully infer the generic arguments for the expression), the var type will not resolve. This can be a fairly complex process, as you might need to walk fairly deep into a tree in order to resolve the type. For instance:

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

The return type is IEnumerable<Bar>, but resolving this required knowing:

  1. myList is of type that implements IEnumerable.
  2. There is an extension method OfType<T> that applies to IEnumerable.
  3. The resulting value is IEnumerable<Foo> and there is an extension method Select that applies to this.
  4. The lambda expression foo => foo.Bar has the parameter foo of type Foo. This is inferred by the usage of Select, which takes a Func<TIn,TOut> and since TIn is known (Foo), the type of foo can be inferred.
  5. The type Foo has a property Bar, which is of type Bar. We know that Select returns IEnumerable<TOut> and TOut can be inferred from the result of the lambda expression, so the resulting type of items must be IEnumerable<Bar>.
顾铮苏瑾 2024-09-08 12:01:22

由于您的目标是 Emacs,因此最好从 CEDET 套件开始。 Eric Lippert 的所有细节都已经在 C++ 的 CEDET/Semantic 工具的代码分析器中涵盖了。还有一个 C# 解析器(可能需要一点 TLC),因此唯一缺少的部分与调整 C# 的必要部分有关。

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

大牛的回答建议使用MonoDevelop来做解析分析。这可能是替代现有 C# 解析器的机制,或者它可以用于增强现有解析器。

Since you are targeting Emacs, it may be best to start with the CEDET suite. All the details that Eric Lippert are covered in the code analyzer in the CEDET/Semantic tool for C++ already. There is also a C# parser (which probably needs a little TLC) so the only parts missing are related to tuning the necessary parts for C#.

The basic behaviors are defined in core algorithms that depend on overloadable functions that are defined on a per language basis. The success of the completion engine is dependent on how much tuning has been done. With c++ as a guide, getting support similar to C++ shouldn't be too bad.

Daniel's answer suggests using MonoDevelop to do parsing and analysis. This could be an alternative mechanism instead of the existing C# parser, or it could be used to augment the existing parser.

软糯酥胸 2024-09-08 12:01:22

这是一个很难做好的问题。基本上,您需要通过大部分词法分析/解析/类型检查来对语言规范/编译器进行建模,并构建源代码的内部模型,然后您可以查询该模型。 Eric 为 C# 详细描述了它。您始终可以下载 F# 编译器源代码(F# CTP 的一部分)并查看 service.fsi 以查看 F# 编译器公开的接口,F# 语言服务使用该接口来提供智能感知、推断类型的工具提示等。如果您已经将编译器作为 API 调用,那么它会给您一种可能的“接口”的感觉。

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

总之,我认为“低预算”版本很难做好,而“真实”版本则非常非常难做好。 (这里的“困难”既衡量“努力”又衡量“技术难度”。)

It's a hard problem to do well. Basically you need to model the language spec/compiler through most of the lexing/parsing/typechecking and build an internal model of the source code which you can then query. Eric describes it in detail for C#. You can always download the F# compiler source code (part of the F# CTP) and take a look at service.fsi to see the interface exposed out of the F# compiler that the F# language service consumes for providing intellisense, tooltips for inferred types, etc. It gives a sense of a possible 'interface' if you already had the compiler available as an API to call into.

The other route is to re-use the compilers as-is as you're describing, and then use reflection or look at the generated code. This is problematic from the point of view that you need 'full programs' to get a compilation output from a compiler, whereas when editing source code in the editor, you often only have 'partial programs' that do not yet parse, don't have all methods implemented yet, etc.

In short, I think the 'low budget' version is very hard to do well, and the 'real' version is very, very hard to do well. (Where 'hard' here measures both 'effort' and 'technical difficulty'.)

独自唱情﹋歌 2024-09-08 12:01:22

NRefactory 将为您完成此操作。

NRefactory will do this for you.

芸娘子的小脾气 2024-09-08 12:01:22

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

For solution "1" you have a new facility in .NET 4 to do this quickly and easily.
So if you can have your program converted to .NET 4 it would be your best choice.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文