程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> 《C# to IL》第四章 關鍵字和操作符(上)

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

編輯:關於C#

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved