網上有很多這方面的文章,但是大部分人總是糾結於他們在原理上的那一點點細微的區別,所以導致了難以區分它們,也不知道什麼時候改用哪一個了。
但是如果從試用場景的角度對它們進行區別的話,以後你一定不會再糾結了。
當你明白它們的適用場景後,再去扣其中的原理,使用中的一些問題也就迎刃而解了~
簡單的來說,它們的區別在於:
ref 關鍵字 是作用是把一個變量的引用傳入函數,和 C/C++ 中的指針幾乎一樣,就是傳入了這個變量的棧指針。
out 關鍵字 的作用是當你需要返回多個變量的時候,可以把一個變量加上 out 關鍵字,並在函數內對它賦值,以實現返回多個變量。
上面說了 ref 和 out 的作用,非常簡單,但是在具體使用的時候卻遇到了很多麻煩,因為 C# 中本身就區分了引用類型和值類型。
我先舉幾個例子,來看看會出現哪些詭異的情況
代碼段一:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17static
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 24static
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 11static
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 13private
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 8private
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 24static
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 8private
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 關鍵字說透