在MSIL中可以做什么,而在C#或VB.NET中不能做什么?[关闭]


165

用.NET语言编写的所有代码都可以编译为MSIL,但是是否存在只能直接使用MSIL才能执行的特定任务/操作?

让我们在MSIL中也比C#,VB.NET,F#,j#或任何其他.NET语言更轻松地完成工作。

到目前为止,我们有:

  1. 尾递归
  2. 通用协/反方差
  3. 仅在返回类型上有所不同的过载
  4. 覆盖访问修饰符
  5. 有一个不能从System.Object继承的类
  6. 过滤的异常(可以在vb.net中完成)
  7. 调用当前静态类类型的虚拟方法。
  8. 获取值类型的盒装版本的句柄。
  9. 尝试/故障。
  10. 禁止名称的用法。
  11. 为值类型定义自己的无参数构造函数
  12. raise元素定义事件。
  13. CLR允许某些转换,但C#不允许。
  14. 将非main()方法设为.entrypoint
  15. 直接使用本机int和本机unsigned int类型。
  16. 玩瞬态指针
  17. 在MethodBodyItem中发出字节指令
  18. 抛出并捕获非System.Exception类型
  19. 继承枚举(未验证)
  20. 您可以将字节数组视为int数组(小4倍)。
  21. 您可以将字段/方法/属性/事件都具有相同的名称(未验证)。
  22. 您可以从其自己的catch块分支回try块。
  23. 您可以访问famandassem访问说明符(protected internal是fam assem)
  24. 直接访问<Module>用于定义全局函数的类或模块初始化器。

17
很好的问题!
Tamas Czinege 09年

5
F#确实支持尾递归,请参见:en.wikibooks.org/wiki/F_Sharp_Programming/Recursion
Bas Bossink 09年

4
继承枚举?有时候那真好..
吉米·霍法

1
.NET中的主要方法使用大写
字母

4
“封闭为非建设性”的主张是荒谬的。这是一个经验问题。
吉姆·巴尔特

Answers:



29

包括C#和VB在内的大多数.Net语言都不使用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,必须手动实现。(抖动也可以自行进行优化,但这完全是另一个问题。)


1
F#不使用MSIL的尾部递归,因为它只在完全受信任(CAS)的情况下工作,因为它不会留下堆栈来检查权限侵犯(等等)。
理查德

10
理查德,我不确定你的意思。F#当然会发出尾巴。呼叫前缀,几乎遍布整个地方。检查IL:“ let print x = print_any x”。
MichaelGG

1
我相信JIT在某些情况下仍将使用尾递归-在某些情况下将忽略对它的显式请求。这取决于处理器架构IIRC。
乔恩·斯基特

3
@Abel:虽然处理器体系结构在理论上无关紧要,但在实践中并不是无关紧要的,因为针对不同体系结构的不同JIT在.NET中具有不同的尾部递归规则。换句话说,您可以很容易地拥有一个在x86而不是x64上崩溃的程序。仅仅因为两种情况下可以实现尾递归并不意味着是这样。请注意,此问题专门与.NET有关。
乔恩·斯基特

2
在特定情况下,C#实际上在x64下进行了尾部调用:community.bartdesmet.net/blogs/bart/archive/2010/07/07/…
Pieter van Ginkel

21

在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

2
@Jon Skeet-致以诚挚的敬意,请您帮助我了解NOAUTOINHERIT的含义。MSDN指定“当未指定基类时,禁用从对象的默认继承。.NETFramework 2.0版中的新增功能。”
拉梅什

2
@Michael-问题是关于MSIL,而不是通用中间语言。我同意这在CIL中可能无法实现,但它仍适用于MSIL
Ramesh,2009年

4
@Ramesh:糟糕,您是完全正确的。我要说的是,这违反了标准规范,因此不应使用。Reflector甚至不会加载。但是,可以通过ilasm完成。我不知道为什么在地球上。
乔恩·斯基特

4
(啊,我在评论后看到添加了/ noautoinherit位。至少我对在未意识到之前有所了解...)
Jon Skeet

1
我将至少在Windows上的.NET 4.5.2上添加它可以编译但不执行(TypeLoadException)。PEVerify返回:[MD]:错误:不是接口且不是Object类的TypeDef扩展了Nil令牌。
xanatos

20

可以结合使用protectedinternal访问修饰符。在C#中,如果编写protected internal成员,则可以从程序集和派生类访问成员。通过MSIL你可以得到一个部件,其内装配从派生类访问。(我认为这可能非常有用!)


4
它是现在要在C#7.1(github.com/dotnet/csharplang/issues/37)中实现的候选者,访问修饰符是private protected
Happypig375

5
它已经发布了作为C#7.2的一部分:blogs.msdn.microsoft.com/dotnet/2017/11/15/...
乔·休厄尔

18

哦,当时我没发现。(如果您添加了jon-skeet标签,则可能性更大,但我不经常检查它。)

看来您已经有了不错的答案。此外:

  • 您无法获得C#中值类型的盒装版本的句柄。您可以在C ++ / CLI中
  • 您无法在C#中进行尝试/错误(“错误”就像“捕获所有内容并在块末尾重新抛出”或“最终但仅在失败时一样”)
  • C#禁止使用很多名称,但合法的IL
  • IL允许您为值类型定义自己的无参数构造函数
  • 您不能在C#中使用“ raise”元素定义事件。(在VB中,您必须进行自定义事件,但“默认”事件不包含一个。)
  • CLR允许某些转换,但C#不允许。如果您使用objectC#进行浏览,这些有时会起作用。有关示例,请参见uint [] / int [] SO问题

如果我有其他想法的话,我会加...


3
啊,jon-skeet标签,我知道我错过了一些东西!
Binoj Antony

要使用非法标识符名称,您可以在C#中使用@前缀
George Polevoy 2010年

4
@乔治:这适用于关键字,但不是所有有效的IL名称。尝试<>a在C#中指定名称...
Jon Skeet 2010年


14

在IL中,您可以抛出并捕获任何类型,而不仅仅是从派生的类型System.Exception


6
您也可以在C#中执行该操作,在catch语句中使用try/ catch不带括号,您也将捕获非类异常。但是,仅当您从继承时,才可能抛出Exception
亚伯

@Abel如果您不能引用它,您几乎不能说您正在抓东西。
Jim Balter

2
@JimBalter如果您不了解它,应用程序将崩溃。如果发现它,该应用程序不会崩溃。因此,引用异常对象不同于捕获异常对象。
Daniel Earwicker

大声笑!终止应用程序还是继续运行程序之间的区别是脚?现在我想我可能已经听到了一切。
Daniel Earwicker

有趣的是,CLR不再允许您执行此操作。默认情况下,它将在RuntimeWrappedException中包装非异常对象。
Jwosty

10

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()


9

在try / catch中,您可以从其自己的catch块中重新输入try块。因此,您可以执行以下操作:

.try {
    // ...

  MidTry:
    // ...

    leave.s RestOfMethod
}
catch [mscorlib]System.Exception {
    leave.s MidTry  // branching back into try block!
}

RestOfMethod:
    // ...

AFAIK,您不能在C#或VB中执行此操作


3
我可以看到为什么省略了它-它有明显的气味GOTO
基本的

3
听起来很像VB.NET中的“ 错误恢复下一个”
Thomas Weller

1
这实际上可以在VB.NET中完成。GoTo语句可以从跳转CatchTry在此处在线运行测试代码。
mbomb007 '17

9

使用IL和VB.NET,可以在捕获异常时添加过滤器,但是C#v3不支持此功能。

这VB.NET例子取自http://blogs.msdn.com/clrteam/archive/2009/02/05/catch-rethrow-and-filters-why-you-should-care.aspx(注意When ShouldCatch(ex) = True在Catch子句):

Try
   Foo()
Catch ex As CustomBaseException When ShouldCatch(ex)
   Console.WriteLine("Caught exception!")
End Try

17
请r取下= True,这使我的眼睛流血!
康拉德·鲁道夫

为什么?这是VB而不是C#,因此不存在= / ==问题。;-)
peSHIr

c#可以执行“ throw;”,因此可以实现相同的结果。
Frank Schwieterman 09年

8
PASHIR,我相信他是在谈论它的冗余性
LegendLength,2009年

5
@Frank Schwieterman:捕获和重新抛出异常与推迟捕获是有区别的。过滤器在任何嵌套的“ finally”语句之前运行,因此导致异常的情况在运行过滤器时仍然存在。如果期望引发大量的SocketException,而人们则想相对安静地捕获它,但是其中一些会发出故障的信号,当抛出有问题的SocketException时能够检查其状态可能非常有用。
supercat


7

Native types
您可以直接使用本机int和本机无符号int类型(在c#中,您只能在不相同的IntPtr上工作。

Transient Pointers
您可以使用瞬态指针,它们是指向托管类型的指针,但是由于它们不在托管堆中,因此保证不会在内存中移动。不能完全确定如何才能有效地使用它而不弄乱非托管代码,但是它不会仅通过诸如stackalloc之类的东西直接暴露给其他语言。

<Module>
如果您愿意,可以与该类混为一谈(可以通过反射来完成此操作,而无需IL)

.emitbyte

15.4.1.1 .emitbyte指令MethodBodyItem :: =…| .emitbyte Int32该伪指令使无符号的8位值直接在该伪指令出现的位置发射到方法的CIL流中。[注意:.emitbyte指令用于生成测试。生成常规程序时不需要。尾注]

.entrypoint
您对此有更多的灵活性,例如,可以将其应用于不称为Main的方法。

阅读规格说明,我相信您还会发现更多。


+1,这里有一些隐藏的宝石。注意,这<Module>是接受全局方法的语言的特殊类(就像VB一样),但实际上C#无法直接访问它。
亚伯2010年

瞬时指针似乎是一种非常有用的类型。对可变结构的许多反对都源于它们的遗漏。例如,“ DictOfPoints(key).X = 5;” 如果DictOfPoints(key)返回指向结构的瞬时指针,而不是按值复制结构,则将是可行的。
supercat

@supercat不能与瞬态指针一起使用,有问题的数据可能在堆上。你想要的是裁判返回埃里克谈到这里:blogs.msdn.com/b/ericlippert/archive/2011/06/23/...
ShuggyCoUk

@ShuggyCoUk:有趣。我真的希望可以说服Eric提供一种“ DictOfPoints(key).X = 5;”的方法。可以工作。现在,如果要对DictOfPoints进行硬编码以专门与Point类型(或某些其他特定类型)一起使用,则几乎可以完成这项工作,但这很痛苦。顺便说一句,我想看到的一件事是一种可以编写开放式泛型函数的方法,例如DoStuff <...>(someParams,ActionByRef <moreParams,...>,...)可以根据需要扩展。我猜想在MSIL中会有某种方法可以做到这一点。与一些编译器帮助...
supercat

@ShuggyCoUk:...它将提供另一种具有按引用属性的方式,额外的好处是,在外部人对引用所做的所有操作完成之后,某些属性可以运行。
supercat

6

您可以修改方法覆盖co / contra-variance,这是C#不允许的(这与常规方差不一样!)。我在此处以及第1部分和第2部分中获得了有关实现此方法的更多信息。



4

还有更多:

  1. 您可以在委托中拥有额外的实例方法。
  2. 代表可以实现接口。
  3. 您可以在委托和接口中具有静态成员。

3

20)您可以将字节数组视为int数组(小4倍)。

我最近使用它来执行快速的XOR实现,因为CLR xor函数在ints上运行,并且我需要对字节流进行XOR。

测得的代码比在C#中完成的代码快10倍(在每个字节上执行XOR)。

===

我没有足够的stackoverflow street credz来编辑问题,并将其添加为20号列表,如果其他人可以将其膨胀;-)


3
您可以使用不安全的指针来完成此操作,而不是深入研究IL。我以为它会一样快,甚至更快,因为它不会做边界检查。
P Daddy

3

混淆器使用的东西-您可以使字段/方法/属性/事件都具有相同的名称。


1
我在我的网站上放了一个示例:jasonhaley.com/files/NameTestA.zip 在该zip中有一个IL和一个exe,其中包含一个具有以下所有相同“ A”的类:-class名称为A -Event命名名为A的方法-名为A的属性-2名为AI的字段找不到很好的参考指向您,尽管我可能在ecma 335规范或Serge Lidin的书中都读了它。
杰森·海莉

2

枚举继承实际上是不可能的:

您可以从Enum类继承。但是结果的表现并不特别像枚举。它的行为甚至不像值类型,而是像普通的类。奇怪的是:IsEnum:True,IsValueType:True,IsClass:False

但这并不是特别有用(除非您想混淆人员或运行时本身。)


2

您还可以从IL中的System.Multicast委托派生一个类,但不能在C#中做到这一点:

//以下类定义是非法的:

公共类YourCustomDelegate:MulticastDelegate {}


1

您还可以在IL和C#中定义模块级(aka全局)方法,相反,仅允许您定义方法,只要它们附加到至少一种类型即可。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.