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

.NET技术    2014-06-27 06:47  

位于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

在线留言

我要留言