前言
菜鳥去重復之Sql的問題還沒有得到滿意的答案。如果哪位大哥有相關的資料解釋,能夠分享給我,那就太謝謝了。
接觸C#一年了,感覺很多東西還是很模糊,像C#中的委托和事件
有些東西看多了不用也還是不會。還有些東西用多了不想也還是不精。
這次發現一篇解除我對於C#里面參數傳遞困惑的詳細條例文章,忍不住翻譯留存以備回顧。
英文好的可以直接點此處看原文了。MSDN相關解釋鏈接在此處。
前奏:引用類型
在C#中有兩種常用的類型:引用類型和值類型。他們表現不同,很多人在使用他們的時候都感到了困惑,這里簡單解釋下他們的區別:
引用類型指引用類型的變量存儲對實際數據的引用。如下代碼:
StringBuilder sb = new StringBuilder();
這里我們定義了一個變量sb,創建一個新的StringBuilder對象,並把這個對象的引用賦給sb。sb實際存儲的值不是該對象,只是它的引用。
引用類型之間的賦值只是簡單地將其表達式或者變量賦給對應的變量。再看如下代碼:
StringBuilder first = new StringBuilder(); first.Append("hello"); StringBuilder second = first; Console.WriteLine(second); // Prints hello
這里我們定義了一個變量first,創建了一個新的StringBuilder對象,並把這個對象的引用賦給first。然后我們將first賦給second。
這意味着他們都指向了同一個對象。如果我們通過使用first.Append的方式修改該對象的內容,second也同樣變化了。如下:
StringBuilder first = new StringBuilder(); first.Append("hello"); StringBuilder second = first; first.Append(" world"); Console.WriteLine(second); // Prints hello world
雖然會這樣,但他們仍然應該被看作相互獨立的變量。修改first指向一個完全不同的對象(或者直接將null賦給它)完全不會影響second。
StringBuilder first = new StringBuilder(); first.Append("hello"); StringBuilder second = first; first.Append(" world"); first = new StringBuilder("goodbye"); Console.WriteLine(first); // Prints goodbye Console.WriteLine(second); // Still prints hello world
類(class),接口(interface),委托(delegate)和數組(array)都是引用類型。
前奏:值類型
引用類型在變量與真實數據之間有個間接層,值類型卻不是。值類型變量直接存儲其數據。
值類型之間賦值是將其值拷貝一份然后賦給對應的變量。下面用一個結構類型來解釋下:
public struct IntHolder { public int i; }
IntHolder是個結構體的值類型,它包含一個單獨的整型i。對其賦值將會拷貝,如下所示:
IntHolder first = new IntHolder(); first.i = 5; IntHolder second = first; first.i = 6; Console.WriteLine (second.i); //輸出5
這里second.i值為5是因為當second=first時second.i保存了first.i的拷貝。自此,second是與first相互獨立的。
所以即使后來first.i=6,second.i依然不變。
簡單的類型(例如float,int,char),enum類型和struct類型都是值類型。
Note:很多類型(例如string)是引用類型,但表現的卻像是值類型。這些是不可變類型,就是這些類型的實例一旦創建就不能再改變。
這使得引用類型在一些方面表現得值類型一樣——尤其是當你使用一個指向是不可變的對象的引用時,
你就不用擔心把它傳遞到一個方法里面或者從某個方法返回后值不對。
無論如何,你總是知道這個變量引用的對象的內容。這也是為什么string.Replace不改變該string的值,而是返回一個新的string實例。
如果string改變了,通過其它所有指向它的引用獲取到的string也改變了,那明顯不是我們期望的。
用一個可變的引用類型對比(例如ArrayList)加深大家理解。
如果一個方法的返回值為保存在變量中的一個ArrayList類型的引用,在方法內部並沒有創建新的實例直接使用,
而是對一個已有的實例進行增加等一些操作,其它人員卻不知道,這就可能出現問題。
之前說過了不可變的引用類型表現得像值類型,但實際不是值類型,大家千萬不要被其表面迷惑,理解其實質,才能更好地運用。
測試你的理解
如果之前的IntHolder不是結構類型而是類,那輸出的結果是多少呢?如果你不理解為什么是6,那估計是我翻譯的有問題,
或者我翻譯得不夠清楚,實在罪過。如果您有好的建議,請告知我,不勝感激。
這里再次奉上原文鏈接,以備有人實在看不下去我過爛的翻譯。
間奏:不同類型的參數
在C#中有四種不同類型的參數:值類型參數(默認),引用類型(ref),out類型,參數數組(params)。
你可以同時使用值類型和引用類型參數。
當你使用這些參數的時候,你應該在腦海里對“值類型”和“引用類型”有非常清晰的認識。
這樣不管是在使用它們還是與它們相關的類型的時候,你都會感覺非常輕松。
值類型參數
C#中默認參數是值類型參數,這意味着在方法成員聲明時會為其變量會創建一份拷貝,它就是你在方法內部調用指定的變量的初始值。
如果你改變這個值,不會對調用中傳的值源造成任何影響。看下代碼:
void Foo (StringBuilder x) { x = null; } ... StringBuilder y = new StringBuilder(); y.Append ("hello"); Foo (y); Console.WriteLine (y==null);// 輸出False
y的值沒有改變只是因為x被賦null。無論如何,請一定記住引用類型變量存儲的是一個引用——如果兩個引用指向同一個對象,
那么改變對象的內容,通過這兩個引用獲取到的對象也會發生改變。例如:
void Foo (StringBuilder x) { x.Append (" world"); } ... StringBuilder y = new StringBuilder(); y.Append ("hello"); Foo (y); Console.WriteLine (y); //輸出 hello world
在調用Foo(y)之后,y指向的值變為“hello world”,在Foo方法內部對引用變量x調用Append拼接“ world”字符實現了效果。
這里我們再來看下值類型參數傳遞如何。正如之前提到的,值類型的值就是它本身。
使用之前的結構類型IntHolder,我們寫一些與之前類似的代碼來測試下:
void Foo (IntHolder x) { x.i=10; } ... IntHolder y = new IntHolder(); y.i=5; Foo (y); Console.WriteLine (y.i); //輸出5
當Foo被調用時,x是個struct類型,並且它的i為5。之后將10賦給了i。
Foo一點都不知道y。當這個方法執行結束,y還是和它之前一模一樣。
我們之前展示了一些關於引用類型作為值類型參數傳遞的例子。
那么你應該明白當IntHolder聲明為類時會發生什么。你應該清楚為什么y.i會因此變成10。
引用類型參數
引用類型參數使用的時候不傳遞其實際值,只是使用變量本身。也就是說不會創建一個新的拷貝,而是使用相同的存儲地址。
因此在方法成員中的值類型與引用類型一直都是一樣的。
使用引用類型參數在聲明和調用的時候需要使用ref關鍵字——這意味着你要使用引用類型參數將會看起來清晰明確。
我們再來改動下之前的例子測試下:
void Foo (ref StringBuilder x) { x = null; } ... StringBuilder y = new StringBuilder(); y.Append ("hello"); Foo (ref y); Console.WriteLine (y==null); //輸出True
這里,因為將y的引用傳遞給x而不是它的值,所以對x進行的操作相當於對y進行操作一樣。在上例中y最后為null。
請將這個結果與上面的沒有使用ref關鍵字的例子結果進行比較。
現在,讓我們測試下之前使用結構作為參數的例子加上ref關鍵字后會發生什么:
void Foo (ref IntHolder x) { x.i=10; } ... IntHolder y = new IntHolder(); y.i=5; Foo (ref y); Console.WriteLine (y.i); //輸出10
這兩個變量共用一個內存地址,因此改變x時y也會發生改變。所以y.i是10。
注意:通過默認的值類型參數方式傳遞引用類型變量與通過引用類型參數方式傳遞值類型變量
有什么區別呢?
你可能已經注意到了在最后一個例子中,將一個結構類型通過引用方式傳遞,與通過值類型方式傳遞一個類有同樣的結果。
但這不意味他們是一樣的。好吧,還是讓我們通過下面的代碼加深下理解:
void Foo (??? IntHolder x) { x = new IntHolder(); } ... IntHolder y = new IntHolder(); y.i=5; Foo (??? y);假設IntHolder是個結構類型(即值類型),方法參數為引用類型(即將???替換為ref),
執行代碼,y最后將會指向一個new IntHolder,y.i會因此變為0。
假設IntHolder是個類(即引用類型),方法參數為值類型(即將???去掉),
執行代碼,y的指向不會改變,y.i仍為5。
在調用函數前y與x確實指向同一個對象。理解C#參數傳遞中的這個區別絕對是相當關鍵的。
這也是為什么原作者認為當人們談及對象默認引用傳遞的時候會非常困惑,其實正確的說法應該是引用類型默認值傳遞。
Out類型參數
Out類型參數和引用類型參數很像,它不會創建新的內存地址,而是共享內存地址。
使用Out類型參數的方法在聲明和調用的時候需要加上關鍵字Out。這樣也會使代碼看起來清晰。
雖然Out類型參數與引用類型參數非常相似,但它們還是有區別的。Out類型與引用類型的不同之處:
1.方法調用時傳遞的變量不用事先賦值。如果方法調用正常結束,即可認為該變量后來被賦值了(這樣,你就可以直接讀取它的值了)。
2.該參數傳遞時被看作沒有初始化(也就是說你在讀取它之前,必須先給它賦值)。
3.在方法結束前必須對這個參數進行賦值,否則編譯器會報錯。
下面展示一個例子進行加深大家理解,使用的是一個int型作為參數
int是值類型,如果你已經理解了引用類型,相信你也會知道引用類型作為參數會發生什么:
void Foo (out int x) { //這里不能讀取x,它被認為是沒有初始化的,讀取會報錯 //賦值-在方法結束前必須對其進行賦值,否則報錯 x = 10; // 這里x可以讀取了: int a = x; } ... // 聲明一個沒有初始化的變量 int y; // 雖然y沒有初始化,但可以作為out類型參數傳遞 Foo (out y); // 現在y已經有值了,輸出: Console.WriteLine (y); //輸出10
參數數組(params)
參數數組允許傳遞給一個方法一組數據。定義包含參數數組的方法時必須加上關鍵字params,調用該方法的時候卻不必加上這個關鍵字。
參數數組必須放在方法參數的最后,並且只能是一維數組。
當使用這類方法時,調用中只要參數與定義時參數數組類型兼容,就能夠傳遞。
由於參數數組的這種使用方式,所以當你想傳遞一個單獨的數組,它的效果就像是值類型參數傳遞。例如:
void ShowNumbers (params int[] numbers) { foreach (int x in numbers) { Console.Write (x+" "); } Console.WriteLine(); } ... int[] x = {1, 2, 3}; ShowNumbers (x); ShowNumbers (4, 5); //輸出: 1 2 3 4 5
第一次調用時,x是一個整型數組,效果等同於將它(引用)作為值類型參數傳遞。
第二次調用時,將會創建一個包含4,5的整型數組,並將它的引用傳遞(仍然是值類型參數)。
尾奏:總結
翻譯出來總是容易使文章讀起來拗口難理解。第一次翻譯技術文章,我肯定也避免不了。只求能夠對大家及我有所幫助。
程序員,英語很重要。
如果您英文比較好的話,我還是建議您讀一下原文。
如果本文中有疏漏或者理解錯誤的地方,還請指出,不勝感激。