把 ref 和 out 關鍵字說透
如果排版不好,可以到我個人博客上看,我是現在個人博客上寫的,歡迎捧場^^:http://www.dozer.cc
ref 和 out 的區別
網上有很多這方面的文章,但是大部分人總是糾結于他們在原理上的那一點點細微的區別,所以導致了難以區分它們,也不知道什么時候改用哪一個了。
但是如果從試用場景的角度對它們進行區別的話,以后你一定不會再糾結了。
當你明白它們的適用場景后,再去扣其中的原理,使用中的一些問題也就迎刃而解了~
簡單的來說,它們的區別在于:
ref 關鍵字 是作用是把一個變量的引用傳入函數,和 C/C++ 中的指針幾乎一樣,就是傳入了這個變量的棧指針。
out 關鍵字 的作用是當你需要返回多個變量的時候,可以把一個變量加上 out 關鍵字,并在函數內對它賦值,以實現返回多個變量。
幾個簡單的演示
上面說了 ref 和 out 的作用,非常簡單,但是在具體使用的時候卻遇到了很多麻煩,因為 C# 中本身就區分了引用類型和值類型。
我先舉幾個例子,來看看會出現哪些詭異的情況
代碼段一:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
static void Main(string[] args){ int a; Test1(out a);//編譯通過 int b; Test2(ref b);//編譯失敗}static void Test1(out int a){ a = 1;}static void Test2(ref int b){ b = 1;} |
這兩個關鍵字看起來用法一樣,為什么會有合格現象?
網上的答案很簡單:out 關鍵字在傳入前可以不賦值,ref 關鍵字在傳入前一定要賦值。
這是什么解釋?受之于魚但并沒有授之予漁!這到底是為什么呢?
想知道背后真正原理的呢,就繼續看下去吧,后面我講會講到這里的區別。
代碼二:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
static void Main(string[] args){ object a = new object(), b = new object(), c = new object(); Test1(out a); Test2(ref b); Test3(c); //最終 a,b,c 分別是什么? //a,b = null //c 還是 object}static void Test1(out object a){ a = null;}static void Test2(ref object b){ b = null;}static void Test3(object c){ c = null;} |
新建三個 object,object是引用類型;三個函數,分別是 out,ref和普通調用;執行了一樣的語句;最后的結果為什么是這樣呢?
如果你只是從淺層次理解了 out 和 ref 的區別,這個問題你一定回答不上了。(我以前也不知道)
所以,這是為什么呢?繼續往下看。
^_^ 相信很多人暈了,我的目的達到了。(邪惡的笑~~)
那么,下面,我為大家從兩個角度來分析一下。
對于值類型來說,加 out、加 ref 和什么都不加有什么共同點和區別?
對于引用類型來說,加 out、加 ref 和什么都不加有什么共同點和區別?
問題一:關于值類型
普通的傳遞值類型很簡單了,傳的只是一個值,沒難度,平時都是這么用的,很好區分,所以這里就不慘和進去了。
接下來是 ref 和 out 的區別,為什么要了解區別呢?當然是為了了解怎么用它們,簡單的來說就是需要了解:什么時候該用哪個。
個人總結有幾個原則:
如果你是為了能多返回一個變量,那么就應該用 out:
用 out 關鍵字有幾個好處:可以不關心函數外是否被賦值,并且如果在函數內沒有賦值的話就會編譯不通過。(提醒你一定要返回)
你可以把它當成是另一種形式的 return 來用,我們來做一個類比:
return 語句的特點:接收 return 的變量事先不需要賦值(當然如果賦值了也沒關系),在函數內必須 return。
可以看到 out 關鍵字的作用、行為、特點 和 return 是完全一樣的。因為它當初設計的目的就是為了解決這個問題的。
如果你想要像引用類型那樣調用值類型,那你就可以 ref:
傳入值類型的引用后,你可以用它,也可以不用它,你也可以重新修改它的各個屬性,而函數外也可以隨之改變。
我們來把 “傳值類型的引用” 和 “傳引用類型” 來做一個類比:
|
1
2
3
4
5
6
7
8
9
10
11
|
static void Main(string[] args){ int a; Test1(ref a);//錯誤 1 使用了未賦值的局部變量“a” object b; Test2(b);//錯誤 2 使用了未賦值的局部變量“b”}static void Test1(ref int a) { }static void Test2(object b) { } |
傳入加了 ref 的值類型 和 傳入一個引用類型 的作用、行為、特點都是類似的。
同樣,他們同時要遵守一個原則:傳入前必須賦值,這個是為什么呢?
如果賦值后,傳入兩個函數的分別是 int a 的指針 和 object b 的指針。
而不賦值的話,a 和 b 根本還不存在,那它們又怎么會有地址呢?
注意:如果你只寫了 object a ,而在后面的代碼中沒有賦值,它并沒有真正地分配內存。
我們可以看一下三個操作的 IL 代碼:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
private static void Main(string[] args){ //IL_0000: nop object a;//沒做任何事 //IL_0002: ldnull //IL_0003: stloc.1 object b = null;//在棧中增加了一個指針,指向 null //IL_0004: newobj instance void [mscorlib]System.Object::.ctor() //IL_0009: stloc.2 object c = new object();//在棧中增加了一個指針,指向新建的 object 對象} |
傳入引用類型的目的是把一個已經存在的對象的地址傳過去,而如果你只是進行了 object a 聲明,并沒做復制,這行代碼跟沒做任何事!
所以,除非你使用了 out 關鍵字,在不用關鍵字和用 ref 關鍵字的情況下,你都必須事先復制。 out 只是一種特殊的 return
總結:
現在你是否明白,當變量什么情況下該用什么關鍵字了嗎?其實有時候 ref 和 out 都可以達到目的,你需要根據你的初衷,和它們的特點,來衡量一下到底使用哪個了!
另外,我們來看看兩個同樣的函數,用 out 和 ref 時候的 IL 代碼
原函數:
|
1
2
3
4
5
6
7
8
|
private static void Test1(out int a){ a = 1;}private static void Test2(ref int a){ a = 1;} |
IL代碼:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
.method private hidebysig static void Test1 ( [out] int32& a ) cil managed{ // Method begins at RVA 0x2053 // Code size 5 (0x5) .maxstack 8 IL_0000: nop IL_0001: ldarg.0 IL_0002: ldc.i4.1 IL_0003: stind.i4 IL_0004: ret} // end of method Program::Test1.method private hidebysig static void Test2 ( int32& a ) cil managed{ // Method begins at RVA 0x2059 // Code size 5 (0x5) .maxstack 8 IL_0000: nop IL_0001: ldarg.0 IL_0002: ldc.i4.1 IL_0003: stind.i4 IL_0004: ret} // end of method Program::Test2 |
發現了嗎? 它們在函數內部完全是一樣的!因為他們的原理都是傳入了這個變量的引用。只是 out 關鍵字前面出現了一個標記 [out]
它們在原理上的區別主要在于編譯器對它們進行了一定的限制。
最上面“代碼段一”中的問題你現在明白了嗎?
問題二:關于引用類型
對于值類型來說,最難區別的是 ref 和 out,而對于引用類型來說就不同了。
首先,引用類型傳的是引用,加了 ref 以后也是引用,所以它們是一樣的?暫時我們就這么認為吧~ 我們暫時認為它們是一樣的,并統稱為:傳引用。
所以,對于引用類型來說,out 和 傳引用 的區別跟對于值類型傳 ref 和 out 的區別類似,具體適用場景也和值類型類似,所以就不多加闡述了。
雖然我們說直接傳和加 ref 都可以統稱為傳引用,但是它們還是有區別的!而且是一個很隱蔽的區別。
我們再來看一下最上面的代碼段二:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
static void Main(string[] args){ object a = new object(), b = new object(), c = new object(); Test1(out a); Test2(ref b); Test3(c); //最終 a,b,c 分別是什么? //a,b = null //c 還是 object}static void Test1(out object a){ a = null;}static void Test2(ref object b){ b = null;}static void Test3(object c){ c = null;} |
out 關鍵字就相當于 return ,所以內部賦值為 null ,就相當于 return 了 null
可是,為什么引用類型還要加 ref 呢?它本身部已經是引用了嗎?為什么加了以后就會有天大的區別呢?!
用一句話概括就是:不加 ref 的引用是堆引用,而加了 ref 后就是棧引用! @_@ 好搞啊。。什么跟什么?讓我們一步步說清楚吧!
正常的傳遞引用類型:
加了 ref 的傳遞引用類型:
這兩張圖對于上面那句話的解釋很清楚了吧?
如果直接傳,只是分配了一個新的棧空間,存放著同一個地址,指向同一個對象。
內外指向的都是同一個對象,所以對 對象內部的操作 都是同步的。
但是,如果把函數內部的 obj2 賦值了 null,只是修改了 obj2 的引用,而 obj1 依然是引用了原來的對象。
所以上面的例子中,外部的變量并沒有收到影響。
同樣,如果內部的對象作了 obj2 = new object() 操作以后,也不會對外部的對象產生任何影響!
而加了 ref 后,傳入的不是 object 地址,傳入的是 object 地址的地址!
所以,當你對 obj2 賦 null 值的時候,其實是修改了 obj1 的地址,而自身的地址沒變,還是引用到了 obj1
雖然在函數內部的語句是一樣的,其實內部機制完全不同。我們可以看一下IL代碼,一看就知道了!
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
.method private hidebysig static void Test1 ( object a ) cil managed{ // Method begins at RVA 0x2053 // Code size 5 (0x5) .maxstack 8 IL_0000: nop IL_0001: ldnull IL_0002: starg.s a IL_0004: ret} // end of method Program::Test1.method private hidebysig static void Test2 ( object& a ) cil managed{ // Method begins at RVA 0x2059 // Code size 5 (0x5) .maxstack 8 IL_0000: nop IL_0001: ldarg.0//多了這行代碼 IL_0002: ldnull IL_0003: stind.ref IL_0004: ret} // end of method Program::Test2 |
上面是直接傳入,并賦 null 值的
下面是加 ref 的
我們可以發現僅僅是多了一行代碼:IL_0001: ldarg.0
其實,這樣代碼的作用就是講參數0加載到堆棧上,也就是先根據引用,找到了外部的變量,然后再根據外部的變量,找到了最終的對象!
那現在你知道什么時候該加 ref,什么時候不用加 ref 了嗎?
再看了一個例子:
|
1
2
3
4
5
6
7
8
|
private static void Test1(List<int> list){ list.Clear();}private static void Test2(ref List<int> list){ list = new List<int>();} |
同樣是清空一個 List,如果沒加 ref ,只能用 clear。
而加了 ref 后可以直接 new 一個新的~
如果你沒加 ref 就直接 new 一個新的了,抱歉,外部根本不知道有這個東西,你們操作的將不是同一個 List
所以,你一定要了解這點,并注意一下幾件事:
1、一般情況下不要用 ref
2、如果你沒加 ref,千萬別直接給它賦值,因為外面會接收不到…
現在你全部明白了嗎?^_^
原文地址:把 ref 和 out 關鍵字說透



浙公網安備 33010602011771號