餘近日複習C#之基礎知識,故作一隨筆,也是對此前幾篇博客中所記錄的傳值參數相關內容之彙總,還望諸位加以批評指正。 該博客包括以下內容: 傳值參數 引用參數 輸出參數 數組參數 具名參數 可選參數 擴展方法(this參數) 傳值參數 C#語言規範中道:“聲明時不帶修飾符的形參是值形參。一個值形參對應於 ...
餘近日複習C#之基礎知識,故作一隨筆,也是對此前幾篇博客中所記錄的傳值參數相關內容之彙總,還望諸位加以批評指正。
該博客包括以下內容:
傳值參數
引用參數
輸出參數
數組參數
具名參數
可選參數
擴展方法(this參數)
傳值參數
C#語言規範中道:“聲明時不帶修飾符的形參是值形參。一個值形參對應於一個局部變數,只是它的初始值來自該方法調用所提供的相應實參。
當形參是值形參時,方法調用中的對應實參必須是表達式,並且它的類型可以隱式轉換為形參的類型。
允許方法將新值賦給值參數。這樣的賦值隻影響由該值形參表示的局部存儲位置,而不會影響在方法調用時由調用方給出的實參。”
註意:1、值參數創建變數之副本;2、對參數之改變永遠不影響變數之值
傳值參數→值類型
static void Main(string[] args) { int y=100; AddOne(y); System.Console.WriteLine(y); } static void AddOne(int x)//此處x便為傳值參數(或值參數) { x+=1; System.Console.WriteLine(x); } /* 運行結果: 101 100 */
根據結果顯示,第一行即為AddOne方法內列印出的語句,其結果為101,因為執行了x+=1;,列印出的亦為x之數值;而第二行列印出的結果是在調用方法完後y(變數)之值,我們發現,y(變數)之數值並未發生改變。
其原因便是,我們所修改的是y(變數)傳進來的一個副本,其並不影響變數之值。
傳值參數→引用類型,創建新的對象
static void Main(string[] args) { Student stu=new Student(){Name="Mark"}; System.Console.WriteLine("Hashcode={0},Name={1}.",stu.GetHashCode(),stu.Name); SomeMethod(stu); } static void SomeMethod(Student stu)//類類型便為典型之引用類型,在此為傳值參數 { stu =new Student(){Name="Mark"}; System.Console.WriteLine("Hashcode={0},Name={1}.",stu.GetHashCode(),stu.Name); } class Student { public string Name { get; set; } } /* 運行結果: Hashcode=1542680,Name=Mark. Hashcode=20054852,Name=Mark. */
根據運行結果顯示,第一行列印的便為方法之外變數的hashcode與名字,第二行列印的便為調用方法內列印的hashcode與名字。很顯然,雖然我們給它們設置的名字都是Mark,但它們的hashcode是完全不一樣的。因為第二個是我們在方法內創建的新對象(如果給其方法內的變數賦一個別的名字比如Elliot,那麼列印出的名字與hashcode都不一樣,這樣似乎更加的直觀)。
其原因在於,引用類型存儲的是實例之地址。方法外的變數stu存儲了對象之地址,傳入方法里的值也便是對象之地址,而傳值參數傳入方法內的是變數之副本(副本里也儲存的是對象之地址)。我們調用方法後,改變了副本里的值,也便就是改變了副本里之前存的地址,換成了一個新地址,那麼自然而然的指向了一個新對象。而對值參數(副本)的改變不會影響變數的值。故方法外之變數依舊指向原來的那個對象,而更改後的副本指向了一個新對象,它們互不影響。
註意:這種現象與實際工作中並無多大意義,我們用方法只是為了讀取值,不會新建個對象引用著。
傳值參數→引用類型,不創建對象,只操作對象
註意:對象還是那個對象,但對象內的值已經發生改變。
class Program { static void Main(string[] args) { Student outterstu=new Student(){Name="Mark"}; System.Console.WriteLine("Hashcode={0},Name={1}.",outterstu.GetHashCode(),outterstu.Name); UpdateObject(outterstu); } static void UpdateObject(Student stu) { stu.Name="Elliot";//未創建新對象,只是講對象的名字屬性的值改變了。 System.Console.WriteLine("Hashcode={0},Name={1}.",stu.GetHashCode(),stu.Name); } } class Student { public string Name { get; set; } } /* 運行結果: Hashcode=1542680,Name=Mark. Hashcode=1542680,Name=Elliot. */
根據運行結果顯示,未調用方法前名字為Mark,調用完之後則變成了Elliot。但是,它們的hashcode值卻都是完全一樣的,這說明它們指向的是同一個對象。而調用方法只是將對象內的值做了改動而已。(註意與引用參數情形之下加以區分,且為後話)
註意:這種現象很少見,對方法而言,其主要輸出還是靠返回值。這是該方法的副作用(side-effect)。
引用參數(引用形參)
C#語言規範中道:“用 ref 修飾符聲明的形參是引用形參。與值形參不同,引用形參並不創建新的存儲位置。相反,引用形參表示的存儲位置恰是在方法調用中作為實參給出的那個變數所表示的存儲位置。
當形參為引用形參時,方法調用中的對應實參必須由關鍵字 ref 並後接一個與形參類型相同的 variable-reference組成。變數在可以作為引用形參傳遞之前,必須先明確賦值。
在方法內部,引用形參始終被認為是明確賦值的。”
註意:1、引用參數並不創建變數之副本。2、使用ref修飾符顯示指出——該方法的副作用是為了修改實際參數之值。
引用參數→值類型
static void Main(string[] args) { int y=100; IWantSideEffect(ref y);//ref修飾符顯式指出副作用 System.Console.WriteLine(y); } static void IWantSideEffect(ref int x) { x+=1; System.Console.WriteLine(x); } /*運行結果: 101 101 */
與"傳值參數→值類型"一小程式對比可見,這一次方法外的y(變數)值發生了改變。這是因為方法的參數(x)與方法外之變數(y)所指記憶體之地址是相同的。我們在方法內改變參數所指記憶體地址中的值,則相當於變數所指記憶體地址中的值發生改變。那麼我們用變數訪問記憶體地址中存儲的值時,拿到的便是改變後的值。
引用參數→引用類型,創建新對象
class Program { static void Main(string[] args) { Student outterstu=new Student(){Name="Mark"}; System.Console.WriteLine("Hashcode={0},Name={1}.",outterstu.GetHashCode(),outterstu.Name); System.Console.WriteLine("----------------------------------------------"); IWantSideEffect(ref outterstu); System.Console.WriteLine("Hashcode={0},Name={1}.",outterstu.GetHashCode(),outterstu.Name); } static void IWantSideEffect(ref Student stu) { stu =new Student (){Name="Elliot"}; System.Console.WriteLine("Hashcode={0},Name={1}.",stu.GetHashCode(),stu.Name); } } class Student { public string Name { get; set; } } /* 運行結果: Hashcode=1542680,Name=Mark. ---------------------------------------------- Hashcode=20054852,Name=Elliot. Hashcode=20054852,Name=Elliot. */
根據運行結果顯示:第一行為未調用方法時的變數之Hashcode與Name。分割線下的第一行是調用方法時列印出的Hashcode與Name,第二行是在調用方法完後再次列印變數之Hashcode與Name。我們發現,調用方法後的參數"值"與變數"值"是完全一樣的。
其原因在於,引用類型變數存儲的是對象(或曰實例)在堆記憶體上之地址。那麼當變數傳入方法時,參數中所存也便為對象在對記憶體上之地址(相同的),在方法內部之邏輯,對參數進行了修改(創建了新的對象),由於這時傳入的不是變數之”副本“,而是真真切切的變數,所以變數中所儲存之值(地址)也隨即發生了改變。因為是創建了新的對象,所以無論是參數還是變數中所存之地址是新對象之在堆記憶體上之地址,所以它們指向了同一個新對象。
引用參數→引用類型,不創建新的對象,只修改對象值
class Program { static void Main(string[] args) { Student outterstu=new Student(){Name="Mark"}; System.Console.WriteLine("Hashcode={0},Name={1}.",outterstu.GetHashCode(),outterstu.Name); System.Console.WriteLine("------------------------------------------"); SomeSideEffect(ref outterstu); System.Console.WriteLine("Hashcode={0},Name={1}.",outterstu.GetHashCode(),outterstu.Name); } static void SomeSideEffect(ref Student stu) { stu.Name="Elliot"; System.Console.WriteLine("Hashcode={0},Name={1}.",stu.GetHashCode(),stu.Name); } } class Student { public string Name { get; set; } } /* 運行結果: Hashcode=1542680,Name=Mark. ------------------------------------------ Hashcode=1542680,Name=Elliot. Hashcode=1542680,Name=Elliot. */
根據運行結果顯示:第一行為未調用方法時,外部變數(outterstu)的Hashcode與Name;分割線下麵的第一行是方法內部列印出來的,註意此時的Hashcode與外部變數是相同的,但是Name已經改寫了;第二行則是在調用完方法後,再次列印出外部變數之Hashcode與Name。其Hashcode值並未改變,而其Name之值已經改寫了。
與上一段程式相比,這次並沒有創建新對象,只是改變原有對象之值。
到此為止,應註意其與"傳值參數→引用類型,不創建對象,只操作對象"之大不同。
class Program { static void Main(string[] args) { Student outterstu=new Student(){Name="Mark"}; System.Console.WriteLine("Hashcode={0},Name={1}.",outterstu.GetHashCode(),outterstu.Name); System.Console.WriteLine("------------------------------------------"); SomeSideEffect(outterstu); System.Console.WriteLine("Hashcode={0},Name={1}.",outterstu.GetHashCode(),outterstu.Name); } static void SomeSideEffect(Student stu) { stu.Name="Elliot"; System.Console.WriteLine("Hashcode={0},Name={1}.",stu.GetHashCode(),stu.Name); } } class Student { public string Name { get; set; } } /* 運行結果: Hashcode=1542680,Name=Mark. ------------------------------------------ Hashcode=1542680,Name=Elliot. Hashcode=1542680,Name=Elliot. */
上面這段代碼,我們將ref關鍵字刪除,運行後之結果並無改變,但其內涵確實極為大不相同的。
當為傳值情況時,變數傳進方法時,在記憶體中創建了其自身之副本,即變數(outterstu)與參數(stu)所指向之記憶體地址是不一樣的。而在此兩個不同之記憶體地址之中,卻都保存著相同之實例在堆記憶體中之地址。
而引用參數情況則為,變數(outterstu)與參數(stu)所指記憶體地址為同一記憶體地址,在這同一記憶體地址中保存的便為實例於堆記憶體中之地址。
輸出參數
C#語言定義文檔道:”用 out 修飾符聲明的形參是輸出形參。類似於引用形參,輸出形參不創建新的存儲位置。相反,輸出形參表示的存儲位置恰是在該方法調用中作為實參給出的那個變數所表示的存儲位置。
當形參為輸出形參時,方法調用中的相應實參必須由關鍵字 out 並後接一個與形參類型相同的 variable-reference組成。變數在可以作為輸出形參傳遞之前不一定需要明確賦值,但是在將變數作為輸出形參傳遞的調用之後,該變數被認為是明確賦值的。
在方法內部,與局部變數相同,輸出形參最初被認為是未賦值的,因而必須在使用它的值之前明確賦值。
在方法返回之前,該方法的每個輸出形參都必須明確賦值。“
通俗點說,比如,方法相當於加工數據之地方,而其返回值便為加工好之產品,但調用後只可產生依次。如若我們想在調用方法後得到多個值,即除了返回值之外還想拿到其他之值,則就要用到輸出參數。
註意:1、輸出參數並不創建變數之副本(變數與參數指向相同之記憶體地址);2、方法體內必須要有對輸出變數之賦值操作;3、使用out修飾符顯式指出——此方法之副作用為通過參數向外輸出值;4、變數初始值可有可空,終歸是要被覆蓋掉的。
在C#里,其本身就帶有Parse與TryParse兩種方法,Parse是解析,TryParse之返回值為布爾類型,判斷解析是否成功,若解析成功後我們想拿到解析後之值,而其返回值被布爾值所占用,此時便用到輸出參數之功能。如圖:
輸出參數→值類型
那麼,我們自己給double加一個"偽"TryParse方法。
class Program { static void Main(string[] args) { string arg="123"; string arg2="asd"; double x=0; double y=0; bool b1=DoubleParser.TryParse(arg,out x); bool b2=DoubleParser.TryParse(arg2,out y); if (b1==false) { System.Console.WriteLine("Input Error!"); } else System.Console.WriteLine(x); System.Console.WriteLine("---------------------"); if (b2==false) { System.Console.WriteLine("Input Error!"); System.Console.WriteLine(y); } } } class DoubleParser { public static bool TryParse(string input,out double result) { try { result =double.Parse (input); return true; } catch { result=0;//必須給result賦初值,在方法返回之前,該方法的每個輸出形參都必須明確賦值。 return false; } } } /* 運行結果: 123 --------------------- Input Error! 0 */
第一行是解析成功之x之值,分割線下麵為解析失敗之y值,其原有值亦被覆蓋成0。
輸出參數→引用類型
變數有無初始值都可,即便其有初始值,即引用著一個在堆記憶體中之對象。調用方法後,對參數進行賦值操作,因為變數與參數指向的是同一記憶體地址,則對參數值修改後(創建新對象),變數也引用上了新創建之對象。
舉例:
有一個"學生工廠",不斷的輸送人才出來,其內部有兩個邏輯,一是判斷成績是否符合要求,一是判斷學生是否有姓名。如果兩條其中有一項不滿足,則為false,人才就流失了;如果均滿足的話,就創造出了人才(即創建一個新對象)。
class Program { static void Main(string[] args) { Student stu=null; bool b=StudentFactory.Creat("Elliot",90,out stu); if (b==true) { System.Console.WriteLine("Name is {0},Score is {1}.",stu.Name,stu.Score); } } } class Student { public string Name { get; set; } public int Score { get; set; } } class StudentFactory { public static bool Creat(string stuName,int stuScore,out Student result) { result =null; if (string.IsNullOrEmpty(stuName))//判斷姓名是否為空 { return false; } if (stuScore<60)//判斷成績 { return false; } result=new Student (){Name=stuName,Score=stuScore};//創建一個新對象 return true; } } /* 運行結果: Name is Elliot,Score is 90. */
在此,稍作總結,對於ref修飾符來說是專為“改變”;對於out修飾符來說是專為"輸出"。
數組參數
數組參數聲明時會用params關鍵字修飾,且必須是形參列表中最後一個,且只有一個。
直接看例子,我們要計算一個數組內所有元素之和。
static void Main(string[] args) { int[] arrray={1,2,3,4,5}; int result=Sum(arrray); System.Console.WriteLine(result); //運行結果:15 } static int Sum(int[] intArray) { int sum=0; foreach (var i in intArray) { sum+=i; } return sum;
我們在調用這個方法之前,必須有一個已經聲明好的數組才可以放進去,但是,若在方法之參數列表內加一params修飾,就不必這麼麻煩了。
static void Main(string[] args) { int result=Sum(1,2,3,4,5);//直接輸入數據即可,無需創建數組 System.Console.WriteLine(result); //運行結果:15 } static int Sum(params int[] intArray)//加params修飾 { int sum=0; foreach (var i in intArray) { sum+=i; } return sum; }
還有其自有之Split方法。
static void Main(string[] args) { //還有其自帶之Split方法 string str ="Elliot;Mark,Ben."; string []strC=str.Split(';',',','.');//刪除人名間之符號 foreach (var name in strC) { System.Console.WriteLine(name); } /*運行結果: Elliot Mark Ben */ }
具名參數
參數之位置不再受約束,而且提高可讀性。
static void Main(string[] args) { printf(Age:19,Name:"Elliot");//沒有按照參數列表之順序存入數據 //運行結果:My name isElliot,I'm 19 years old. } static void printf(string Name,int Age) { System.Console.WriteLine("My name is{0},I'm {1} years old.",Name,Age); }
可選參數
參數具有預設值。
static void Main(string[] args) { printf();//不輸入數據,則列印出預設值 //運行結果:My name isElliot,I'm 19 years old. } static void printf(string Name="Elliot",int Age=19)//聲明時賦有預設值 { System.Console.WriteLine("My name is{0},I'm {1} years old.",Name,Age); }
擴展方法(this參數)
註意:1、方法必須是公有、靜態的,即被public static所修飾;2、必須是形參列表中之第一個,且被this關鍵字所修飾;3、必須由一個靜態類(一般名為SomeTypeExtension)來統一收納對SomeType之擴展方法。
比如,我們想對一個double類型之變數值進行四捨五入,然而其自身並無此方法,必須藉助Math中的Round方法。
static void Main(string[] args) { double x=3.14159; double y=Math.Round(x,4);//只能藉助於Math類之Round方法 System.Console.WriteLine(y); //運行結果:3.1416 }
所以,我們對double類型之變數加一個擴展方法:
class Program { static void Main(string[] args) { double x=3.14159; double y=x.Round(4);//這裡x就是input,其已自動傳入了 System.Console.WriteLine(y); //運行結果:3.1416 } } public static class DoubleExtension { public static double Round(this double input,int digital)//參數列表分別對應輸入值(this修飾),保留小數點後幾位 { double result=Math.Round(input,digital); return result; }
這樣,就方便甚多。
其擴展方法還與LINQ有很大之聯繫,只因餘現時所學尚淺,不敢妄議。待日後餘學識見漲再加以補充。
總結:
傳值參數:參數預設傳遞方式
引用參數:用於需要修改實際參數之值
輸出參數:用於除需返回值外還需其他輸出
數組參數:簡化方法調用
具名參數:提高可讀性,且參數之位置不受約束
可選參數:參數具有預設值
擴展方法(this參數):為目標數據類型“追加”方法