參數,也叫參變量,是一個變量。在方法簽名中隨處可見,實現了不一樣方法間對於數據的寄雁傳書,基本上充斥在代碼的各個角落裏。
在方法簽名或者原型中,方法名稱後的括號包含方法的參數及其類型的完整列表。參數聲明指定參數中存儲的值的類型、大小和標識符。
然而小小參數的背後其實也是有着大大的學問,因此本篇博文,您能夠和博主一塊兒把C#裏面各式各樣的參數複習一遍。
咱們先簡單回顧一下各類各樣的參數概念,對不一樣類型參數的使用場景有一個瞭解,再慢慢深刻探討參數的傳遞,內存堆棧分佈,抽絲剝繭,步步爲營,帶着思考由淺入深的去閱讀本文。數組
形參全稱爲「形式參數」,因爲它不是實際存在變量,因此又稱虛擬變量。
形參是在定義方法簽名的時候使用的參數,目的是用來接收調用該方法時傳遞的參數(值),它的做用是實現主調方法與被調方法之間的聯繫。
形參只在方法內部有效,方法調用結束返回主調用方法後則不能再使用該形參變量。
形參(自身也是變量)和局部變量有所區別,且在方法內部(做用域內)不容許存在一個同名的局部變量,哪怕它們類型是相同的。安全
//oldValue、parameter一、optionalParam一、optionalParam2就是Change方法的形參 //方法簽名若是有多個形參,則多個形參用逗號隔開 private static void Change<T>(T oldValue, object parameter1, object optionalParam1 = null, object optionalParam2 = null) { T newValue = default(T); oldValue = newValue; }
實參全稱爲"實際參數",是在調用時傳遞給方法的參數,即傳遞給被調用方法的值,形參實際上就是實參的替身。
實參能夠是常量、變量、表達式、方法等,不管實參是何種類型的量或值,在進行方法調用時,它們都必須具備肯定的值,以便把這些值傳送給形參。 所以應預先用賦值,輸入等辦法使實參得到肯定值。
實參和形參在數量上,類型上、順序上應嚴格一致,不然就會發生類型不匹配的錯誤。
app
private static void Main() { int a = 5; //傳進去2個實參,實參1爲變量,實參2爲值 //實參初始化了形參的初始值 Change(a, 99); }
C# 4.0 中引入的命名實參,可以爲特定形參指定實參,方法的調用者將再也不須要記住或查找形參在所調用方法的形參列表中的順序,能夠按形參名稱指定每一個實參的形參。
less
private static void Main() { Change(99, 88); //若是不記得形參的順序,但卻知道其名稱,能夠按任意順序發送實參。 Change(oldValue: 99, parameter1: 88); Change(parameter1: 99, oldValue: 88); //命名實參還能夠標識每一個實參所表示的含義,從而改進代碼的可讀性。 //命名實參能夠放在位置實參後面,如此處所示。 Change(99, parameter1: 88); //可是,位置實參不能放在命名實參後面。 下面的語句會致使編譯器報錯。 //Change(parameter1: 99, 88); }
若是方法簽名的形參比較多,則命名實參技術的使用會使得方法調用變的簡便許多。
可是,這種簡便性是以犧牲方法簽名自由修改的靈活性爲代價的。
若是被調用的方法封裝在外部dll且不開源,則DLL一旦升級並改變方法的形參名稱,則存有依賴的客戶端命名實參代碼會報錯,以下圖所示:
ide
C# 4.0 中還提供了可選參數,任何調用都必須爲全部必需的形參提供實參,但能夠爲可選的形參省略實參。若是沒有爲該形參發送實參,則使用定義時的默認值。
每一個可選形參都必須有一個初始化的默認值做爲其定義的一部分。
可選形參在形參列表的末尾定義,位於任何須需的形參以後。
若是調用方爲一系列可選形參中的任意一個形參提供了實參,則它必須爲前面的全部可選形參提供實參。
函數
private static void Main() { //下面對 Change 的調用致使編譯器報錯,緣由是調用者爲任意一個形參提供了實參,則它必須爲前面的全部可選形參提供實參。 //Change(3, 2, , 4); //若是你想跳過第三個可選形參,則可使用命名實參來達到目的。 Change(3, optionalParam2: 4, parameter1: 99); }
智能提示使用中括號指示可選形參,以下圖所示:
命名實參和可選參數這兩種技術均可與方法、索引器、構造函數和委託一塊兒使用。動畫
若是方法簽名的參數列表存在多個數量不固定的形參,那麼params能夠幫你解決這個問題。
在方法聲明中的 params 關鍵字以後不容許任何其餘參數,而且在方法聲明中只容許一個 params 關鍵字。ui
private static void Change(params object[] aryParameters) { } private static void Main() { //若是不傳實參,則aryParameters長度爲0 Change(); Change(3); Change(3, 4, 8); Change(new string[] { "as", "er" }); }
若是同時使用命名實參、可選參數,params ,方法重載等功能時,可能會形成同一個方法調用或者實參列表能夠適用多個方法簽名的狀況,那麼就須要編譯器對其作出方法解析和重載決策。this
多個方法都適用調用的實參列表時,優先選擇無可選參數的方法,以下圖所示:
spa
多個方法都適用調用的實參列表時,優先選擇形參類型更加具體的方法,以下圖所示:
多個方法(且方法的形參均爲可選)都適用調用的實參列表時,優先選擇可選參數更少的方法,以下圖所示:
概念1:值類型,值類型的變量直接包含其數據,分配在線程堆棧(Thread Stack)上。
概念2:引用類型,引用類型的變量存儲對其數據(對象實例)的引用(內存地址),該引用類型的變量(內存地址)會被分配到線程堆棧上,而被引用的數據(對象實例)則會被分配到託管堆(Heap)。
概念3:參數的值傳遞。
概念4:參數的引用傳遞。
以上4種概念,必定不能混淆,特別是引用類型和引用傳遞。
因爲本篇博文主要是講解參數,因此在此就不對值類型和引用類型的基礎概念作深刻講解了。
通常按照參數類型和傳遞方式的不一樣,能夠分爲如下4種參數傳遞的狀況:
在進行參數的值傳遞時,當傳遞的參數爲值類型時,實際上傳遞的是該值類型實例的一個拷貝副本。所以方法操做的是實例副本,因此不會對實例自己構成任何影響。
private static void ChangeValue<T>(T oldValue) { T newValue = default(T); oldValue = newValue; } private static void Main() { int a = 100; //傳遞的是值類型實例的副本,因此針對方法內部的改變絲絕不會影響到實例自己 ChangeValue(a); Console.WriteLine(a);//輸出是:100 }
經過指針操做來更進一步的加深理解,注意看下圖的實例的指針地址和實例副本的指針地址是不同的:
由此能夠看出這是2個不一樣的內存塊,因此ChangeValue改變的僅僅只是實例副本的內存塊裏面的值。
在進行參數的值傳遞時,當傳遞的參數爲引用類型時,實際上傳遞的是該引用類型實例的引用的一個拷貝副本(稍微有點繞),所以方法操做的是引用類型實例的引用的副本。
若是方法不改變引用副本的指向,那麼在方法中對參數所作的任何更改都將反映在該變量中。
可是,若是方法改變了引用副本的指向,那麼不會對實例自己構成任何影響。
有點繞且拗口,接下來,咱們上幾個典型的代碼例子,並加以講解,從而幫助咱們深層次的理解值傳遞。
private static void ChangeValue(string value) { //此時傳遞進來的value副本和value都指向"init" value = "update";//value副本作賦值操做,分配一個新的string類型實例對象(new string("update")),value副本已經指向 "update"(地址爲0x02),而value仍然仍是指向"init"(地址爲0x01) } private static void Main() { string value = "init"; //value變量存的是對"init"的引用(內存地址:0x01) //那麼傳遞的是value變量的副本(本質也就是對"init"的引用的副本,也是0x01) //這2個0x01都存儲在線程堆棧上,而且都指向"init"的託管堆內存塊 ChangeValue(value); //ChangeValue方法只是改變了value副本的指向,value自己的指向不受任何影響,因此結果可想而知輸出「init」 Console.WriteLine(value); }
public string TestProperty { get; set; } private static void ChangeValue(Program p) { //此時傳遞進來的p副本和p都指向new Program { TestProperty = "init" } //對p副本所指向內存塊的TestProperty屬性作賦值操做, //而不是對p副本作賦值操做,那麼也就是說p副本的指向並未改變 //實際上操做的內存塊依然是new Program { TestProperty = "init" } //因此p副本所指向內存塊的TestProperty屬性已經指向"update"了 p.TestProperty = "update"; //這裏只是改變了一個地址,就是p副本和p共同指向的內存塊裏面的TestProperty屬性的地址 } private static void Main() { Program p = new Program { TestProperty = "init" }; //p變量存的是對new Program { TestProperty = "init" }的引用(內存地址:0x001) //那麼傳遞的是p變量的副本(本質也就是對new Program { TestProperty = "init" }的引用的副本,也是0x001) //這2個0x001都存儲在線程堆棧上,而且都指向new Program { TestProperty = "init" }的託管堆內存塊 ChangeValue(p); //ChangeValue既沒有改變p的指向,也沒有改變p副本的指向 //p和p副本依然指向同一個內存塊 //因爲ChangeValue修改了該內存塊TestProperty屬性的值,因此輸出結果「update」 Console.WriteLine(p.TestProperty); }
public string TestProperty { get; set; } private static void ChangeValue(Program p) { //此時傳遞進來的p副本和p都指向new Program { TestProperty = "init" } p = new Program();//對p副本作賦值操做,這個時候,p副本已經指向另一個內存塊0x002了,而p依然仍是指向0x001 //對p副本所指向內存塊0x002的TestProperty屬性作賦值操做 //p副本所指向內存塊0x002的TestProperty屬性已經指向"update"了 p.TestProperty = "update"; //這裏改變了二個地址,就是p副本的地址和0x002裏面的的TestProperty屬性的地址 } private static void Main() { Program p = new Program { TestProperty = "init" }; //p變量存的是對new Program { TestProperty = "init" }的引用(內存地址:0x001) //那麼傳遞的是p變量的副本(本質也就是對new Program { TestProperty = "init" }的引用的副本,也是0x001) //這2個0x001都存儲在線程堆棧上,而且都指向new Program { TestProperty = "init" }的託管堆內存塊 ChangeValue(p); //ChangeValue並無改變p的指向,而是將p副本的指向由0x001改爲0x002,而後又將0x002的TestProperty改爲「update」了 //因此p指向的0x001內存塊沒有受到任何影響 //那麼p指向的0x001內存塊裏面的TestProperty值並未改變,因此輸出結果「init」 Console.WriteLine(p.TestProperty); }
經過以上代碼的講解和分析,我想咱們能夠窺探出其本質:
值傳遞的本質是傳「值」的副本,只不過值類型變量的「值」是實例自己,而引用類型變量的「值」是實例的引用,這麼一說就清晰不少了是吧!
在進行參數的引用傳遞時,當傳遞的參數爲值類型時,實際上傳遞的是該值類型實例的引用(不是實例副本)。
所以方法操做的是值類型實例的引用,在方法中對參數所作的任何更改都將反映在該變量中。
值類型經過引用傳遞時,不會對值類型進行裝箱。
private static void ChangeValue(ref int value) { value = 0; } private static void Main() { int value = 100; //實際上傳遞的是value實例的引用(相似於指針) ChangeValue(ref value); //因爲ChangeValue方法修改了value的引用所指向的內存塊裏面的值(實例自己),因此輸出是:0 Console.WriteLine(value); }
經過指針操做來更進一步的加深理解,注意看下圖的實例的指針地址和實例引用的指針地址是同樣的:
由此能夠看出這是同一個內存塊,因此ChangeValue改變就是實例所在的內存塊裏面的值。
在進行參數的引用傳遞時,當傳遞的參數爲引用類型時,實際上傳遞的是該引用類型實例的引用的引用(不是實例引用的副本)。
所以方法操做的是引用類型實例的引用的引用,在方法中對參數所作的任何更改都將反映在該變量中。
private static void ChangeValue(ref string value) { /* value的引用 * value的引用的引用 * value的引用指向對象實例,而[value的引用的引用]指向[value的引用] * 因此其本質就是修改value的引用所指向的託管堆裏的內存塊裏面的值 */ value = "update"; } private static void Main() { string value = "init"; //實際上傳遞的是value實例的引用的引用(相似於指針的指針) ChangeValue(ref value); //因爲ChangeValue方法修改了value的引用的引用所指向的內存塊裏面的值,因此輸出是:「update」 Console.WriteLine(value); }
因爲C#指針只能用來操做值類型,因此上面只有值類型對象的指針操做的屏幕演示gif動畫,就當錦上添花加深理解吧。
既然指針已通過來湊熱鬧了,我就多說一句,C#指針和引用的確相似,可是仍是有所區別,是兩個不一樣的概念。
講解到這裏的時候,咱們基本上在腦海中已經開始有一些結論了:
值類型的值傳遞: 實際傳的就是實例的 副本。
引用類型的值傳遞: 實際傳的就是實例的 引用的副本。
值類型的引用傳遞: 實際傳的就是實例的 引用。
引用類型的引用傳遞:實際傳的就是實例的 引用的引用。
在C#中,必須使用ref或者out參數修飾符顯式聲明的狀況下,參數纔會按照引用傳遞,不然默認就是按照值傳遞。
無論使用了什麼障眼法,或者代碼繞了什麼彎,只要抓住其本質,均可以把賦值操做和內存分佈及其地址指向說的清清楚楚。
若是方法簽名中的參數使用了ref、out修飾符,那麼就是顯式的告訴編譯器,該參數是按照引用傳遞。咱們不妨看看IL代碼,就一目瞭然了,以下圖所示:
經過上圖能夠看出,在調用帶有ref、out參數修飾符的方法時,傳遞參數時都會加上&運算符。
看看IL代碼,順便對比一下加了ref以後是什麼樣子:
private static void ChangeValue(int value) { value = 0; } private static void ChangeValue(ref int value) { value = 0; } private static void Main() { int a = 1; ChangeValue(a); ChangeValue(ref a); } //如下是IL代碼 .method private hidebysig static void Main () cil managed { // Method begins at RVA 0x2350 // Code size 16 (0x10) .maxstack 1 .entrypoint .locals init ( [0] int32 a ) IL_0000: ldc.i4.1 //把1推送到堆棧上。 IL_0001: stloc.0 //從堆棧的頂部彈出1並存到索引 0 處的局部變量列表中。 IL_0002: ldloc.0 //將索引 0 處的局部變量a加載到堆棧上。【這個地方走ChangeValue(a),直接就是進行堆棧數據拷貝。】 IL_0003: call void ConsoleTest.Program::ChangeValue(int32) IL_0008: ldloca.s a //將局部變量a的地址加載到堆棧上。 【這個地方走ChangeValue(ref a),則是把堆棧上的地址拿過來。因而可知,加上ref修飾符以後,傳的就是實例的引用地址了】 IL_000a: call void ConsoleTest.Program::ChangeValue(int32&) //注意看這地方多了&運算符 IL_000f: ret } // end of method Program::Main
傳遞到ref形參的實參必須先通過初始化,而後才能傳遞,out則不須要。
屬性或索引器不能做爲 out 或 ref 參數傳遞,由於它們的本質是方法,而不是變量。
ref或者out能夠實現方法重載,可是不能同時實現方法重載。例如void Test(ref int a)和void Test(out int a)同時出現並實現方法重載則會編譯報錯。
在泛型類型或方法定義中,類型參數是客戶端在實例化泛型類型的變量時指定的特定類型的佔位符。
客戶端代碼必須經過指定尖括號中的類型參數來聲明和實例化構造類型。
此特定類的類型參數能夠是編譯器識別的任何類型。
能夠建立任意數目的構造類型實例,每一個實例使用不一樣的類型參數。
尖括號中出現的每一個 T 都會在運行時替換爲相應的類型參數。經過這種替換方式,咱們可使用一個泛型定義並建立多個獨立的類型安全的有效對象。
約束 |
說明 |
---|---|
where T: struct |
類型參數必須是值類型。能夠指定除 Nullable 之外的任何值類型。 |
where T: class |
類型參數必須是引用類型;這一點也適用於任何類、接口、委託或數組類型。 |
where T:new() |
類型參數必須具備無參數的公共構造函數。當與其餘約束一塊兒使用時,new() 約束必須最後指定。 |
where T:<基類名> |
類型參數必須是指定的基類或派生自指定的基類。 |
where T:<接口名稱> |
類型參數必須是指定的接口或實現指定的接口。能夠指定多個接口約束。約束接口也能夠是泛型的。 |
where T:U |
爲 T 提供的類型參數必須是爲 U 提供的參數或派生自爲 U 提供的參數。 |
in/out關鍵字都可以在泛型接口和委託中使用。
用in修飾的泛型類型參數,表示該類型參數是逆變的。
用out修飾的泛型類型參數,表示該類型參數是協變的。
關於泛型的逆變和協變須要大篇幅來說解,且與本文的側重點有所偏離,故不做深刻探討。
本文先是對形參/實參、命名實參、可選參數、params數目可變參數等基礎知識概念做了一翻講解,順便走馬觀花了值類型與引用類型,而後以此爲鋪墊,開始按部就班、由淺入深的揭祕參數傳遞的神祕面紗。
參數傳遞纔是本文的重難點,比較繞,並且拗口,理解起來須要靜下心細細品味與琢磨。爲了更好的理解參數傳遞的本質,本文也引入了IL代碼和指針來加以輔助。
然而不甘寂寞的泛型類型參數也做爲特邀嘉賓到場助興,爲本文增色很多。
本篇文章主要是對C#.NET裏面與參數有關的知識進行盤點,可是卻牽連其餘知識點:
指針、內存分佈、地址指向、堆棧、託管堆、泛型類型參數等...
那麼問題來了:
一、引用類型加上ref、out修飾符的意義和應用場景?
二、指針和引用是什麼關係,或者兩者之間有何異同之處?
三、值類型的引用傳遞,其傳的地址是堆棧地址仍是託管堆地址?
四、值類型必定存儲在堆棧上面嗎?
五、C#的指針傳遞是怎樣的內幕?
六、....C#的語法點滴還有哪些醉美可賞?CLR又究竟還存在多少奇幻魅惑?
這些問題仍是先留給你們本身去思考一下吧,待博主後續博文繼續揭曉。
對於問題或者迷惑,咱們必須深刻探討,揪出其本質,才能在技術的道路上愈加沉澱和累積。
您的支持是我寫文的強勁動力,但願本文可以給您帶來幫助和益處,很是感謝您的閱讀!
看完文章,來一首好聽的音樂:《Five Hundred Miles》—Justin Timberlake