程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C >> 關於C >> 把 ref 和 out 關鍵字說透

把 ref 和 out 關鍵字說透

編輯:關於C

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 關鍵字說透

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