(翻译) 《C# to IL》第四章 关键字和操作符
-4-
关键字和操作符
位于return语句之后的代码是不会被执行的。在下面给出的第1个程序中,你将发现在C#中有一个WriteLine函数调用,但是在我们的IL代码中却看不到。这是因为编译器意识到任何return之后的语句都不会被执行,从而,也就不用将其转换到IL中了。
a.cs
class zzz
{
public static void Main()
{
return;
System.Console.WriteLine("hi");
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
br.s IL_0002
IL_0002: ret
}
}
编译器不会在编译从不执行的代码上浪费时间,而是在遇到这种情形时生成一个警告。
a.cs
class zzz
{
public static void Main()
{
}
zzz( int i)
{
System.Console.WriteLine("hi");
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ret
}
.method private hidebysig specialname rtspecialname instance void .ctor(int32 i) il managed
{
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ldstr "hi"
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
如果在源代码中不存在构造函数,那么就会生成一个默认的无参构造函数。如果存在构造函数,那么这个无参构造函数就会从代码中被排除。
基类的无参构造函数总是会被调用,并且会被首先调用。上面的IL代码证明了这一事实。
a.cs
namespace vijay
{
namespace mukhi
{
class zzz
{
public static void Main()
{
}
}
}
}
a.il
.assembly mukhi {}
.namespace vijay.mukhi
{
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ret
}
}
}
我们可能会在一个命名空间中编写另一个命名空间,但是编译器会将它们全都转换为IL文件的一个命名空间中。从而,C#文件中的这两个命名空间vijay和mukhi都会被合并到IL文件的一个单独的命名空间vijay.mukhi中。
a.il
.assembly mukhi {}
.namespace vijay
{
.namespace mukhi
{
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ret
}
}
}
}
在C#中,一个命名空间可以出现在另一个命名空间中,但是C#编译器更喜欢只使用一个单独的命名空间,从而IL输出只显示了一个命名空间。IL中的.namespace指令在概念上类似于C#中的namespace关键字。命名空间的观点起源于IL而不是C#这样的程序语言。
a.cs
namespace mukhi
{
class zzz
{
public static void Main()
{
}
}
}
namespace mukhi
{
class pqr
{
}
}
a.il
.assembly mukhi {}
.namespace mukhi
{
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ret
}
}
.class private auto ansi pqr extends [mscorlib]System.Object
{
}
}
在C#文件中,我们可能有2个名为mukhi的命名空间,但是它们会变成IL文件中的一个大的命名空间,而它们的内容会被合并。合并命名空间的工具是由C#编译器提供的。
设计者认为这么处理是恰当的——他们本可以将上面的程序替代地标记为一个错误。
a.cs
class zzz
{
public static void Main()
{
int i = 6;
zzz a = new zzz();
a.abc(ref i);
System.Console.WriteLine(i);
}
public void abc(ref int i)
{
i = 10;
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (int32 V_0,class zzz V_1)
ldc.i4.6
stloc.0
newobj instance void zzz::.ctor()
stloc.1
ldloc.1
ldloca.s V_0
call instance void zzz::abc(int32&)
ldloc.0
call void [mscorlib]System.Console::WriteLine(int32)
ret
}
.method public hidebysig instance void abc(int32& i) il managed
{
ldarg.1
ldc.i4.s 10
stind.i4
ret
}
}
Output
10
我们现在要解释IL是如何实现传递引用的。与C#不同,在IL中可以很方便的使用指针。IL有3种类型的指针。
当函数abc被调用时,变量i会被作为一个引用参数传递到函数中。在IL中,ldloca.s指令会被调用,它把变量的地址放到栈上。替代地,如果这个指令是ldloc,那么就会把变量的值放到栈上。
在函数调用中,我们添加符号&到类型名称的结尾来表示变量的地址。数据类型后面的&后缀表示变量的内存位置,而不是在变量中包括的值。
在函数本身中,ldarg.1用于把参数1的地址放到栈上。然后,我们把想要初始化的数值放到栈上。在上面的例子中,我们首先把变量i的地址放到栈上,随后是我们想要初始化的值,即10。
stind指令把出现在栈顶的值,也就是10,放到变量中,这个变量的地址存储为栈上的第2项。在这个例子中,因为我们传递变量i的地址到栈上,所以变量i分配到值10。
当在栈上给出一个地址时,使用stind指令。它会使用特定的值填充该内存位置。
如果使用关键字ref取代out,那么IL还是会显示相同的输出,因为不管是哪种情形,变量的地址都会被放到栈上。因此,ref和out是C#实现中的“人为”概念,而在IL中没有任何等价的表示。
IL代码无法知道原始的程序使用的是ref还是out。因此,在反汇编这个程序时,我们将无法区别ref和out,因为这些信息在从C#代码到IL代码的转换中会丢失。
a.cs
class zzz
{
public static void Main()
{
string s = "hi" + "bye";
System.Console.WriteLine(s);
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class System.String V_0)
ldstr "hibye"
stloc.0
ldloc.0
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
Output
hibye
下面关注的是2个字符串的连接。C#编译器通过将它们转换为一个字符串来实现。这取决于编译器优化常量的风格。存储在局部变量中的值随后被放置在栈上,从而在运行期,C#编译器会尽可能的优化代码。
a.cs
class zzz
{
public static void Main()
{
string s = "hi" ;
string t = s + "bye";
System.Console.WriteLine(t);
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class System.String V_0,class System.String V_1)
ldstr "hi"
stloc.0
ldloc.0
ldstr "bye"
call class System.String [mscorlib]System.String::Concat(class System.String,class System.String)
stloc.1
ldloc.1
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
Output
hibye
无论编译器何时对变量进行处理,都会在编译器间忽略它们的值。在上面的程序中会执行以下步骤:
l 变量s和t会被相应地转换为V_0和V_1。
l 为局部变量V_0分配字符串"hi"。
l 随后这个变量会被放到栈上。
l 接下来,常量字符串"bye"会被放到栈上。
l 之后,+操作符被转化为静态函数Concat,它属于String类。
l 这个方法会连接两个字符串并在栈上创建一个新的字符串。
l 这个合成的字符串会被存储在变量V_1中。
l 最后,这个合成的字符串会被打印出来。
在C#中,有两个PLUS(+)操作符。
l 一个处理字符串。这个操作符会被转换为IL中String类的Concat函数。
l 另一个则处理数字。这个操作符会被转换为IL中的add指令。
从而,String类和它的函数是在C#编译器中创建的。因此我们能够断定,C#可以理解并处理字符串运算。
a.cs
class zzz
{
public static void Main()
{
string a = "bye";
string b = "bye";
System.Console.WriteLine(a == b);
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class System.String V_0,class System.String V_1)
ldstr "bye"
stloc.0
ldstr "bye"
stloc.1
ldloc.0
ldloc.1
call bool [mscorlib]System.String::Equals(class System.String,class System.String)
call void [mscorlib]System.Console::WriteLine(bool)
ret
}
}
Output
True
就像+操作符那样,当==操作符和字符串一起使用时,编译器会将其转换为函数Equals。
从上面的例子中,我们推论出C#编译器对字符串的处理是非常轻松的。下一个版本将会引进更多这样的类,编译器将会从直观上理解它们。
a.cs
class zzz
{
public static void Main()
{
System.Console.WriteLine((char)65);
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ldc.i4.s 65
call void [mscorlib]System.Console::WriteLine(wchar)
ret
}
}
Output
A
无论我们何时转换一个变量,例如把一个数字值转换为一个字符值,在内部,程序仅调用了带有转换数据类型的函数。转换不能修改原始的变量。实际发生的是,在WriteLine被调用时带有一个wchar,而不是一个int。从而,转换不会导致任何运行期间的负载。
a.cs
class zzz
{
public static void Main()
{
char i = 'a';
System.Console.WriteLine((char)i);
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (wchar V_0)
ldc.i4.s 97
stloc.0
ldloc.0
call void [mscorlib]System.Console::WriteLine(wchar)
ret
}
}
Output
a
C#的字符数据类型是16字节大小。在转换为IL时,它会被转换为wchar。字符a会被转换为ASCII数字97。这个字符会被放在栈上并且变量V_0会被初始化为这个值。之后,程序会在屏幕上显示值a。
a.cs
class zzz
{
public static void Main()
{
System.Console.WriteLine('"u0041');
System.Console.WriteLine(0x41);
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ldc.i4.s 65
call void [mscorlib]System.Console::WriteLine(wchar)
ldc.i4.s 65
call void [mscorlib]System.Console::WriteLine(int32)
ret
ret
}
}
Output
A
65
IL不能理解字符UNICODE或数字HEXADECIMAL。它更喜欢简单明了的十进制数字。转义符\u的出现为C#程序员带来了方便,极大提高的效率。
你可能已经注意到,即使上面的程序有2套指令,但还是不会有任何错误生成。标准是——至少应该存在一个ret指令。
a.cs
class zzz
{
public static void Main()
{
int @int;
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (int32 V_0)
ret
}
}
在C#中,在栈上创建的变量被转换为IL后不再具有原先给定的名称。因此,“C#保留字可能会在IL中产生问题”——这种情况是不会发生的。
a.cs
class zzz
{
int @int;
public static void Main()
{
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.field private int32 'int'
.method public hidebysig static void vijay() il managed
{
.entrypoint
ret
}
}
在上面的程序中,局部变量@int变成了一个名为int的字段。而数据类型int改变为int32,后者是IL中的保留字。之后,编译器在一个单引号内写字段名称。在转换到IL的过程中,@符号会直接从变量的名称中消失。
a.cs
// hi this is comment
class zzz
{
public static void Main() // allowed here
{
/*
A comment over
two lines
*/
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ret
}
}
当你看到上面的代码时,你将理解为什么全世界的程序员都讨厌写注释。C#中的所有注释在生成的IL中都会被删除。单引号不会被复制到IL代码中。
编译器对注释是缺乏“尊重”的,它会把所有的注释都扔掉。程序员认为写注释是徒劳的,他们会产生极大的挫折感——这并不奇怪。
a.cs
class zzz
{
public static void Main()
{
System.Console.WriteLine("hi "nBye"tNo");
System.Console.WriteLine("""");
System.Console.WriteLine(@"hi "nBye"tNo");
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ldstr "hi "nBye"tNo"
call void [mscorlib]System.Console::WriteLine(class System.String)
ldstr """"
call void [mscorlib]System.Console::WriteLine(class System.String)
ldstr "hi ""nBye""tNo"
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
Output
hi
Bye No
"
hi "nBye"tNo
C#处理字符串的能力是从IL中继承而来的。像\n这样的转义符会被直接复制。
双斜线\\,在显示时,结果是一个单斜线\。
如果一个字符串以一个@符号作为开始,在该字符串中的特殊意思就是这个转移符会被忽略,而这个字符串会被逐字显示,正如上面的程序所显示的那样。
如果IL没有对字符串格式提供支持,那么它就会烦心于要处理大多数现代程序语言的所面临的困境。
a.cs
#define vijay
class zzz
{
public static void Main()
{
#if vijay
System.Console.WriteLine("1");
#else
System.Console.WriteLine("2");
#endif
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed {
.entrypoint
ldstr "1"
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
ret
}
}
Output
1
接下来的一系列程序与预处理指令有关,这与C#编译器是不同的。只有预处理指令能够理解它们。
在上面的.cs程序中,#define指令创建了一个名为"vijay"的词。编译器知道#if语句是TRUE,因此,它会忽略#else语句。从而,所生成的IL文件只包括具有参数'1'的WriteLine函数,而不是具有参数'2'的那个。
这就涉及到了编译期间的知识。大量不会使用到的代码,会在被转换为IL之前,被预处理直接除去。
a.cs
#define vijay
#undef vijay
#undef vijay
class zzz
{
public static void Main()
{
#if vijay
System.Console.WriteLine("1");
#endif
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ret
}
}
我们可以使用很多#undef语句,只要我们喜欢。编译器知道'vijay'这个词被事先定义了,之后,它会忽略#if语句中的代码。
在从IL到C#的再次转换中,原始的预处理指令是无法被恢复的。
a.cs
#warning We have a code red
class zzz
{
public static void Main()
{
}
}
C#中的预处理指令#warning,用于为运行编译器的程序员显示警告。
预处理指令#line和#error并不会生成任何可执行的输出。它们只是用来提供信息。
继承
a.cs
class zzz
{
public static void Main()
{
xxx a = new xxx();
a.abc();
}
}
class yyy
{
public void abc()
{
System.Console.WriteLine("yyy abc");
}
}
class xxx : yyy
{
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class xxx V_0)
newobj instance void xxx::.ctor()
stloc.0
ldloc.0
call instance void yyy::abc()
ret
}
}
.class private auto ansi yyy extends [mscorlib]System.Object
{
.method public hidebysig instance void abc() il managed
{
ldstr "yyy abc"
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
.class private auto ansi xxx extends yyy
{
}
Output
yyy abc
继承的概念在所有支持继承的程序语言中都是相同的。单词extends起源于IL和Java而不是C#。
当我们编写a.abc()时,编译器决定在abc函数中的调用要基于下面的标准:
l 如果类xxx有一个函数abc,那么在函数vijay中的调用将具有前缀xxx。
l 如果类yyy有一个函数abc,那么在函数vijay中的调用将具有前缀yyy。
之后,人工智能决定了关于哪个函数abc会被调用,它驻留于编译器中而不是生成的IL代码中。
a.cs
class zzz
{
public static void Main()
{
yyy a = new xxx();
a.abc();
}
}
class yyy
{
public virtual void abc()
{
System.Console.WriteLine("yyy abc");
}
}
class xxx : yyy
{
public new void abc()
{
System.Console.WriteLine("xxx abc");
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class yyy V_0)
newobj instance void xxx::.ctor()
stloc.0
ldloc.0
callvirt instance void yyy::abc()
ret
}
}
.class private auto ansi yyy extends [mscorlib]System.Object
{
.method public hidebysig newslot virtual instance void abc() il managed
{
ldstr "yyy abc"
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
.class private auto ansi xxx extends yyy
{
.method public hidebysig instance void abc() il managed
{
ldstr "xxx abc"
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
Output
yyy abc
在上面程序的上下文中,我们要向C#新手多做一点解释。
我们能够使基类的一个对象和派生类xxx的一个对象相等。我们调用了方法a.abc()。随之出现的问题是,函数abc的下列2个版本,哪个将会被调用?
l 出现在基类yyy中的函数abc,调用对象属于这个函数。
或
l 函数abc存在于类xxx中,它会被初始化为这个类型。
换句话说,是编译期间类型有意义,还是运行期间的类型有意义?
基类函数具有一个名为virtual的修饰符,暗示了派生类能覆写这个函数。派生类,通过添加修饰符new,通知编译器——这个函数abc与派生类的函数abc无关。它会把它们当作单独的实体。
首先,使用ldloc.0把this指针放到栈上,而不是使用call指令。这里有一个callvirt作为替代。这是因为函数abc是虚的。除此之外,没有区别。类yyy中的函数abc被声明为虚的,还被标记为newslot。这表示它是一个新的虚函数。关键字new位于C#的派生类中。
IL还使用了类似于C#的机制,来断定哪个版本的abc函数会被调用。
a.cs
class zzz
{
public static void Main()
{
yyy a = new xxx();
a.abc();
}
}
class yyy
{
public virtual void abc()
{
System.Console.WriteLine("yyy abc");
}
}
class xxx : yyy
{
public override void abc()
{
System.Console.WriteLine("xxx abc");
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class yyy V_0)
newobj instance void xxx::.ctor()
stloc.0
ldloc.0
callvirt instance void yyy::abc()
ret
}
}
.class private auto ansi yyy extends [mscorlib]System.Object
{
.method public hidebysig newslot virtual instance void abc() il managed
{
ldstr "yyy abc"
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
.class private auto ansi xxx extends yyy
{
.method public hidebysig virtual instance void abc() il managed
{
ldstr "xxx abc"
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
.method public hidebysig specialname rtspecialname instance void .ctor() il managed
{
ldarg.0
call instance void yyy::.ctor()
ret
}
}
Output
xxx abc
如果类xxx的基构造函数没有被调用,那么在输出窗体中就不会有任何显示。通常,我们不会在IL程序中包括默认的无参构造函数。
如果没有关键字new或override,默认使用的关键字就是new。在上面的类xxx的函数abc中,我们使用到了override关键字,它暗示了这个函数abc覆写了基类的函数。
IL默认调用对象所属类的虚函数,并使用编译期间的类型。在这个例子中,它是yyy。
随着在派生类中的覆写而发生的第1个改变是,除函数原型外还会多一个关键字virtual。之前并没有提供new,因为函数new是和隔离于基类中的函数一起被创建的。
override的使用有效地实现了对基类函数的覆写。这使得函数abc成为类xxx中的一个虚函数。换句话说,override变成了virtual,而new则会消失。
因为在基类中有一个newslot修饰符,并且在派生类中有一个具有相同名称的虚函数,所以派生类会被调用。
在虚函数中,对象的运行期间类型会被优先选择。指令callvirt在运行期间解决了这个问题,而不是在编译期间。
a.cs
class zzz
{
public static void Main()
{
yyy a = new xxx();
a.abc();
}
}
class yyy
{
public virtual void abc()
{
System.Console.WriteLine("yyy abc");
}
}
class xxx : yyy
{
public override void abc()
{
base.abc();
System.Console.WriteLine("xxx abc");
}
}
a.il
.method public hidebysig virtual instance void abc() il managed
{
ldarg.0
call instance void yyy::abc()
ldstr "xxx abc"
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
在类xxx中只有函数abc会在上面显示。剩下的IL代码会被省略。base.abc()调用基类的函数abc,即类yyy。关键字base是内存中指向对象的一个引用。C#的这个关键字不能被IL所理解,因为它是一个编译期间的问题。base不关心函数是不是虚的。
无论我们何时首次创建一个虚方法,将它标注为newslot是一个好主意,只是为了表示存在于超类中具有相同名称的所有函数中的一个断点。
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
newobj instance void yyy::.ctor()
callvirt instance void iii::pqr()
ret
}
}
.class interface iii
{
.method public virtual abstract void pqr() il managed
{
}
}
.class public yyy implements iii
{
.override iii::pqr with instance void yyy::abc()
.method public virtual hidebysig newslot instance void abc() il managed
{
ldstr "yyy abc"
call void System.Console::WriteLine(class System.String)
ret
}
.method public hidebysig specialname rtspecialname instance void .ctor() il managed
{
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ret
}
}
Output
yyy abc
我们创建了一个接口iii,它只有一个名为pqr的函数。然后,类yyy实现了接口iii,但是没有实现函数pqr,而是添加了一个名为abc的函数。在入口点函数vijay中,函数pqr会被接口iii调用。
我们之所以没有得到任何错误,是因为override指令的存在。这个指令通知编译器重定向对接口iii的函数pqr以及对类yyy的函数abc的任何调用。编译器对override指令是非常严格的。可以从这样的事实中对此进行考量——如果在类yyy的定义中没有实现iii,那么我们就会得到下列异常:
Output
Exception occurred: System.TypeLoadException: Class yyy tried to override method pqr but does not implement or inherit that methods.
at zzz.vijay()
析构函数
a.cs
class zzz
{
public static void Main()
{
}
~zzz()
{
System.Console.WriteLine("hi");
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ret
}
.method family hidebysig virtual instance void Finalize() il managed
{
ldstr "hi"
call void [mscorlib]System.Console::WriteLine(class System.String)
ldarg.0
call instance void [mscorlib]System.Object::Finalize()
ret
}
}
No output
析构函数被转换为Finalize函数。在C#文档中也制定了这条信息。Finalize函数的调用源于Object。文本"hi"不会显示,因为只要运行时决定了,这个函数就会被调用。我们所知道的全部是——在对象“死亡”时Finalize就会被调用。因此,无论何时一个对象“死亡”,它都会调用Finalize。没有办法销毁任何事物,包括.NET对象在内。
a.cs
class zzz
{
public zzz()
{
}
public zzz(int i)
{
}
public static void Main()
{
}
~zzz()
{
System.Console.WriteLine("hi");
}
}
class yyy : zzz
{
}
a.il
.class private auto ansi yyy extends zzz
{
.method public hidebysig specialname rtspecialname instance void .ctor() il managed
{
ldarg.0
call instance void zzz::.ctor()
ret
}
}
在上面的代码中,我们只显示了类yyy。即使我们有2个构造函数和1个析构函数,类yyy只接收默认的无参构造函数。因此,派生类不会从基类中继承构造函数和析构函数。
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
call void yyy::abc()
ret
}
}
.class private auto ansi yyy extends [mscorlib]System.Array
{
.method public hidebysig static void abc() il managed
{
ldstr "hi"
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
Output
hi
在C#中,不允许我们从像System.Array这样的类中派生一个类,在IL中没有这样的约束。因此,上面的代码不会生成任何错误。
我们确实能够推断出C#编译器具有上面的约束而IL的约束则比较少。一门语言的规则是由编译器在编译期间决定的。
需要说明的是,在C#中,有一些类,是我们不能从中派生的——Delegate、Enum和ValueType。
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class aa V_0)
newobj instance void aa::.ctor()
stloc.0
ret
}
}
.class public auto ansi aa extends bb
{
.method public hidebysig specialname rtspecialname instance void .ctor() il managed
{
ldarg.0
call instance void bb::.ctor()
ldstr "aa"
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
.class public auto ansi bb extends cc
{
.method public hidebysig specialname rtspecialname instance void .ctor() il managed
{
ldarg.0
call instance void cc::.ctor()
ldstr "bb"
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
.class public auto ansi cc extends aa
{
.method public hidebysig specialname rtspecialname instance void .ctor() il managed
{
ldarg.0
call instance void aa::.ctor()
ldstr "cc"
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
Error
Exception occurred: System.TypeLoadException: Could not load class 'aa' because the format is bad (too long?)
at zzz.vijay()
在C#中,循环引用是禁止的。编译器会检查循环引用,并且如果发现了它,就会报告一个错误。然而,IL并不检查循环引用,因为Microsoft不希望所有的程序员都使用纯的IL。
因此,类aa继承自类bb,类bb继承自类cc,最后类cc又继承自类aa。这就形成了一个循环引用。在运行时抛出的异常不会给出循环引用的任何迹象。从而,如果我在这里没有为你揭示这个秘密,那么这个异常就可能让你感到困惑。我并不打算显摆对理解IL有多深这样的事实,但是偶尔给出一些提示信息是无妨的。
a.cs
internal class zzz
{
public static void Main()
{
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ret
}
}
访问修饰符,如关键字internal,只是C#词法的一部分,而与IL没有任何关系。关键字internal表示这个特定的类只能在它所在的文件中被访问到。
因此,通过掌握IL,我们能够区分.NET核心和C#领域存在的特性之间的不同。
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ret
}
}
.class public auto ansi yyy extends xxx
{
}
.class private auto ansi xxx extends [mscorlib]System.Object
{
}
在C#中,有一条规则:基类的可访问性要大于派生类。这条规则在IL中不适用。从而,即使基类xxx是私有的而派生类yyy是公共的,也不会在IL中生成任何错误。
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ret
}
}
在C#中,一个函数的可访问性不能大于它所在类的可访问性。函数vijay是公有的,然而它所在的类却是私有的。因此,这个类对包含在它内部的函数具有更多的约束。再说一遍,在IL中没有强加这样的约束。
a.cs
class zzz
{
public static void Main()
{
yyy a = new yyy();
xxx b = new xxx();
a = b;
b = (xxx) a;
}
}
class yyy
{
}
class xxx : yyy
{
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class yyy V_0,class xxx V_1)
newobj instance void yyy::.ctor()
stloc.0
newobj instance void xxx::.ctor()
stloc.1
ldloc.1
stloc.0
ldloc.0
castclass xxx
stloc.1
ret
}
}
.class private auto ansi yyy extends [mscorlib]System.Object
{
}
.class private auto ansi xxx extends yyy
{
.method public hidebysig specialname rtspecialname instance void .ctor() il managed
{
ldarg.0
call instance void yyy::.ctor()
ret
}
}
如果在xxx中没有构造函数,那么就会抛出下列异常:
Output
Exception occurred: System.InvalidCastException: An exception of type System.InvalidCastException was thrown.
at zzz.vijay()
在上面的例子中,我们创建了2个对象a和b,它们分别是类yyy和xxx的实例。类xxx是派生类而yyy是基类。我们能写出a=b,如果我们使一个派生类和一个基类相等,那么就会生成一个错误。因此,就需要一个转换操作符。
在C#中,cast会被转换为castclass指令,后面紧跟着派生类的名称,也就是要被转换到的类。如果它不能被转换,就会触发上面提到的异常。
在上面的代码中,没有构造函数,从而,就会生成异常。
因此,IL具有大量高级的用来处理对象和类的准则。
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class yyy V_0,class xxx V_1)
newobj instance void yyy::.ctor()
stloc.0
newobj instance void xxx::.ctor()
stloc.1
ldloc.1
stloc.0
ldloc.0
castclass xxx
stloc.1
ret
}
}
.class private auto ansi yyy extends [mscorlib]System.Object
{
}
.class private auto ansi xxx extends [mscorlib]System.Object
{
.method public hidebysig specialname rtspecialname instance void .ctor() il managed
{
ldarg.0
call instance void System.Object::.ctor()
ret
}
}
在上面的例子中,类xxx不再从类yyy中派生。它们都是从Object类中派生的。但是,我们可以把类yyy转换为类xxx。在带有构造函数的类xxx中不会生成任何错误,但是如果移除了这个构造函数,就会生成异常。IL还具有它自己的独特工作方式。
a.il
.assembly mukhi {}
.class private auto ansi sealed zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ret
}
}
.class private auto ansi yyy extends zzz
{
}
文档非常清晰地表示了一个密闭类不能被进一步扩展或子类化。在这个例子中,我们希望看到一个错误但是什么也不会生成。必须提醒你的是,我们现在使用的是beta版本。下一个版本可能会生成一个错误。
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class yyy V_0)
newobj instance void yyy::.ctor()
stloc.0
ret
}
}
.class private auto ansi abstract yyy
{
}
抽象类不能被直接使用。只能从中派生。上面的代码应该生成一个错误,但并不是这样。
a.cs
public class zzz
{
const int i = 10;
public static void Main()
{
System.Console.WriteLine(i);
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
ldc.i4.s 10
call void [mscorlib]System.Console::WriteLine(int32)
ret
ret
}
}
Output
10
常量是只存在于编译期间的一个实体。它在运行期间是不可见的。这就证实了编译器会移除对编译期间对象的所有跟踪。在转换到IL的过程中,在C#中出现的所有int i都会被数字10取代。
a.cs
public class zzz
{
const int i = j + 4;
const int j = k - 1;
const int k = 3;
public static void Main()
{
System.Console.WriteLine(k);
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.field private static literal int32 i = int32(0x00000006)
.field private static literal int32 j = int32(0x00000002)
.field private static literal int32 k = int32(0x00000003)
.method public hidebysig static void vijay() il managed
{
.entrypoint
ldc.i4.3
call void [mscorlib]System.Console::WriteLine(int32)
ret
}
}
Ouput
3
所有的常量都是由编译器计算的,即使它们可能关联到其它常量,但它们会被设定为一个绝对的值。IL运行时不会为文本字段分配任何内存。这涉及到元数据的领域,稍后我们将对其分析。
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.field private static literal int32 i = int32(0x00000006)
.method public hidebysig static void vijay() il managed
{
.entrypoint
ldc.i4.6
stsfld int32 zzz::i
ret
}
}
Output
Exception occurred: System.MissingFieldException: zzz.i
at zzz.vijay()
文本字段表示一个常量值。在IL中,不允许访问任何文本字段。编译器在编译期间不会生成任何错误,但是在运行期间会抛出一个异常。我们希望一个编译期间错误,因为我们在指令stsfld中使用了一个文本字段。
a.cs
public class zzz
{
public static readonly int i = 10;
public static void Main()
{
System.Console.WriteLine(i);
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.field public static initonly int32 i
.method public hidebysig static void vijay() il managed
{
.entrypoint
ldsfld int32 zzz::i
call void [mscorlib]System.Console::WriteLine(int32)
ret
}
.method public hidebysig specialname rtspecialname static void .cctor() il managed
{
ldc.i4.s 10
stsfld int32 zzz::i
ret
}
}
Output
10
只读字段不能被修改。在IL中,有一个名为initonly的修饰符,它实现了相同的概念。
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.field public static initonly int32 i
.method public hidebysig static void vijay() il managed
{
.entrypoint
ldc.i4.s 10
stsfld int32 zzz::i
ldsfld int32 zzz::i
call void [mscorlib]System.Console::WriteLine(int32)
ret
}
}
文档非常清晰地表明了只读字段只能在构造函数中改变,但是CLR不会严格地对此进行检查。可能在下一个版本,他们应该注意这样的事情。
因此,在readonly上的全部约束,必须由(将源代码转换为IL的)程序语言强制执行。我们没有试图在IL上运行,但是IL希望有人在这种情形中进行错误检查。
a.cs
public class zzz
{
public static void Main()
{
zzz a = new zzz();
pqr();
a.abc();
}
public static void pqr()
{
}
public void abc()
{
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.field public static initonly int32 i
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class zzz V_0)
newobj instance void zzz::.ctor()
stloc.0
call void zzz::pqr()
ldloc.0
call instance void zzz::abc()
ret
}
.method public hidebysig static void pqr() il managed
{
ret
}
.method public hidebysig instance void abc() il managed
{
ret
}
}
这个例子是一个更新过的版本。静态函数pqr不会传递这个指针到栈上,但是,非静态函数abc会把这个指针或引用传递到它的变量存储在内存中的位置。
因此,在调用函数abc之前,指令ldloc.0会把zzz的引用放到栈上。
a.cs
public class zzz
{
public static void Main()
{
pqr(10,20);
}
public static void pqr(int i , int j)
{
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.field public static initonly int32 i
.method public hidebysig static void vijay() il managed
{
.entrypoint
ldc.i4.s 10
ldc.i4.s 20
call void zzz::pqr(int32,int32)
ret
}
.method public hidebysig static void pqr(int32 i,int32 j) il managed
{
ret
}
}
调用约定指出了这些参数应该被放到栈上的顺序。在IL中默认的顺序是它们被写入的顺序。因此,数字10会首先进栈,之后是数字20。
Microsoft实现了相反的顺序。从而,20会首先进栈,之后是10。我们不能推论出这个特性。
a.cs
public class zzz
{
public static void Main()
{
bb a = new bb();
}
}
public class aa
{
public aa()
{
System.Console.WriteLine("in const aa");
}
public aa(int i)
{
System.Console.WriteLine("in const aa" + i);
}
}
public class bb : aa
{
public bb() : this(20)
{
System.Console.WriteLine("in const bb");
}
public bb(int i) : base(i)
{
System.Console.WriteLine("in const bb" + i);
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class bb V_0)
newobj instance void bb::.ctor()
stloc.0
ret
}
}
.class public auto ansi aa extends [mscorlib]System.Object
{
.method public hidebysig specialname rtspecialname instance void .ctor() il managed
{
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ldstr "in const aa"
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
.method public hidebysig specialname rtspecialname instance void .ctor(int32 i) il managed
{
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ldstr "in const aa"
ldarga.s i
box [mscorlib]System.Int32
call class System.String [mscorlib]System.String::Concat(class System.Object,class System.Object)
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
.class public auto ansi bb extends aa
{
.method public hidebysig specialname rtspecialname instance void .ctor() il managed
{
ldarg.0
ldc.i4.s 20
call instance void bb::.ctor(int32)
ldstr "in const bb"
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
.method public hidebysig specialname rtspecialname instance void .ctor(int32 i) il managed
{
ldarg.0
ldarg.1
call instance void aa::.ctor(int32)
ldstr "in const bb"
ldarga.s i
box [mscorlib]System.Int32
call class System.String [mscorlib]System.String::Concat(class System.Object,class System.Object)
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
Output
in const aa20
in const bb20
in const bb
我们只创建了一个对象,它是类bb的一个实例。有3个构造函数会被调用,而不是2个构造函数(一个是基类的,一个是派生类的)。
l 在IL中,首先,会调用没有参数的构造函数。
l 然后,当到达构造函数bb时,就会对相同类的另一个带有参数值20的构造函数进行调用。This(20)会被转换为对一个实际的带有一个参数的构造函数的调用。
l 现在,我们转移到bb的一个构造函数上。这里,初始化对aa的一个构造函数的调用,被作为需要首先被调用的基类的构造函数。
幸运的是,aa的基类构造函数不会使我们徒劳无功。在它完成执行之后,就会显示这个字符串,而最后,bb的无参构造函数会被调用。
因此,base和this在IL中是不存在的,它们是编译期间被硬编译到IL代码中的“赝品”。
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class aa V_0)
newobj instance void aa::.ctor()
ret
}
}
.class public auto ansi aa extends [mscorlib]System.Object
{
.method private hidebysig specialname rtspecialname instance void .ctor() il managed
{
ret
}
}
Output
Exception occurred: System.MethodAccessException: aa..ctor()
at zzz.vijay()
我们不能在类的外部访问它的私有成员。因此,正如我们在类bb中创建唯一的私有构造函数那样,我们不能创建任何看上去像类bb的对象。在C#中,同样的规则也适用于访问修饰符。
a.cs
public class zzz
{
public static void Main()
{
yyy a = new yyy();
}
}
class yyy
{
public int i;
public bool j;
public yyy()
{
System.Console.WriteLine(i);
System.Console.WriteLine(j);
}
}
a.il
.assembly mukhi {}
.class public auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (class yyy V_0)
newobj instance void yyy::.ctor()
stloc.0
ret
}
}
.class private auto ansi yyy extends [mscorlib]System.Object
{
.field public int32 i
.field public bool j
.method public hidebysig specialname rtspecialname instance void .ctor() il managed
{
ldarg.0
call instance void [mscorlib]System.Object::.ctor()
ldarg.0
ldfld int32 yyy::i
call void [mscorlib]System.Console::WriteLine(int32)
ldarg.0
ldfld bool yyy::j
call void [mscorlib]System.Console::WriteLine(bool)
ret
}
}
Output
0
False
这里,变量i和j没有被初始化。因此,这些字段没有在类yyy的静态函数中被初始化。在类yyy的任何代码被调用之前,这些变量会分派到它们的默认值,它们依赖于它们的数据类型。在这个例子中,它们是通过int和bool类的构造函数来实现的,因为这些构造函数会首先被调用。
a.cs
class zzz
{
public static void Main()
{
int i = 10;
string j;
j = i >= 20 ? "hi" : "bye";
System.Console.WriteLine(j);
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (int32 V_0,class System.String V_1)
ldc.i4.s 10
stloc.0
ldloc.0
ldc.i4.s 20
bge.s IL_000f
ldstr "bye"
br.s IL_0014
IL_000f: ldstr "hi"
IL_0014: stloc.1
ldloc.1
call void [mscorlib]System.Console::WriteLine(class System.String)
ret
}
}
Output
bye
如果把语句压缩到单独的一行中,那么使用三元操作符会更加“壮丽”。在C#中,变量i和j在转换到IL时变成了V_0和V_1。我们首先将变量V_0初始化为10,随后把条件值20放到栈上。
l 如果条件为TRUE,那么bge.s就会执行到标号IL_0014的跳转。
l 如果条件为FALSE,那么程序就会进行到标号IL_000f。
然后,程序进行到WriteLine函数,并打印出相应的文本。
从最终的IL代码中,无法解释原始的C#代码是否使用一个if语句或?操作符。C#中的大量操作符,例如三元操作符,都是从C程序语言中借用过来的。
a.cs
class zzz
{
public static void Main()
{
int i = 1, j= 2;
if ( i >= 4 & j > 1)
System.Console.WriteLine("& true");
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (int32 V_0,int32 V_1)
ldc.i4.1
stloc.0
ldc.i4.2
stloc.1
ldloc.0
ldc.i4.4
clt
ldc.i4.0
ceq
ldloc.1
ldc.i4.1
cgt
and
brfalse.s IL_001c
ldstr "& true"
call void [mscorlib]System.Console::WriteLine(class System.String)
IL_001c: ret
}
}
C#中的&操作符使if更加复杂。如果条件都是TRUE,那么它就只返回TRUE;否则,它就返回FALSE。在IL中没有&操作符的等价物。因此,会以一种间接方式来实现它,如下所示:
l 首先我们使用ldc指令来把一个常量值当到栈上。
l 接下来,stloc指令初始化变量i和j,即V_0和V_1。
l 然后,V_0的值被放在栈上。
l 之后,检查条件的值4。
l 然后,条件clt用来检查栈上的第1个项是否小于第2个。如果是,正如在上面的示例那样,值1(TRUE)就会被放到栈上。
l C#中的原始表达式是i >= 4。在IL中,会使用<或clt。
l 然后我们使用ceq检查相等性,即=,并把0放到栈上。结果为FALSE。
l 然后我们对j > 1采用相同的规则。这里,我们使用cgt而不是clt。cgt操作符的结果是TRUE。
l 这个结果TRUE和前面的结果FALSE进行AND位运算,最后得到一个FALSE值。
注意到AND指令将返回1,当且仅当这两个条件都是TURE。在所有其它的条件中,它将会返回FLASE。
a.cs
class zzz
{
public static void Main()
{
int i = 1, j= 2;
if ( i >= 4 && j > 1)
System.Console.WriteLine("&& true");
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (int32 V_0,int32 V_1)
ldc.i4.1
stloc.0
ldc.i4.2
stloc.1
ldloc.0
ldc.i4.4
blt.s IL_0016
ldloc.1
ldc.i4.1
ble.s IL_0016
ldstr "&& true"
call void [mscorlib]System.Console::WriteLine(class System.String)
IL_0016: ret
}
}
像&&这样的操作符被称为短路运算符,因为它们只有当第一个条件为True时才会执行第2个条件。我们重复了和先前一样的IL代码,但是现在条件是使用blt.s指令进行检查的,它是clt和brtrue指令的组合。
如果条件为FALSE,就会跳转到标号IL_0016处的ret指令。只有当条件为TRUE时,我们就可以向下进行并检查第2个条件。为此,我们使用ble.s指令,它是cgt和brfalse的组合。如果第2个条件为FALSE,我们就像前面一样跳转到ret指令,如果为TRUE,我们就执行WriteLine函数。
&&操作符执行比&快,因为它只能当第一个条件为TRUE时才会进行到下一步。这样做,第一个表达式的输出就会影响到最后的输出。
|和||操作符也以类似的方式来表现。
a.cs
class zzz
{
public static void Main()
{
bool x,y;
x = true;
y = false;
System.Console.WriteLine( x ^ y);
x = false;
System.Console.WriteLine( x ^ y);
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (bool V_0,bool V_1)
ldc.i4.1
stloc.0
ldc.i4.0
stloc.1
ldloc.0
ldloc.1
xor
call void [mscorlib]System.Console::WriteLine(bool)
ldc.i4.0
stloc.0
ldloc.0
ldloc.1
xor
call void [mscorlib]System.Console::WriteLine(bool)
ret
}
}
Output
True
False
^符号被称为XOR操作符。XOR就像一个OR语句,但是有一点不同:OR只有当它的一个操作数为TRUE(其它的操作数为FALSE)时才会返回TRUE。即使这两个操作数都是TRUE,它也会返回FALSE。xor是一个IL指令。
!=操作符被转换为一组常规的IL指令,即完成一次比较操作,而程序会相应地进入分支。
a.cs
class zzz
{
public static void Main()
{
bool x = true;
System.Console.WriteLine(!x);
}
}
a.il
.assembly mukhi {}
.class private auto ansi zzz extends [mscorlib]System.Object
{
.method public hidebysig static void vijay() il managed
{
.entrypoint
.locals (bool V_0)
ldc.i4.1
stloc.0
ldloc.0
ldc.i4.0
ceq
call void [mscorlib]System.Console::WriteLine(bool)
ret
}
}
Output
False
C#中的!操作符会被转换为TRUE或FALSE,反之亦然。在IL中,使用指令ceq。这个指令检查了栈上最后的2个参数。如果它们相同,就返回TRUE;否则,就返回FALSE。
由于变量x为TRUE,它会被初始化为1。此后,会检查它是否和0相同。因为它们是不相等的,结果为0或FALSE。这个结果会被放到栈上。同样适用的逻辑使x为FALSE。0将会被放到栈上,并检查它是否和另一个0相等。由于它们是匹配的,所以最后的答案将会是TRUE。