C# 圖解教程 第五章 方法
方法
方法的結構
方法是一塊具有名稱的程式碼。
可以使用方法的名稱從別的地方執行程式碼,也可以把資料傳入方法並接收資料輸出。
方法是類的函式成員,主要有兩個部分,方法頭和方法體。
- 方法頭 指定方法的特徵
- 方法是否返回資料,若返回,返回什麼型別
- 方法的名稱
- 哪種型別的資料可以傳遞給方法或從方法返回,以及應如何處理這些資料
- 方法體 包含可執行程式碼序列
int MyMethod(int par1,string par2) ↑ ↑ ↑ 返回 方法 引數 型別 名稱 列表
方法體內部程式碼的執行
方法體可包含以下專案
- 本地變數
- 控制流結構
- 方法呼叫
- 內嵌的塊
static void Main() { int myInt = 3; //本地變數 while(myInt > 0) //控制流結構 { --myInt; PrintMyMessage(); //方法呼叫 } }
本地變數
與類的欄位一樣,本地變數也儲存資料。欄位通常儲存和物件狀態有關的資料,而本地變數通常用於儲存本地的或臨時的計算資料。
- 本地變數的存在性和生存期僅限於建立它的塊及其內嵌的塊
- 它從宣告它的那一點開始存在
- 它在塊完成執行時結束存在
- 可以在方法體內任意位置宣告本地變數,但必須在使用前宣告
型別推斷和var關鍵字
觀察下面的程式碼,你會發現編譯器其實能從初始化語句的右邊推斷出來型別名。
- 第一個變數宣告中,編譯器能推斷出15是int型
- 第二個變數宣告中,右邊的物件建立表示式返回了一個MyExcellentClass型別物件
所以在這兩種情況中,在宣告開始的顯式的型別名是多餘的。
static void Main() { int myInt = 15; MyExcellentClass mec = new MyExcellentClass(); ... }
為了避免這種冗餘,可以在變數宣告開始的顯式型別名位置使用var關鍵字
static void Main() { var myInt = 15; var mec = new MyExcellentClass(); ... }
var不是特定的型別變數符號。它表示任何可以從初始化語句的右邊推斷出來的型別。
使用var有一些重要的條件
- 只能用於本地變數,不能用於欄位
- 只能在變數宣告中包含初始化時使用
- 一旦編譯器推斷出變數的型別,它就是固定且不能更改的
說明:var關鍵字不像JavaScript的var那樣可以引用不同的型別。它是從等號右邊推斷出的實際型別的速記。var關鍵字並不改變C#的強型別性質。
巢狀塊中的本地變數
方法體內部可以巢狀其他的塊
- 可以有任意數量的塊,並且它們既可以是順序的也可以更深層巢狀。
- 本地變數可以在內嵌塊內部宣告,並且和所有本地變數一樣,它的生存期和可見性僅限於宣告它們的塊及其內嵌塊
說明:在C和C++中,可以先宣告一個本地變數,然後在巢狀塊中宣告另一個同名本地變數。在內部範圍,內部變數覆蓋外部變數。然而,在C#中不管巢狀級別如何,都不能在第一個本地變數的有效範圍內宣告另一個同名本地變數。
本地常量
本地常量一旦被初始化就不能改變了,且必須宣告在塊的內部
常量的兩個重要特徵
- 常量在宣告時必須初始化
- 常量在聲明後不能改變
常量宣告語法
關鍵字 ↓ const Type Identifier = Value; ↑ 初始化值是必須的
控制流
控制流指的是程式從頭到尾的執行流程。
預設情況下,程式從上到下執行,控制流語句允許你改變執行順序。
- 選擇語句 選擇哪條語句或語句塊來執行
- if 判斷true則執行
- if…else true執行if,false執行else
- switch 在一組語句中執行某一條
- 迭代語句 在一個語句塊上迴圈或迭代
- for 迴圈-頂部判斷迴圈條件
- while 迴圈-頂部判斷迴圈條件
- do 迴圈-底判斷迴圈條件
- foreach 一組中每個成員執行一次
- 跳轉語句 在程式碼塊或方法體內部跳轉
- break 跳出當前迴圈
- continue 到當前迴圈底部
- goto 到一個指定的語句
- return 返回呼叫方法繼續執行
void SomeMethod() { int intVal = 3; if(intVal == 3) { Console.WriteLine("Value is 3."); } for(int i=0;i<5;i++) { Console.WriteLine("Value of i:{0}",i); } }
方法呼叫
可以從方法體的內部呼叫(call/invoke)其它方法
呼叫方法時要使用方法名並帶上引數列表
- 當前方法的執行在呼叫點被掛起
- 控制轉移到被呼叫方法的開始
- 被呼叫方法執行直到完成
- 控制回到發起呼叫的方法
返回值
方法可以向呼叫程式碼返回一個值
- 要返回值,方法必須在方法名前面宣告一個返回型別
- 如果程式碼不返回值,它必須宣告返回型別為void(空)
- 聲明瞭返回型別的,通過return語句返回值
返回型別 ↓ int GetHour() { DateTime dt = DateTime.Now; int hour = dt.Hour; //獲取當前小時 return hour; ↑ 返回語句 }
也可以返回使用者定義型別的物件
返回型別---MyClass ↓ MyClass method3() { MyClass mc = new MyClass(); ... return mc; }
返回語句和void方法
- 可以在任何時候使用下面形式的語句退出方法,不帶引數
return
- 這種形式的返回語句只能用於void宣告的方法
例
- 方法獲取當前日期和時間
- 如果小時小於12,那麼執行return語句,不在螢幕上輸出任何東西,直接把控制返回給呼叫方法
- 如果小時大於等於12,則跳過return語句,程式碼執行WriteLine語句,在螢幕上輸出資訊
class MyClass { ↓ void返回型別 void TimeUpdate() { DateTime dt = DateTime.Now; if(dt.Hour<12) return; Console.WriteLine("It's afternoon!"); } static void Main() { MyClass mc = new MyClass(); mc.TimeUpdate(); } }
引數
引數允許你在方法開始執行時把資料傳入方法,或是在一個方法體中返回多個返回值。
形參
形參是本地變數,它宣告在方法的引數列表中,而不是在方法體中
public void PrintSum(int x, float y) { ↑ ... 形參宣告 }
- 因為形參是變數,所以它們有型別和名稱,並能被寫入和讀取
- 和方法體中的其他本地變數不同,引數在方法體的外面定義並在方法開始前初始化(輸出引數除外)
- 引數列表中可以有任意數目的形參宣告,而且宣告必須用逗號隔開
形參在整個方法體內使用,在大部分地方就像其他本地變數一樣
實參
- 用於初始化形參表示式或變數稱作實參
- 實參位於方法呼叫的引數列表中
- 每一個實參必須與對應形參的型別相匹配,或是編譯器必須能夠把實參隱式轉換為那個型別
第二次呼叫,編譯器把int 5 和 someInt隱式轉換成了float
值引數
使用值引數,通過將實參的值複製到形參的方式把資料傳遞給方法。方法被呼叫時,系統做如下操作
- 在棧中為形參分配空間
- 將實參的值複製給形參
你應該記得第3章介紹了值型別,所謂值型別就是指型別本身包含其值。不要把值型別和這裡介紹的值引數混淆,它們是完全不同的兩個概念。值引數是把實參的值複製給形參。
class MyClass { public int Val=20; } class Program { static void MyMethod(MyClass f1,int f2) { f1.Val=f1.Val+5; f2=f2+5; Console.WriteLine("f1.val:{0},f2:{1}",f1.Val,f2); } static void Main() { MyClass a1=new MyClass(); int a2=10; MyMethod(a1,a2); Console.WriteLine("f1.Val:{0},f2:{1}",a1.Val,a2); } }
- 在方法被呼叫前,用作實參的變數a2已經在棧裡了
- 方法開始時,系統在棧中為形參分配空間,並從實參複製值
- 因為a1是引用型別,所以引用被複制,結果實參和形參都引用堆中同一物件
- 因為a2是值型別,所以值被複制,產生了一個獨立的資料項
- 方法的結尾,f2和物件f1的欄位都被加上5
- 方法執行後,形參從棧中彈出
- a2 值型別,它的值不受方法行為的影響
- a1 引用型別,它的值被方法的行為改變了
引用引數
- 使用引用引數,必須在方法的宣告和呼叫時都使用ref修飾符
- 實參必須是變數,在用作實參前必須被賦值。如果是引用型別變數,可以賦值為一個引用或null
包含ref修飾符 ↓ void MyMethod(ref int val) { ... } int y = 1; MyMethod(ref y); ↑ 包含ref修飾符 MyMethod(ref 3+5); //報錯 ↑ 必須使用變數
對於值引數,系統在棧上為形參分配記憶體,引用引數則不同
- 不會為形參在棧上分配記憶體
- 實際情況是:形參的引數名將作為實參變數的別名,指向相同的記憶體位置
由於形參名和實參名的行為就好像指向相同記憶體位置,所以在方法的執行過程中對形參做的任何改變在方法完成後依然有效
clas MyClass { public int Val = 20; } class Program { ref修飾符 ref修飾符 ↓ ↓ static void MyMethod(ref MyClass f1,ref int f2) { f1.Val=f1.Val+5; f2=f2+5; Console.WriteLine("f1.Val:{0},f2:{1}",f1.Val,f2); } static void Main() { MyClass a1=new MyClass(); int a2 =10; ref修飾符 ↓ ↓ MyMethod(ref a1,ref a2); Console.WriteLine("f1.Val:{0},f2:{1}",a1.Val,a2); } }
- 方法呼叫前,將要被用作實參的變數a1和a2已經在棧裡了
- 方法開始,形參名被設定為實參的別名。變數a1和f1引用相同的記憶體位置,a2和f2引用相同的記憶體位置
- 在方法結束位置,f2和f1的物件欄位都被加上了5
- 方法執行後,形參的名稱已經失效,但是值型別a2的值和引用型別a1所指向的物件的值都被方法內的行為改變了
引用型別作為值引數和引用引數
從前幾節看到,對於引用型別物件,不管是將其作為值引數傳遞還是引用引數傳遞,我們都可以在方法成員內部修改它的成員。不過我們並沒有在方法內部修改形參本身。本節來看看方法內修改引用型別形參會發生什麼。
- 將引用型別物件作為值引數傳遞:如果在方法內建立一個新物件並賦值給形參,將切斷形參與實參間的關聯,並且在方法呼叫結束後,新物件也將不復存在。
- 將引用型別物件作為引用引數傳遞:如果在方法內建立一個新物件並賦值給形參,在方法結束後該物件依然存在,並且是實參所引用的值
例:將引用型別物件作為值引數傳遞
class MyClass{public int Val=20;} class Program { static void RefAsParameter(MyClass f1) { f1.Val=50; Console.WriteLine("After meber assignment:{0}",f1.Val); f1=new MyClass(); Console.WriteLine("After new object creation:{0}",f1.Val); } static void Main() { MyClass a1=new MyClass(); Console.WriteLine("Before method call:{0}",a1.Val); RefAsParameter(a1); Console.WriteLine("After method call:{0}",a1.Val); } }
- 在方法開始時,實參和形參都指向堆中相同的物件
- 在為物件的成員賦值後,它們仍指向堆中相同的物件
- 當方法分配新的物件並賦值給形參時,(方法外部的)實參仍指向原始物件,而形參指向的是新物件
- 在方法呼叫後,實參指向原始物件,形參和新物件消失
例:將引用型別物件作為引用引數傳遞
class MyClass{public int Val=20;} class Program { static void RefAsParameter(ref MyClass f1) { f1.Val=50; Console.WriteLine("After meber assignment:{0}",f1.Val); f1=new MyClass(); Console.WriteLine("After new object creation:{0}",f1.Val); } static void Main(string[] args) { MyClass a1=new MyClass(); Console.WriteLine("Before method call:{0}",a1.Val); RefAsParameter(ref a1); Console.WriteLine("After method call:{0}",a1.Val); } }
引用引數的行為就像是將實參作為形參的別名。
- 在方法呼叫時,形參和實參都指向堆中相同的物件
- 對成員值的修改會同時影響到形參和實參
- 當方法建立新的物件並賦值給形參時,形參和實參的引用都指向該新物件
- 在方法結束後,實參指向在方法內建立的新物件
輸出引數
輸出引數用於從方法體內把資料傳出到呼叫程式碼,它們的行為與引用引數非常類似。
輸出引數有以下要求
- 必須在宣告和呼叫中都使用 out 修飾符
- 和引用引數類似,實參必須是變數
- 在方法內部,輸出引數在被讀取前必須賦值
- 方法返回前,方法內任何返回路徑都必須為所有輸出引數進行賦值
class MyClass { public int Val=20; } class Program { static void MyMethod(out MyClass f1,out int f2) { f1=new MyClass(); f1.Val=25; f2=15; } static void Main() { MyClass a1=null; int a2; MyMethod(out a1,out a2); } }
- 方法呼叫前,將作為實參的變數a1和a2已經在棧裡了
- 方法的開始,形參的名稱設定為實參的別名。你可以認為變數a1和f1指向相同的記憶體位置,a2和f2指向相同記憶體位置。a1、a2不在作用域內,所以不能在MyMethod中訪問
- 方法內部,對f1和f2的賦值是必需的,因為它們是輸出引數
- 方法執行後,形參名稱失效,但是引用型別的a1和值型別a2的值都被方法內的行為改變
引數陣列
上述的引數型別都必須嚴格地一個實參對應一個形參。引數陣列則不同,它允許零個或多個實參對應一個特殊的形參
- 在一個引數列表中只能有一個引數陣列
- 如果有,它必須是列表中最後一個
- 由引數陣列表示的所有引數都必須具有相同型別
宣告引數陣列必須做的事如下
- 在資料型別前使用 params 修飾符
- 在資料型別最後放置一組空的方括號
例:int型引數陣列宣告語法
void ListInts(params int[] inVals) {...}
- 陣列是一組整齊的同類型資料項
- 陣列使用一個數字索引進行訪問
- 陣列是引用型別,它的所有資料項都存在堆中
方法呼叫
可以使用兩種方式為引數陣列提供實參
- 逗號分隔的該資料型別元素列表
ListInts(10,20,30); - 該資料型別元素的一維陣列
int[] intArray={1,2,3};
ListInts(intArray);
在使用一個為引數陣列分離實參的呼叫時,編譯器做下面的事
- 接受實參列表,用它們在堆中建立並初始化一個數組
- 把陣列的引用儲存在棧中的形參裡
- 如果在對應的形引數組位置沒有實參,編譯器會建立一個有零個元素的陣列來使用
class MyClass { public void ListInts(params int[] inVals) { if((inVals!=null)&&(inVlas.Length!=0)) { for(int i=0;i<inVals.Length;i++) { inVals[i]=inVals[i]*10; Console.WriteLine("{0}",inVals[i]); } } } } class Program { static void Main() { int first=5,second=6,third=7; MyClass mc=new MyClass(); mc.ListInts(first,second,third); Console.WriteLine("{0},{1},{2}",first,second,third); } }
- 方法呼叫前,3個實參已經在棧裡
- 方法開始,3個實參被用於初始化堆中的陣列,並且陣列的引用被賦值給形參inVals
- 方法內部,程式碼首先檢查以確認陣列引用不是null,然後處理陣列,把每個元素乘以10並儲存回去
- 方法執行後,形參inVals失效
關於引數陣列,需記住重要的一點是當陣列在堆中被建立時,實參的值被複制到陣列中。它們就像值引數。
- 如果陣列引數是值型別,那麼值被複制,實參不受方法內部影響
- 如果陣列引數是引用型別,那麼引用被複制,實參引用的物件可以受到方法內部的影響
用陣列作為實參
直接把陣列變數作為實參傳遞,這種情況下,編譯器使用你的陣列而不是重新建立一個。
引數型別總結
方法過載
一個類中可以用一個以上的方法擁有相同名稱,這叫方法過載(method overload)。使用相同名稱的方法必須有一個和其他方法不同的簽名(signature)
- 方法的簽名由下列資訊組成,它們在方法宣告的方法頭中
- 方法的名稱
- 引數的數目
- 引數的資料型別和順序
- 引數修飾符
- 返回型別不是簽名的一部分
- 形參名稱也不是簽名的一部分
例:4個AddValue的過載
class A { long AddValues(int a,int b){return a+b;} long AddValues(int c int d,int e){return c+d+e;} long AddValues(float f,float g){return (long)(f+g);} long AddValues(long h,long m){return h+m;} }
例:錯誤的過載
class B { long AddValues(long a,long b){return a+b;} int AddValues(long c,long d){return c+d;} }
命名引數
至今我們所用到的引數都是位置引數,每個實參的位置都必須與相應的形參位置一一對應。
C#允許我們使用命名引數(named parameter),只要顯式指定引數名字,就可以以任意順序在方法呼叫中列出實參
- 方法的宣告沒有什麼不一樣。形參已經有名字
- 不過呼叫方法時,形參的名字後面跟著冒號和實際的引數值或表示式
例:使用命名引數的結構
class MyClass { public int Calc(int a,int b,int c) { return (a+b)*c; } static void Main() { MyClass mc=new MyClass(); int r0 = mc.Calc(4,3,2); int r1 = mc.Calc(4,b:3,c:2); int r2 = mc.Calc(4,c:2,b:3); int r3 = mc.Calc(c:2,b:3,a:4); int r4 = mc.Calc(c:2,b:1+2,a:3+1); Console.WriteLine("{0},{1},{2},{3},{4}",r0,r1,r2,r3,r4); } }
程式碼輸出
命名引數對於自描述程式來說很有用,我們可以在方法呼叫時顯示那個值賦給那個形參。
例:使用命名引數 增強程式易讀性
class MyClass { double GetCylinderVolume(double radius,double height) { return 3.1416*radius*radius*height; } static void Main(string[] args) { MyClass mc=new MyClass(); double volume; volume = mc.GetCylindreVolume(3.0,4.0); ... volume = mc.GetCylindreVolume(radius:3.0,height:4.0) } }
可選引數
可選引數就是我們可以在呼叫方法時包含這個引數,也可以省略。
為了表名某引數可選,你需要在方法宣告時為引數提供預設值
- 給形參b設定預設值3
- 因此,若呼叫方法時只有一個引數,方法會使用3作為第二個引數的初始值
class MyClass { public int Calc(int a ,int b=3) { return a+b; } static void Main() { MyClass mc=new MyClass(); int r0=mc.Calc(5,6); int r1=mc.Calc(5); Console.WriteLine("{0},{1}",r0,r1); } }
- 不是所有引數型別都可以作為可選引數
- 只要值型別的預設值在編譯時可以確定,就可以使用值型別作為可選引數
- 只有在預設值是null時,引用型別才可以作為可選引數
- 所有必填引數必須在可選引數前宣告。如果有params引數,必須在可選引數後宣告。
當有多個可選引數時,預設情況下只能省略後面幾個
- 你必須從可選引數列表的最後開始省略,一直到列表開頭
- 即你可以省略最後一個或n個可選引數,但不可以隨意選擇省略任意的可選引數
class MyClass { public int Calc(int a=2,int b=3,int c=4) { return (a+b)*c; } static void Main() { MyClass mc=new MyClass(); int r0=mc.Calc(5,6,7); int r1=mc.Calc(5,6); int r2=mc.Calc(5); int r3=mc.Calc(); Console.WriteLine("{0},{1},{2},{3}",r0,r1,r2,r3); } }
當有多個可選引數時,可以通過引數名字來選擇可選引數
class MyClass { double GetCylinderVolume(double radius=3.0,double height=4.0) { return 3.1416*radius*radius*height; } static void Main() { MyClass mc=new MyClass(); double volume; volume =mc.GetCylindervoume(3.0,4.0)://位置引數 Condole.WriteLine("Volume="+volume); volume =mc.GetCylindervoume(radius:2.0)://使用hieght預設引數 Condole.WriteLine("Volume="+volume); volume =mc.GetCylindervoume(height:2.0)://使用radius預設引數 Condole.WriteLine("Volume="+volume); volume =mc.GetCylindervoume()://使用預設值 Condole.WriteLine("Volume="+volume); } }
棧幀
至此,我們已經知道區域性變數和引數是位於棧上的,再來深入探討一下其組織。
呼叫方法時,記憶體從棧頂開始分配,儲存和方法關聯的一些資料項。這塊記憶體叫做方法的棧幀(stack frame)。
- 棧幀記憶體包含以下內容
- 返回地址,即方法退出時繼續執行的位置
- 這些引數分配的記憶體,也就是方法的值引數,或引數陣列
- 各種和方法呼叫相關的其他管理資料項
- 在方法呼叫時,整個棧幀會壓入棧
- 在方法退出時,整個棧幀從棧上彈出。彈出棧幀也叫棧展開(unwind)
例:下面程式碼聲明瞭3個方法。Main呼叫MethodA,MethodA呼叫MethodB,建立了3個棧幀。方法退出時,棧展開。
class Program { static void MethodA(int par1,int par2) { Console.WriteLine("Enter MethodA:{0},{1}",par1,par2); MethodB(11,18); Console.WriteLine("Exit MethodA"); } static void MethodB(int par1,int par2) { Console.WriteLine("Enter MethodB:{0},{1}",par1,par2); Console.WriteLine("Exit MethodB"); } static void Main() { Console.WriteLine("Enter Main"); MethodA(15,30); Console.WriteLine("Exit Main"); } }
呼叫方法時棧幀壓棧和棧展開的過程
遞迴
除了呼叫其他方法,方法也可以呼叫自身。這就是遞迴。
遞迴會產生很優雅的程式碼。
class Program { public void Count(int inVal) { if(inVal==0) { return; } else { Count(inVal-1); Console.WriteLine("{0}",inVal); } } static void Main() { Program pr=new Program(); pr.Count(3); } }
from: http://www.cnblogs.com/moonache/p/6063051.html