所有用.NET语言编写的代码都编译为MSIL,但是否有只能直接使用MSIL的特定任务/操作?
让我们在MSIL中做的事情比C、VB.NET、F、J或任何其他.NET语言都容易。
到目前为止,我们有:
尾部递归
一般协变/逆变
仅返回类型不同的重载
重写访问修饰符
具有不能从System.Object继承的类
筛选的异常(可以在vb.net中完成)
调用当前静态类类型的虚拟方法。
获取值类型的装箱版本的句柄。
做一个尝试/错误。
禁止使用的名称。
为值类型定义自己的无参数构造函数。
使用raise元素定义事件。
一些转换是由clr允许的,但不是由c_允许的。
制作一个非main()方法作为.entrypoint方法。
直接使用原生int和原生unsigned int类型。
玩瞬时指针
methodBodyItem中的emitByte指令
引发和捕获非System.Exception类型
继承枚举(未验证)
您可以将一个字节数组视为一个整数数组(小4倍)。
您可以让字段/方法/属性/事件都具有相同的名称(未验证)。
您可以从自己的catch块分支回try块。
您可以访问famandassem访问说明符(protected internal是famorasem)
直接访问类以定义全局函数或模块初始值设定项。
- 好问题!
- f是否支持尾部递归,请参见:en.wikibooks.org/wiki/f_sharp_programming/recursion
- 继承枚举?有时候会很好……
- 主方法在.NET中有一个大写M
- "封闭的非建设性"主张是荒谬的。这是一个经验问题。
msil允许仅在返回类型上不同的重载,因为
1
| call void [mscorlib]System.Console::Write(string) |
或
- 你怎么知道这种东西?:)
- msmvps.com/blogs/luisabreu/archive/2007/09/06/…
- 这太棒了。谁不想让返回过载?
- 如果两个方法除返回类型外都相同,那么可以从C或vb.net调用它们吗?
大多数.NET语言(包括C和VB)都不使用MSIL代码的尾部递归功能。
尾递归是函数语言中常见的一种优化。当方法A以返回方法B的值结束时,会发生这种情况,这样一旦调用方法B,就可以释放方法A的堆栈。
MSIL代码显式支持尾部递归,对于某些算法,这可能是一个重要的优化。但由于C和VB不生成执行此操作的指令,因此必须手动执行(或使用F或其他语言)。
下面是一个示例,说明如何在C中手动实现尾部递归:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| 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,它必须手动实现。(抖动也可以自行进行优化,但这是一个完全不同的问题。)
- f不使用msil的尾部递归,因为它只在完全受信任(cas)情况下工作,因为它不离开堆栈来检查权限关联(等等)。
- 理查德,我不知道你的意思。F当然会发射尾巴。呼叫前缀,几乎到处都是。为此检查IL:"让print x=print_any x"。
- 我相信在某些情况下,JIT无论如何都会使用尾部递归——在某些情况下,它会忽略对它的显式请求。它取决于处理器体系结构IIRC。
- @乔恩SKET,处理器体系结构与实现尾部递归无关(即,像埃菲尔这样的处理器不可知的语言被称为以这种方式优化代码,就像Saxon的XSLT2-SA和Java字节码一样)。但可以,可以优化JIT来实现这一点,而不管处理器是什么,它确实是通过.NET的JIT实现的。
- @Abel:虽然理论上处理器体系结构是不相关的,但实际上它并不是不相关的,因为不同体系结构的不同JIT在.NET中有不同的尾部递归规则。换句话说,您可以很容易地拥有一个在x86上爆炸的程序,而不是在x64上爆炸的程序。仅仅因为尾部递归可以在这两种情况下实现并不意味着它是。注意这个问题是关于.NET的。
- @乔恩·斯基特,是的,这样才有意义。很抱歉早点误会你。你从不睡觉,是吗?;)
- 实际上,在特定情况下,C在X64下执行尾调用:community.barddesmet.net/blogs/bart/archive/2010/07/07/…。
- @Pieter-我不相信C编译器生成的MSIL代码在x86和X64之间会有任何不同。我认为您链接的文章引用了如何对MSIL代码进行JIT编译。因为C和vb仍然没有生成.tail指令修饰,所以这仍然是一个在msil中可用的示例,在C或vb中不可用。在C中,目前甚至无法向MSIL生成器或JIT编译器暗示应该进行尾部递归优化。这是一件好事,X64抖动能够自己解决,所以我们不必与微星垃圾!
- 天哪,认为你是对的。几个月前读了这篇文章,显然记错了。好吧,这仍然存在:)。
- @Jeffreylwhitledge:我更愿意让一种语言包含一个tail return语句,它要么生成"tail-return"指令,要么在不可能的情况下拒绝编译,而不是让编译器尝试自动生成tail-return。如果一段代码需要使用tail-return才能正确运行,那么不使用tail-return就不可能编译该代码。如果不需要返回尾部,通常不会有任何效果。
在MSIL中,可以有一个不能从System.Object继承的类。
示例代码:使用ilasm.exe更新编译它:必须使用"/noautoinherit"以防止汇编程序自动继承。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| // 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 |
- 有没有链接来验证这个?
- #比诺克·安东尼:我添加了示例代码,可以使用ilasm.exe编译并执行。
- 这并不是从System.Object显式继承的,但我相信它是隐式继承的。参见ECMA 335第8.9.9节-它必须至少间接继承自System.Object。
- 如果编译然后反汇编,您将看到:.class public auto ansi beforefieldinit hello扩展[mscorlib]system.object
- @www.trausch.us-您需要使用ilasm code.il/noautoinherit进行编译
- @Ramesh是不可移植的;该选项不存在于(例如)Ilasm的Mono2.0版本中,该版本严格遵循由jon skeet引用的ECMA。
- @乔恩·斯基特-恕我直言,你能帮我理解"不可继承"的意思吗?msdn指定"在未指定基类时禁用对象的默认继承。在.NET Framework 2.0版中是新的。"
- @迈克尔-问题是关于MSIL,而不是通用的中间语言。我同意这在CIL中可能是不可能的,但它仍然与MSIL一起工作
- @拉梅什:噢,你说得对。我会说,在那一点上,它打破了标准规格,不应该使用。Reflector甚至不能加载。但是,它可以用ILASM来完成。我想知道为什么它在地球上。
- (啊,我看到/noautoinherit位是在我的评论之后添加的。至少我对之前没有意识到这一点感觉好多了…)
- @乔恩-是的,我看到后补充说,迈克尔评论。我会修改答案以反映我已经更新了它。
- @拉梅什:不好意思,我不是想批评你,因为你之前没有把更新做得更明显。我只是松了一口气,因为我没有错过任何显而易见的东西,也没有第一次读不懂你的问题。但我仍然想知道问题的关键是…
- @ramesh/@jon skeet/@all:fwiw ECMA-335中定义和禁止这一点的行是:"每个类(除System.Object和特殊类外)都应扩展一个类,而只有一个类是其他类,因此,一个类的Extends应为非空[错误]"(此处的"错误"一词表示:如果不遵守此规则,则必须引发错误)。我想这很好地概括了为什么这行不通。
- 我将至少在.NET 4.5.2上添加它,在Windows上它编译但不执行(TypeLoadException)。peverify返回:[md]:错误:不是接口且不是对象类的typedef扩展了nil标记。
可以结合protected和internal访问修饰符。在C中,如果编写protected internal,则可以从程序集和派生类访问成员。通过msil,您可以获取仅从程序集中的派生类访问的成员。(我认为这很有用!)
- 现在它是C_7.1(github.com/dotnet/csharplang/issues/37)中要实现的一个候选对象,访问修饰符是private protected。
- 它作为C 7.2:blogs.msdn.microsoft.com/dotnet/2017/11/15/&hellip的一部分发布;
哦,我当时没发现这个。(如果你加上jon-skeet标签,可能更容易,但我不会经常检查。)
看来你已经有了很好的答案了。此外:
- 在C中,无法获得值类型的装箱版本的句柄。你可以在C++/CLI中使用
- 你不能在C中尝试/犯错("犯错"类似于"抓住每件事,在块的末尾重新思考"或"最后但只在失败时")。
- 有很多名字被C禁止,但法律上是不允许的。
- IL允许您为值类型定义自己的无参数构造函数。
- 不能用c_中的"raise"元素定义事件。(在VB中,您必须为自定义事件设置,但"默认"事件不包括一个。)
- 有些转换是由clr允许的,但C不允许。如果你通过c中的object,这些有时会起作用。例如,请参见uint[]/int[]so问题。
如果我想到其他的事情,我会加上这个…
- 啊,乔恩·斯基特的标签,我知道我错过了什么!
- 若要使用非法的标识符名称,您可以在其前面加上C中的@。#
- @乔治:这适用于关键字,但不是所有有效的IL名称。尝试在c中指定<>a作为名称…
clr已经支持通用的co/contravariance,但是c直到4.0才获得这个特性。
在IL中,您可以抛出和捕获任何类型,而不仅仅是从System.Exception派生的类型。
- 您也可以在C语言中这样做,使用catch语句中没有括号的try/catch,您也可以捕获非异常的异常。然而,只有当你从以东王那里继承时,投掷才是可能的。
- @亚伯,你很难说你抓住了什么东西,如果你不能参照它。
- @Jimbalter如果你没有抓住它,应用程序就会崩溃。如果捕捉到它,应用程序不会崩溃。因此,引用异常对象不同于捕获它。
- 大声笑!应用程序终止或继续之间的区别是迂腐?现在我想我可能听到了一切。
- 有趣的是,CLR不再允许您这样做。默认情况下,它将在RuntimeWrappedException中包装非异常对象。
IL在虚拟方法调用中区分了call和callvirt。通过使用前者,可以强制调用当前静态类类型的虚方法,而不是动态类类型中的虚函数。
C没有办法做到这一点:
1 2 3 4 5 6 7 8 9 10 11
| 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!"; }
} |
vb和il一样,可以使用MyClass.Method()语法发出非虚调用。在上面,这将是MyClass.ToString()。
在try/catch中,可以从自己的catch块重新输入try块。所以,你可以这样做:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| .try {
// ...
MidTry:
// ...
leave.s RestOfMethod
}
catch [mscorlib]System.Exception {
leave.s MidTry // branching back into try block!
}
RestOfMethod:
// ... |
Afaik你不能在C或VB中这样做
- 我明白为什么省略了这一点——它有一种明显的GOTO的气味。
- 听起来很像在vb.net下一个错误恢复
- 这实际上可以在vb.net中完成。goto语句可以从Catch分支到其Try。在此处联机运行测试代码。
使用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(注意catch子句中的When ShouldCatch(ex) = True)。
1 2 3 4 5
| Try
Foo()
Catch ex As CustomBaseException When ShouldCatch(ex)
Console.WriteLine("Caught exception!")
End Try |
- 请把= True移开,它让我的眼睛流血了!
- 为什么?这是vb而不是c,因此不存在=/==问题。;-)
- 好的,C可以做"throw;",所以可以得到相同的结果。
- 帕希尔,我相信他是在说它的简化。
- @弗兰克·施维特曼:捕获和重新引发一个异常与拖延捕获它之间有区别。过滤器在任何嵌套的"finally"语句之前运行,因此当过滤器运行时,导致异常的情况仍然存在。如果一个人希望抛出大量的socketException,那么他会希望相对安静地捕捉到,但其中一些会表示有问题,能够在抛出有问题的socketException时检查状态可能非常有用。
据我所知,无法在C中直接生成模块初始值设定项(整个模块的静态构造函数):
http://blogs.msdn.com/junfeng/archive/2005/11/19/494914.aspx
- + 1点!正如我在这里注意到的,C的一个伟大的小姐。
Native types您可以直接使用本机int和本机无符号int类型(在c中,您只能使用不同的intptr。
Transient Pointers您可以使用临时指针,这些指针是指向托管类型的指针,但保证不会在内存中移动,因为它们不在托管堆中。不完全确定如何在不干扰非托管代码的情况下有效地使用它,但是它不会直接通过stackalloc等方式暴露给其他语言。
如果你愿意的话,你可以在课堂上乱搞(你可以通过反思来做到这一点,而不需要IL)
.emitbyte
15.4.1.1 The .emitbyte directive MethodBodyItem ::= … | .emitbyte
Int32 This directive causes an
unsigned 8-bit value to be emitted
directly into the CIL stream of the
method, at the point at which the
directive appears. [Note: The
.emitbyte directive is used for
generating tests. It is not required
in generating regular programs. end
note]
.entrypoint您在这方面有点灵活性,例如,您可以将其应用于未调用main的方法。
读一读说明书,我相信你会发现更多的。
- +1,这里有一些隐藏的宝石。注意,EDOCX1[1]是指接受全局方法(如vb)的语言的特殊类,但实际上,c不能直接访问它。
- 瞬时指针似乎是一种非常有用的类型;许多对可变结构的反对源于它们的遗漏。例如,如果dictofpoints(key).x=5;"返回到结构的临时指针,而不是按值复制结构,则它是可行的。
- @supercat不能与临时指针一起工作,相关数据可能在堆中。你想要的是参考返回埃里克谈论这里:blogs.msdn.com/b/ericlippet/archive/2011/06/23/&hellip;
- @Shuggycouk:有趣。我真的希望能说服埃里克提供一种方法,使"dictofpoints(key.x=5)"能够发挥作用。现在,如果你想硬编码dictofpoints来专门处理类型point(或其他特定类型),你几乎可以让它工作,但这是一种痛苦。顺便说一句,我想看到的是一种方法,通过这种方法可以编写一个开放的通用函数,例如dostuf<…>(someparams,actionboref,…),它可以根据需要进行扩展。我想在MSIL中有某种方法可以做到这一点;在编译器的帮助下…
- @Shuggycouk:…它将提供另一种通过引用来拥有属性的方式,在一些属性完成了外部人员对引用所做的一切之后,可以运行额外的奖金。
- @超级卫星可变结构是相当邪恶的,我真的不想推进一个语言功能,鼓励它…
- @我强烈反对。当前语言中的值结构实现是邪恶的,因为它们有时会在不通知任何人的情况下用事物的副本来代替原始值(例如,当通过引用传递只读值类型时,编译器将静默地复制并传递该值,而不是简单地完全禁止该操作)。我假设通过引用传递值类型通常比通过值传递引用类型更好的编程范式;后者imho更邪恶。
- @Shuggycouk:引用类型的问题在于,它们可以去哪里没有限制。相比之下,对值类型的引用具有明确的范围。持有值类型并通过引用访问方法公开它的类可以知道它不会在该方法之外发生更改,并且(如果按我希望看到的方式处理属性引用)将保证在更改它们的实体完成时得到通知。
- @如果supercat通过引用传递值类型在堆栈生存期内,则在其变得非常复杂之外,这是很好的,因为您可能会以复杂的方式显著地改变生存期。我承认你可以用它做一些很酷的事情,我只是不确定它应该用C语言。一个关键的问题是,它使类型系统非常复杂(你不能将它们框在对象中,不能在通用类型中使用它们,你可以加载更多的选项来检查反射场景等),我只是认为它们不能为语言和框架的其他部分带来额外的成本。
- @Supercat对于有一个令人信服的用例的人来说,我的推理并不适用,我承认。
- @Shuggycouk让我们在聊天中继续讨论
您可以使用hack方法覆盖co/contra方差,这是c_不允许的(这与一般方差不同!).我有更多关于在这里实现这一点的信息,以及第1部分和第2部分
还有一些:
在委托中可以有额外的实例方法。
委托可以实现接口。
在委托和接口中可以有静态成员。
我想我一直希望(完全错误的原因)的是遗赠。在SMIL中似乎不难做到(因为枚举只是类),但C语法并不希望您做到这一点。
20)您可以将一个字节数组视为一个整数数组(小4倍)。
我最近用它来做一个快速的XOR实现,因为clr xor函数在ints上操作,我需要在字节流上执行XOR。
得到的代码比用C(对每个字节执行XOR)执行的等效代码快约10倍。
= =
我没有足够的stackoverflow street credz来编辑问题并将其作为20添加到列表中,如果其他人可能会膨胀的话;-)
- 你可以用不安全的指针来完成这项工作,而不必使用IL。我可以想象它也会一样快,甚至更快,因为它不会进行边界检查。
模糊处理程序使用的东西-您可以让一个字段/方法/属性/事件都具有相同的名称。
- 有没有提到这个?
- 我在我的站点上发布了一个示例:jasonhaley.com/files/nametesa.zip在这个压缩包中有一个IL和一个exe,它包含一个类,具有以下所有相同的"a":-类名是一个名为a的-event,名为a的-method,名为a的-2个字段,我找不到一个好的引用指向您,尽管我可能在ecma 335 spe中读到它。C或谢尔盖·利丁的书。
枚举继承实际上不可能:
可以从枚举类继承。但结果并不像枚举那样具体。它的行为甚至不像一个值类型,而是像一个普通类。范围是:isEnum:真,isValueType:真,isClass:假
但这并不特别有用(除非你想混淆一个人或运行时本身)。
您还可以从IL中的system.multicast委托派生类,但在C中不能这样做:
// The following class definition is illegal:
public class YourCustomDelegate :
MulticastDelegate
{
}
您还可以在IL中定义模块级(aka global)方法,而C_则相反,只允许您定义方法,只要它们连接到至少一个类型。