按引用傳遞的參數算是C#與很多其他語言相比的一大特色,想要深入理解這一概念應該說不是一件容易的事,再把值類型和引用類型給參雜進來的話就變得更加讓人頭暈了。經??吹接腥税寻匆脗鬟f和引用類型混為一談,讓我有點不吐不快。再加上前兩天碰到的一個有意思的問題,讓我更加覺得應該整理整理關于ref和out的內容了。
一、什么是按引用傳遞
ref和out用起來還是非常簡單的,就是在普通的按值傳遞的參數前加個ref或者out就行,方法定義和調用的時候都得加。ref和out都是表示按引用傳遞,CLR也完全不區分ref還是out,所以下文就直接以ref為例來進行說明。
大家都知道,按值傳遞的參數在方法內部不管怎么改變,方法外的變量都不會受到影響,這從學C語言時候就聽老師說過的了。在C語言里想要寫一個Swap方法該怎么做?用指針咯。那么在C#里該怎么做?雖然也可以用指針,但是更通常也更安全的做法就是用ref咯。
說到這里,有一點需要明確,按值傳遞的參數到底會不會被改變。如果傳的是int參數,方法外的變量肯定是完完全全不變的咯,可是如果傳的是個List呢?方法內部對這個List的所有增刪改都會反映到方法外頭,方法外查一下Count就能看出來了是吧。那么傳List的這個情況,也代表了所有引用類型參數的情況,方法外的變量到底變沒變?不要聽信某些論調說什么“引用類型就是傳引用”,不用ref的情況下引用類型參數仍然傳的是“值”,所以方法外的變量仍然是不變的。
以上總結起來就是一句話:按值傳遞參數的方法永遠不可能改變方法外的變量,需要改變方法外的變量就必須按引用傳遞參數。
PS:不是通過傳參的方式傳入的變量當然是可以被改變的,本文不對這種情況做討論。
二、參數傳遞的是什么
按值傳參傳的就是值咯,按引用傳參傳的就是引用咯,這么簡單的問題還有啥可討論的呢??墒窍胍幌?,值類型變量和引用類型變量組合上按值傳參和按引用傳參,一共四種情況,某些情況下“值”和“引用”可能指的是同一個東西。
先簡單地從變量說起吧,一個變量總是和內存中的一個對象相關聯。對于值類型的變量,可以認為它總是包含兩個信息,一是引用,二是對象的值。前者即是指向后者的引用。對于引用類型的變量,可以認為它也包含兩個信息,一是引用,二是另一個引用。前者仍然是指向后者的引用,而后者則指向堆中的對象。
所謂的按值傳遞,就是傳遞的“二”;按引用傳遞,就是傳遞的“一”。也就是說,在按值傳遞一個引用類型的時候,傳遞的值的內容是一個引用。
大概情況類似于這樣:
按值傳遞時就像是這樣:
可以看到,不管方法內部對“值”和“B引用”作什么修改,兩個變量包含的信息是不會有任何變化的。但是也可以看到,方法內部是可以通過“B引用”對“引用類型對象”進行修改的,這就出現了前文所說的發生在List上的現象。而按引用傳遞時就像是這樣:
可以看到,這個時候方法內部是可以通過“引用”和“A引用”直接修改變量的信息的,甚至可能發生這樣的情況:
這個時候的方法實現可能是這樣的:
void SampleMethod(ref object obj){ //..... obj = new object(); //.....}
三、從IL來看差異
接下來看一看IL是怎么對待按值或者按引用傳遞的參數。比如這一段C#代碼:
class Class{ void Method(Class @class) { } void Method(ref Class @class) { } // void Method(out Class @class) { }}
這一段代碼是可以正常通過編譯的,但是取消注釋就不行了,原因前面也提到了,IL是不區分ref和out的。也正是因為這一種重載的可能性,所以在調用方也必須寫明ref或out,不然編譯器沒法區分調用的是哪一個重載版本。Class類的IL是這樣的:
.class PRivate auto ansi beforefieldinit CsConsole.Class extends [mscorlib]System.Object{ // Methods .method private hidebysig static void Method ( class CsConsole.Class 'class' ) cil managed { // Method begins at RVA 0x20b4 // Code size 1 (0x1) .maxstack 8 IL_0000: ret } // end of method Class::Method .method private hidebysig static void Method ( class CsConsole.Class& 'class' ) cil managed { // Method begins at RVA 0x20b6 // Code size 1 (0x1) .maxstack 8 IL_0000: ret } // end of method Class::Method} // end of class CsConsole.Class
為了閱讀方便,我把原有的默認無參構造函數去掉了??梢钥吹絻蓚€方法的IL僅僅只有一個&符號的差別,這一個符號的差別也是兩個方法可以同名的原因,因為它們的參數類型是不一樣的。out和ref參數的類型則是一樣的?,F在給代碼里加一點內容,讓差別變得更明顯一些:
class Class{ int i; void Method(Class @class) { @class.i = 1; } void Method(ref Class @class) { @class.i = 1; }}
現在的IL是這樣的:
.class private auto ansi beforefieldinit CsConsole.Class extends [mscorlib]System.Object{ // Fields .field private int32 i // Methods .method private hidebysig instance void Method ( class CsConsole.Class 'class' ) cil managed { // Method begins at RVA 0x20b4 // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.1 IL_0001: ldc.i4.1 IL_0002: stfld int32 CsConsole.Class::i IL_0007: ret } // end of method Class::Method .method private hidebysig instance void Method ( class CsConsole.Class& 'class' ) cil managed { // Method begins at RVA 0x20bd // Code size 9 (0x9) .maxstack 8 IL_0000: ldarg.1 IL_0001: ldind.ref IL_0002: ldc.i4.1 IL_0003: stfld int32 CsConsole.Class::i IL_0008: ret } // end of method Class::Method} // end of class CsConsole.Class
帶ref的方法里多了一條指令“ldind.ref”,關于這條指令MSDN的解釋是這樣的:
將對象引用作為 O(對象引用)類型間接加載到計算堆棧上。
簡單來說就是從一個地址取了一個對象引用,這個對象引用與無ref版本的“arg.1”相同的,即按值傳入的@class。再來換一個角度看看,把代碼改成這樣:
class Class{ void Method(Class @class) { @class = new Class(); } void Method(ref Class @class) { @class = new Class(); }}
IL是這樣的:
.class private auto ansi beforefieldinit CsConsole.Class extends [mscorlib]System.Object{ // Methods .method private hidebysig instance void Method ( class CsConsole.Class 'class' ) cil managed { // Method begins at RVA 0x20b4 // Code size 8 (0x8) .maxstack 8 IL_0000: newobj instance void CsConsole.Class::.ctor() IL_0005: starg.s 'class' IL_0007: ret } // end of method Class::Method .method private hidebysig instance void Method ( class CsConsole.Class& 'class' ) cil managed { // Method begins at RVA 0x20bd // Code size 8 (0x8) .maxstack 8 IL_0000: ldarg.1 IL_0001: newobj instance void CsConsole.Class::.ctor() IL_0006: stind.ref IL_0007: ret } // end of method Class::Method} // end of class CsConsole.Class
這一次兩方的差別就更大了。無ref版本做的事很簡單,new了一個Class對象然后直接賦給了@class。但是有ref版本則是先取了ref引用留著待會用,再new了Class,然后才把這個Class對象賦給ref引用指向的地方。在來看看調用方會有什么差異:
class Class{ void Method(Class @class) { } void Method(ref Class @class) { } void Caller() { Class @class = new Class(); Method(@class); Method(ref @class); }}
.method private hidebysig instance void Caller () cil managed { // Method begins at RVA 0x20b8 // Code size 22 (0x16) .maxstack 2 .locals init ( [0] class CsConsole.Class 'class' ) IL_0000: newobj instance void CsConsole.Class::.ctor() IL_0005: stloc.0 IL_0006: ldarg.0 IL_0007: ldloc.0 IL_0008: call instance void CsConsole.Class::Method(class CsConsole.Class) IL_000d: ldarg.0 IL_000e: ldloca.s 'class' IL_0010: call instance void CsConsole.Class::Method(class CsConsole.Class&) IL_0015: ret} // end of method Class::Caller
差別很清晰,前者從局部變量表取“值”,后者從局部變量表取“引用”。
四、引用與指針
說了這么久引用,再來看一看同樣可以用來寫Swap的指針。很顯然,ref參數和指針參數的類型是不一樣的,所以這么寫是可以通過編譯的:
unsafe struct Struct{ void Method(ref Struct @struct) { } void Method(Struct* @struct) { }}
這兩個方法的IL非常有意思:
.class private sequential ansi sealed beforefieldinit CsConsole.Struct extends [mscorlib]System.ValueType{ .pack 0 .size 1 // Methods .method private hidebysig instance void Method ( valuetype CsConsole.Struct& 'struct' ) cil managed { // Method begins at RVA 0x2050 // Code size 1 (0x1) .maxstack 8 IL_0000: ret } // end of method Struct::Method .method private hidebysig instance void Method ( valuetype CsConsole.Struct* 'struct' ) cil managed { // Method begins at RVA 0x2052 // Code size 1 (0x1) .maxstack 8 IL_0000: ret } // end of method Struct::Method} // end of class CsConsole.Struct
ref版本是用了取地址運算符(&)來標記,而指針版本用的是間接尋址運算符(*),含義也都很明顯,前者傳入的是一個變量的地址(即引用),后者傳入的是一個指針類型。更有意思的事情是這樣的:
unsafe struct Struct{ void Method(ref Struct @struct) { @struct = default(Struct); } void Method(Struct* @struct) { *@struct = default(Struct); }}
.class private sequential ansi sealed beforefieldinit CsConsole.Struct extends [mscorlib]System.ValueType{ .pack 0 .size 1 // Methods .method private hidebysig instance void Method ( valuetype CsConsole.Struct& 'struct' ) cil managed { // Method begins at RVA 0x2050 // Code size 8 (0x8) .maxst
新聞熱點
疑難解答