程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> [C#]淺析ref、out參數,

[C#]淺析ref、out參數,

編輯:C#入門知識

[C#]淺析ref、out參數,


按引用傳遞的參數算是C#與很多其他語言相比的一大特色,想要深入理解這一概念應該說不是一件容易的事,再把值類型和引用類型給參雜進來的話就變得更加讓人頭暈了。
經常看到有人把按引用傳遞和引用類型混為一談,讓我有點不吐不快。再加上前兩天碰到的一個有意思的問題,讓我更加覺得應該整理整理關於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參數的類型則是一樣的。
現在給代碼裡加一點內容,讓差別變得更明顯一些:

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)
        .maxstack 8

        IL_0000: ldarg.1
        IL_0001: initobj CsConsole.Struct
        IL_0007: ret
    } // end of method Struct::Method

    .method private hidebysig 
        instance void Method (
            valuetype CsConsole.Struct* 'struct'
        ) cil managed 
    {
        // Method begins at RVA 0x2059
        // Code size 8 (0x8)
        .maxstack 8

        IL_0000: ldarg.1
        IL_0001: initobj CsConsole.Struct
        IL_0007: ret
    } // end of method Struct::Method

} // end of class CsConsole.Struct

兩個方法體的IL是一模一樣的!可以想見引用的本質到底是什麼了吧~?

五、this和引用

這個有趣的問題是前兩天才意識到的,以前從來沒有寫過類似這樣的代碼:

struct Struct
{
    void Method(ref Struct @struct) { }

    public void Test()
    {
        Method(ref this);
    }
}

上面這段代碼是可以通過編譯的,但是如果像下面這樣寫就不行了:

class Class
{
    void Method(ref Class @class) { }

    void Test()
    {
        // 無法將“<this>”作為 ref 或 out 參數傳遞,因為它是只讀的
        Method(ref this);
    }
}

紅字部分代碼會報出如注釋所述的錯誤。兩段代碼唯一的差別在於前者是struct(值類型)而後者是class(引用類型)。
前面已經說過,ref標記的參數在方法內部的修改會影響到方法外的變量值,所以用ref標記this傳入方法可能導致this的值被改變。
有意思的是,為什麼struct裡的this允許被改變,而class裡的this不允許被改變呢?

往下的內容和ref其實沒啥太大關系了,但是涉及到值和引用,所以還是繼續寫吧:D

MSDN對“this”關鍵字的解釋是這樣的:

 this 關鍵字引用類的當前實例

這裡的“當前實例”指的是內存中的對象,也就是下圖中的“值”或“引用類型對象”:

如果對值類型的this進行賦值,那麼“值”被修改,“當前實例”仍然是原來實例對象,只是內容變了。
而如果對引用類型的this進行復制,那麼“B引用”被修改,出現了類似於這個圖的情況,現在的“當前實例”已經不是原來的實例對象了,this關鍵字的含義就不再明確。所以引用類型中的this應該是只讀的,確保“this”就是指向的“這個”對象。

最後也沒想到有啥可多說的,那就到此為止吧~

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