方法的結構
方法體內部代碼的執行
本地變量
類型推斷和var關鍵字
嵌套塊中的本地變量
本地常量
控制流
方法調用
返回值
返回語句和void方法
參數
形參
實參
值參數
引用參數
引用類型作為值參數和引用參數
輸出參數
參數數組
方法調用
用數組作為實參
參數類型總結
方法重載
命名參數
可選參數
棧幀
遞歸
方法
方法的結構
方法是一塊具有名稱的代碼。
可以使用方法的名稱從別的地方執行代碼,也可以把數據傳入方法並接收數據輸出。
方法是類的函數成員,主要有兩個部分,方法頭和方法體。
- 方法頭 指定方法的特征
- 方法是否返回數據,若返回,返回什么類型
- 方法的名稱
- 哪種類型的數據可以傳遞給方法或從方法返回,以及應如何處理這些數據
- 方法體 包含可執行代碼序列
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); } }