(翻译) 《C# to IL》第四章 关键字和操作符

时间:2022-02-15 17:45:29

-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#文件中的这两个命名空间vijaymukhi都会被合并到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中可以很方便的使用指针。IL3种类型的指针。

当函数abc被调用时,变量i会被作为一个引用参数传递到函数中。在IL中,ldloca.s指令会被调用,它把变量的地址放到栈上。替代地,如果这个指令是ldloc,那么就会把变量的值放到栈上。

       在函数调用中,我们添加符号&到类型名称的结尾来表示变量的地址。数据类型后面的&后缀表示变量的内存位置,而不是在变量中包括的值。

       在函数本身中,ldarg.1用于把参数1的地址放到栈上。然后,我们把想要初始化的数值放到栈上。在上面的例子中,我们首先把变量i的地址放到栈上,随后是我们想要初始化的值,即10。

       stind指令把出现在栈顶的值,也就是10,放到变量中,这个变量的地址存储为栈上的第2项。在这个例子中,因为我们传递变量i的地址到栈上,所以变量i分配到值10。

    当在栈上给出一个地址时,使用stind指令。它会使用特定的值填充该内存位置。

 

       如果使用关键字ref取代out,那么IL还是会显示相同的输出,因为不管是哪种情形,变量的地址都会被放到栈上。因此,refoutC#实现中的“人为”概念,而在IL中没有任何等价的表示。

IL代码无法知道原始的程序使用的是ref还是out。因此,在反汇编这个程序时,我们将无法区别refout,因为这些信息在从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

 

无论编译器何时对变量进行处理,都会在编译器间忽略它们的值。在上面的程序中会执行以下步骤:

变量st会被相应地转换为V_0V_1

为局部变量V_0分配字符串"hi"

随后这个变量会被放到栈上。

接下来,常量字符串"bye"会被放到栈上。

之后,+操作符被转化为静态函数Concat,它属于String类。

这个方法会连接两个字符串并在栈上创建一个新的字符串。

这个合成的字符串会被存储在变量V_1中。

最后,这个合成的字符串会被打印出来。

 

C#中,有两个PLUS+)操作符。

一个处理字符串。这个操作符会被转换为ILString类的Concat函数。

另一个则处理数字。这个操作符会被转换为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语句中的代码。

       在从ILC#的再次转换中,原始的预处理指令是无法被恢复的。

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函数中的调用要基于下面的标准:

如果类xxx有一个函数abc,那么在函数vijay中的调用将具有前缀xxx。

如果类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个版本,哪个将会被调用?

出现在基类yyy中的函数abc,调用对象属于这个函数。

函数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程序中包括默认的无参构造函数。

       如果没有关键字newoverride,默认使用的关键字就是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#中,有一些类,是我们不能从中派生的——DelegateEnumValueType

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个对象ab,它们分别是类yyyxxx的实例。类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个构造函数(一个是基类的,一个是派生类的)。

IL中,首先,会调用没有参数的构造函数。

然后,当到达构造函数bb时,就会对相同类的另一个带有参数值20的构造函数进行调用。This(20)会被转换为对一个实际的带有一个参数的构造函数的调用。

现在,我们转移到bb的一个构造函数上。这里,初始化对aa的一个构造函数的调用,被作为需要首先被调用的基类的构造函数。

 

幸运的是,aa的基类构造函数不会使我们徒劳无功。在它完成执行之后,就会显示这个字符串,而最后,bb的无参构造函数会被调用。

       因此,basethisIL中是不存在的,它们是编译期间被硬编译到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

 

       这里,变量ij没有被初始化。因此,这些字段没有在类yyy的静态函数中被初始化。在类yyy的任何代码被调用之前,这些变量会分派到它们的默认值,它们依赖于它们的数据类型。在这个例子中,它们是通过intbool类的构造函数来实现的,因为这些构造函数会首先被调用。

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#中,变量ij在转换到IL时变成了V_0V_1。我们首先将变量V_0初始化为10,随后把条件值20放到栈上。

如果条件为TRUE,那么bge.s就会执行到标号IL_0014的跳转。

如果条件为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中没有&操作符的等价物。因此,会以一种间接方式来实现它,如下所示:

首先我们使用ldc指令来把一个常量值当到栈上。

接下来,stloc指令初始化变量i和j,即V_0和V_1。

然后,V_0的值被放在栈上。

之后,检查条件的值4。

然后,条件clt用来检查栈上的第1个项是否小于第2个。如果是,正如在上面的示例那样,值1(TRUE)就会被放到栈上。

C#中的原始表达式是i >= 4。在IL中,会使用<或clt。

然后我们使用ceq检查相等性,即=,并把0放到栈上。结果为FALSE

然后我们对j > 1采用相同的规则。这里,我们使用cgt而不是cltcgt操作符的结果是TRUE

这个结果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指令进行检查的,它是cltbrtrue指令的组合。

       如果条件为FALSE,就会跳转到标号IL_0016处的ret指令。只有当条件为TRUE时,我们就可以向下进行并检查第2个条件。为此,我们使用ble.s指令,它是cgtbrfalse的组合。如果第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,它也会返回FALSExor是一个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

由于变量xTRUE,它会被初始化为1。此后,会检查它是否和0相同。因为它们是不相等的,结果为0FALSE。这个结果会被放到栈上。同样适用的逻辑使xFALSE0将会被放到栈上,并检查它是否和另一个0相等。由于它们是匹配的,所以最后的答案将会是TRUE