在MSIL中,有哪些C#或VB.NET无法实现的功能?

172

所有使用.NET语言编写的代码都会编译为MSIL,但是是否有特定任务/操作只能直接使用MSIL完成呢?

在MSIL中,我们也可以更轻松地完成一些任务,而不像C#、VB.NET、F#、j#或其他任何.NET语言那样麻烦。

到目前为止,我们有以下这些:

  1. 尾递归
  2. 泛型协变/逆变(在C#4和VB 10中允许)
  3. 仅返回类型不同的重载
  4. 覆盖访问修饰符
  5. 创建一个无法从System.Object继承的类
  6. 筛选异常(在VB和C#6中允许)
  7. 调用当前静态类类型的虚拟方法。
  8. 获取值类型的装箱版本的句柄。
  9. 进行try / fault操作。
  10. 使用被禁止的名称。
  11. 为值类型定义自己的无参构造函数
  12. 定义具有raise元素的事件。
  13. CLR允许的某些转换在C#中不允许。
  14. 将非main()方法作为.entrypoint
  15. 直接使用本机int和本机unsigned int类型。
  16. 使用瞬态指针进行操作
  17. 在MethodBodyItem中使用emitbyte指令。
  18. 抛出和捕获非System.Exception类型。
  19. 继承枚举(未经验证)
  20. 您可以将字节数组视为(4倍较小的)整数数组。
  21. 一个字段/方法/属性/事件都可以拥有相同的名称(未经验证)。
  22. 从catch块返回到try块内部。
  23. 您可以使用famandassem访问修饰符(protected internal是famorassem,但在C# 7.2和VB 15.5中现在已允许)。
  • 可以直接访问<Module>类来定义全局函数或模块初始化器。
  • 创建并使用非零起始的1-based数组。
  • 创建开放实例和封闭静态委托,以及getter/setter的委托。
  • 不使用临时变量交换两个值。
  • 能够显式实现任何名称的接口,并在一个函数中实现两个接口(VB可实现)。
  • 声明vtfixup(C中等同于extern
  • 指定任意的modoptmodreq

  • 6
    F# 支持尾递归,请参阅:http://en.wikibooks.org/wiki/F_Sharp_Programming/Recursion - Bas Bossink
    5
    继承枚举?有时那真的太好了。 - Jimmy Hoffa
    1
    在.NET中,Main方法的M是大写的。 - Concrete Gannet
    7
    “将问题标记为‘无建设性’是荒谬的。这是一个实证问题。” - Jim Balter
    20个回答

    35

    由于 MSIL,允许在仅返回类型不同的情况下进行重载

    call void [mscorlib]System.Console::Write(string)
    
    或者
    callvirt int32 ...
    

    6
    你是怎么知道这些事情的? :) - Gerrie Schenck
    http://msmvps.com/blogs/luisabreu/archive/2007/09/06/c-overloading-methods-by-return-type-is-not-permitted.aspx - Anton Gogolev
    9
    太棒了!谁不想编写返回值重载的功能呢? - Jimmy Hoffa
    13
    如果两种方法除了返回类型之外完全相同,它们能否从C#或vb.net中调用? - supercat

    30

    大多数 .Net 语言,包括 C# 和 VB,不使用 MSIL 代码的尾递归特性。

    尾递归是函数式语言中常见的一种优化。当方法 A 通过返回方法 B 的值来结束时,就会发生尾递归,这样一旦调用方法 B,方法 A 的栈就可以被释放。

    MSIL 代码明确支持尾递归,对于某些算法来说,这可能是一个重要的优化。但由于 C# 和 VB 不生成执行此操作所需的指令,因此必须手动完成(或使用 F# 或其他语言)。

    下面是一个手动在C#中实现尾递归的示例:

    private static int RecursiveMethod(int myParameter)
    {
        // Body of recursive method
        if (BaseCase(details))
            return result;
        // ...
    
        return RecursiveMethod(modifiedParameter);
    }
    
    // Is transformed into:
    
    private static int RecursiveMethod(int myParameter)
    {
        while (true)
        {
            // Body of recursive method
            if (BaseCase(details))
                return result;
            // ...
    
            myParameter = modifiedParameter;
        }
    }
    

    通常通过将硬件栈上的本地数据移动到堆分配的堆栈数据结构中来消除递归。如上所示,在尾调用递归消除中,堆栈被完全消除,这是一种非常好的优化。此外,返回值不必沿着长的调用链向上走,而是直接返回。

    无论如何,CIL将此功能作为语言的一部分提供,但在C#或VB中必须手动实现。(Jitter也可以自行进行此优化,但这是另一个问题。)


    1
    F# 不使用 MSIL 的尾递归,因为它只在完全受信任的 (CAS) 情况下工作,由于它不留下堆栈来检查权限断言(等等)。 - Richard
    11
    理查德,我不确定你的意思。F#确实会在很多地方发出尾调用前缀。请检查下面这个IL代码:"let print x = print_any x"。 - MichaelGG
    1
    我相信JIT在某些情况下会使用尾递归,而在某些情况下会忽略对其的显式请求。这取决于处理器架构,如果我没记错的话。 - Jon Skeet
    3
    在理论上,处理器架构是无关紧要的,但在实践中却很重要,因为不同架构的 JIT 在 .NET 中有不同的尾递归规则。换句话说,你可能会很容易地编写一个在 x86 上崩溃但在 x64 上不会崩溃的程序。尾递归虽然在两种情况下都可以实现,但并不意味着它被实现了。请注意,这个问题特指 .NET。 - Jon Skeet
    2
    C#在特定情况下实际上会在x64下进行尾调用:http://community.bartdesmet.net/blogs/bart/archive/2010/07/07/the-case-of-the-failed-demo-stackoverflowexception-on-x64.aspx。 - Pieter van Ginkel
    显示剩余5条评论

    22

    在MSIL中,你可以有一个不能从System.Object继承的类。

    示例代码:使用ilasm.exe编译更新:必须使用“/NOAUTOINHERIT”以防止汇编器自动继承。

    // Metadata version: v2.0.50215
    .assembly extern mscorlib
    {
      .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         // .z\V.4..
      .ver 2:0:0:0
    }
    .assembly sample
    {
      .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) 
      .hash algorithm 0x00008004
      .ver 0:0:0:0
    }
    .module sample.exe
    // MVID: {A224F460-A049-4A03-9E71-80A36DBBBCD3}
    .imagebase 0x00400000
    .file alignment 0x00000200
    .stackreserve 0x00100000
    .subsystem 0x0003       // WINDOWS_CUI
    .corflags 0x00000001    //  ILONLY
    // Image base: 0x02F20000
    
    
    // =============== CLASS MEMBERS DECLARATION ===================
    
    .class public auto ansi beforefieldinit Hello
    {
      .method public hidebysig static void  Main(string[] args) cil managed
      {
        .entrypoint
        // Code size       13 (0xd)
        .maxstack  8
        IL_0000:  nop
        IL_0001:  ldstr      "Hello World!"
        IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
        IL_000b:  nop
        IL_000c:  ret
      } // end of method Hello::Main
    } // end of class Hello
    

    3
    @Jon Skeet - 尊敬的先生,恳请您帮助我理解NOAUTOINHERIT的含义。MSDN指明:“在未指定基类时,禁用从Object的默认继承。.NET Framework 2.0版本中新增。” - Ramesh
    3
    @Michael - 这个问题涉及到MSIL而不是通用中间语言。我同意在CIL中可能不可能实现,但在MSIL中仍然可以工作。 - Ramesh
    5
    @Ramesh:哎呀,你说得对。我想在那一点上它违反了标准规范,不应该使用。Reflector甚至没有加载程序集。然而,可以使用ilasm来实现。我想知道这是为什么。 Translated: @Ramesh: Oops, 你完全正确。我会说在那个点上它破坏了标准规范,不应该使用。Reflector甚至没有加载这个程序集。但是,使用ilasm可以做到。我不知道为什么会有这个。 - Jon Skeet
    4
    啊,我看到在我的评论后添加了“/noautoinherit”这个参数。至少现在我不那么难过没有在之前意识到它了... - Jon Skeet
    1
    我会补充一下,至少在Windows上的.NET 4.5.2编译通过但无法执行(TypeLoadException)。PEVerify返回:*[MD]:错误:非接口且不是Object类扩展Nil标记的TypeDef。* - xanatos
    显示剩余7条评论

    21

    可以组合使用 protectedinternal 访问修饰符。在C#中,如果你使用protected internal修饰成员,该成员既可以从程序集中访问,也可以从派生类中访问。通过MSIL,你可以获取一个只能从程序集内部的派生类访问的成员。(我认为这非常有用!)


    5
    目前private protected是C# 7.1实现的候选功能(https://github.com/dotnet/csharplang/issues/37),它是一种访问修饰符。 - Happypig375
    5
    它已经作为C# 7.2的一部分发布:https://blogs.msdn.microsoft.com/dotnet/2017/11/15/welcome-to-c-7-2-and-span/ - Joe Sewell

    19

    哦,当时我没有注意到这个问题。(如果你添加jon-skeet标签,就更有可能了,但我不经常检查。)

    看起来你已经有了相当好的答案。 此外:

    • 在C#中,无法获取值类型的盒装版本的句柄。 但在C ++ / CLI中可以。
    • 在C#中无法使用try / fault(“fault”类似于“捕获所有内容并在块末重新抛出”或“仅在失败时最终执行”)
    • C#禁止使用许多名称,但IL合法
    • IL允许您为值类型定义自己的无参构造函数
    • 在C#中无法使用“raise”元素定义事件。(在VB中,您必须使用自定义事件,但“默认”事件不包括其中一个。)
    • CLR允许一些转换,但C#不允许。 如果在C#中通过 object 进行转换,则有时会起作用。 参见uint [] / int [] SO问题以获取示例。

    如果我想到其他内容,我会添加到此处...


    3
    啊,Jon Skeet标签,我知道我漏了什么! - Binoj Antony
    在C#中,如果要使用非法的标识符名称,可以在其前面加上@。 - George Polevoy
    4
    @George:这适用于关键字,但不适用于所有有效的IL名称。尝试在C#中将<>a指定为名称... - Jon Skeet

    18

    你能提供更多关于这个的信息链接吗? - Binoj Antony

    15

    在 IL 中,你可以抛出和捕获任何类型的异常,而不仅仅是从 System.Exception 派生的类型。


    6
    你也可以用C#中的try/catch来实现,只需在catch语句中不使用括号即可捕获非异常类似的异常。然而,抛出异常确实只有在继承自Exception时才可能实现。 - Abel
    @Abel 如果你不能引用它,那么你几乎无法说你正在捕获某些东西。 - Jim Balter
    2
    如果您没有捕获异常,应用程序就会崩溃。如果您捕获了异常,应用程序就不会崩溃。因此,引用异常对象与捕获异常是不同的。 - Daniel Earwicker
    哈哈!区分应用程序是终止还是继续运行算是小学问?现在我想我可能已经听到了所有的事情。 - Daniel Earwicker
    1
    有趣的是,CLR不再允许您这样做。默认情况下,它会将非异常对象包装在RuntimeWrappedException中。 - Jwosty

    11

    在IL中,callcallvirt有区别,用前者可以强制调用当前静态类类型的虚方法而不是动态类类型中的虚函数。

    C#没有这样的功能:

    abstract class Foo {
        public void F() {
            Console.WriteLine(ToString()); // Always a virtual call!
        }
    
        public override string ToString() { System.Diagnostics.Debug.Assert(false); }
    };
    
    sealed class Bar : Foo {
        public override string ToString() { return "I'm called!"; }
    }
    

    与IL类似,VB可以使用MyClass.Method()语法发出非虚拟调用。在上面的示例中,这将是MyClass.ToString()


    10
    在 try/catch 中,您可以从 catch 块重新进入 try 块。因此,您可以这样做:
    .try {
        // ...
    
      MidTry:
        // ...
    
        leave.s RestOfMethod
    }
    catch [mscorlib]System.Exception {
        leave.s MidTry  // branching back into try block!
    }
    
    RestOfMethod:
        // ...
    

    据我所知,在C#或VB中无法做到这一点。

    3
    我明白为什么这被省略了 - 它有着明显的“GOTO”的味道。 - Basic
    3
    这句话的意思是:这听起来很像VB.NET中的"On Error Resume Next"。 - Thomas Weller
    1
    这实际上可以在VB.NET中完成。GoTo语句可以从Catch跳转到其Try。在线运行测试代码here - mbomb007

    9

    17
    请移除= True,它让我眼睛难受! - Konrad Rudolph
    为什么?这是VB而不是C#,所以没有=/==的问题存在。;-) - peSHIr
    好的,C#可以使用"throw;"语句来实现相同的结果。 - Frank Schwieterman
    8
    “paSHIr”,我认为他在谈论它的冗余性。 - LegendLength
    5
    捕获并重新抛出异常与推迟捕获异常是有区别的。筛选器在任何嵌套的“finally”语句之前运行,因此导致异常的情况在筛选器运行时仍然存在。如果预计会抛出大量SocketException,并希望相对安静地处理它们,但其中一些会发出警告信号,则能够在出现问题的异常时检查状态将非常有用。 - supercat

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接