C#筆記 – 參數
可選參數與命名參數
-
可選參數
-
聲明可選參數:
-
void Dump(int x, int y = 20, int z = 30) -
x為必備參數;y和z為可選參數,20和30分別為y和z的默認值
-
-
動機:降低重載的方法數量
-
可選參數就是在定義方法/有參屬性的參數時,為它們賦默認值,如果調用參數時沒有傳值,C#編譯器就自動嵌入參數的默認值
- 委托的一部分參數也可以指定默認值
-
有默認值的參數需放在沒默認值的參數之後,參數數組(如有)之前
-
參數數組不能聲明為可選的
-
如果調用者沒有指定值,將使用空數組代替
-
-
默認值約束:
-
必須為C#認定的基元類型/枚舉/可空的引用類型,如:
-
數字/字符串字面量
-
null
-
const成員
-
枚舉成員
-
default(T)操作符
-
-
對於值類型可以使用與default等價的無參構造函數
-
-
如果參數使用了ref/out進行標識,就不能設置默認值
-
一旦為參數分配了默認值,編譯器會向該參數應用定制特性「Optional」,並持久性地保存到元數據中
-
此外,編譯器向參數應用DefaultParameterValue特性,並向該特性構造器傳遞開發者指定的參數默認值。同樣,持久性地保存到元數據中。
-
Method #2 (06000002) ------------------------------------------------------- MethodName: DefaultParam (06000002) Flags : [Private] [Static] [HideBySig] [ReuseSlot] (00000091) RVA : 0x000020c1 ImplFlags : [IL] [Managed] (00000000) CallCnvntn: [DEFAULT] ReturnType: Void 1 Arguments Argument #1: I4 1 Parameters (1) ParamToken : (08000002) Name : i flags: [Optional] [HasDefault] (00001010) Default: (I4) 10 -
當編譯器發現某個方法調用缺失了部分實參,就會嘗試從元數據中提取默認值,將值自動嵌入到調用中。
-
-
調用含有可選參數的方法時,編譯器默認我們傳遞的實參順序是與形參聲明順序是一致的
-
-
命名參數
-
在指定實參的值時,可以同時指定相應參數的名稱。編譯器將判斷參數的名稱是否正確,並將指定的值賦給這個參數。
-
命名實參可使編譯器幫我們判斷該實參具體是要傳給哪個形參,調用方法時的參數順序變得不再重要
-
void Main() { Show("SB", "HEHE"); Show(caption:"HEHE", text:"SB"); } void Show(string text, string caption) { Console.WriteLine($"{text}: {caption}"); }
-
-
編譯器會根據名稱進行識別
-
如果要對包含ref或out的參數指定名稱,需要將ref或out修飾符放在名稱之後,實參之前
-
int.TryParse("10", result: out number);
-
-
命名實參與位置實參的混合使用
-
未命名的實參稱為「位置實參」
- 位置實參總是指向方法聲明中相應的參數,我們不能跳過參數之後,再通過命名相應位置的實參來指定
-
所有命名實參必須放在「位置實參」之後。
- 除非所有實參的位置與形參聲明位置保持一致,但這樣也沒必要用命名實參了
-
void Main() { Show("SB", name: "GO", caption:"HEHE"); //Valid Show(name: "GO", "SB", caption:"HEHE"); //InValid Show(text: "GO", "SB", name:"HEHE"); //Valid } void Show(string text, string caption, string name) { Console.WriteLine($"{name}({text}): {caption}"); }
-
-
實參求值順序
- 按編寫順序求值,即使這個順序不同於參數的聲明順序
-
命名實參與可選參數混合使用
-
void Main() { //省略了encoding參數,直接使用timestamp的命名實參 AppendTimestamp("utf8.txt", "Message in the future", timestamp: new DateTime(2030, 1, 1)); } void AppendTimestamp(string filename, string message, Encoding encoding = null, DateTime? timestamp = null) { } -
不易變性和對象初始化
-
命名實參和可選參數還可以用在不易變的對象初始化上
-
void Main() { Message msg = new Message( from: "skeet@pobox.com", to: "csharp-in-depth-readers@everywhere.com", body: "I hope you like the third edition", subject: "A quick message" ); } public class Message { string from; string to; string body; string subject; byte[] attachment; public Message() { } public Message(string from, string to, string body, string subject = null, byte[] attachment = null) { this.from = from; this.to = to; this.body = body; this.subject = subject; this.attachment = attachment; } }
-
-
重載決策
-
可選參數會增加適用方法(如果方法參數數量多於指定的實參數量)
-
命名實參會減少適用方法的數量(排除那些沒有適當參數名稱的方法)
-
為了檢查是否存在特定的適用方法,編譯器會使用位置參數的順序構建一個傳入實參的列表
- 然後對命名實參和剩餘的參數進行匹配。如果沒有指定某個必備參數,或某個命名實參不能與剩餘的參數相匹配,那這個方法就是不適用的
-
如果兩個方法都是適用的,其中一個方法的所有實參都顯式指定,而另一個方法使用了某個可選參數的默認值,則未使用默認值的方法勝出
-
但這不適用於僅比較所使用的默認值數量的情況——它是嚴格按照「是否使用了默認值」來劃分的
-
static void Foo(int x = 10) { } static void Foo(int x = 10, int y = 20) { } Foo(); //Error:兩個方法的參數都是可選的,編譯器無法判斷到底要調用哪一個方法 Foo(1); //Foo(int x = 10):使用這個方法不涉及默認值 Foo(y: 2); //Foo(int x = 10, int y = 20):命名了y實參,只有第二個方法有y的形參 Foo(1, 2); //Foo(int x = 10, int y = 20):傳了兩個參數,只有第二個方法有2個參數
-
-
如果某些方法聲明在基類中,而派生類中包含適用方法,那派生類中的方法勝出
-
命名實參可以代替強制轉換
-
void Method(int x, object y) { } void Method(object x, int y) { } Method(10, 10); //有歧義:兩個方法都適用,哪一個都不比另一個更優 //消除歧義方法 Method(10, (object)10); //強制轉換 Method(x: 10, y: 10); //命名實參
-
-
引用方式的參數傳遞(out/ref)
-
static void GetVal(out int v) { v = 20; } static void AddVal(ref int v) { v += 10; } -
CLR不區分out和ref,兩者生成的IL代碼是一樣的

-
-
元數據幾乎完全一致,只有一個標記不同,用於記錄聲明方法時指定的是out還是ref
.method private hidebysig static void GetVal([out] int32& v) cil managed .method private hidebysig static void AddVal(int32& v) cil managed
-
-
C#編譯器把兩者區分開,這個區別決定了由哪個方法負責初始化引用的對象
-
如果參數用out標記,則不需要調用者在調用方法前初始化好對象
-
被調用的方法不能讀取參數的值
-
在返回前必須向這個值寫入
-
-
如果參數用ref標記,則需要在調用方法前把參數初始化完成
- 被調用的方法可以讀/寫值
-
-
值類型的out/ref
-
class Program { static void Main(string[] args) { //值類型 int x; GetVal(out x); Console.WriteLine(x); int y = 5; AddVal(ref y); Console.WriteLine(y); } static void GetVal(out int v) { v = 20; } static void AddVal(ref int v) { v += 10; } } -
「out」在該代碼中:
-
x在Main的棧幀中聲明
-
x的地址傳給GetVal
-
GetVal的v是一個指針,指向Main棧幀中的int32值
-
GetVal內部把v指向的值修改為20
-
返回(x = 20)
- 為大的值類型使用out可提升代碼執行效率,因為它避免了進行方法調用時複製值類型實例的字段
-
-
「ref」在該代碼中:
-
y在Main的棧幀中聲明,並初始化為5
-
y的地址傳給AddVal
-
AddVal的v是一個指針,指向Main棧幀中的int32值
-
由於v指向的值必然是已經初始化過的,因此AddVal內部可以隨意修改v指向的值(+=10)
-
返回(y=15)
-
-
從IL和CLR看來,out和ref是一樣的:導致傳遞指向實例的一個指針
-
編譯器則會辨別兩者,並根據區別,用不同的標準去驗證代碼的正確性
-
CLR雖然允許使用out和ref來對一般參數傳遞類型的方法進行重載
- 但由於CLR認為out和ref是一樣的,因此兩個重載方法之間不能只有out和ref的區別
-
-
-
引用類型的out/ref
-
out/ref對於值類型而言,就是允許方法操縱單一的值類型實例
- 調用者必須為實例分配內存,被調用者則操縱該內存中的內容
-
out/ref對於引用類型而言,則是調用代碼為一個指針(指向一個引用類型對象)分配內存,被調用者則操縱這個指針
- 因此,只有方法「返回」對「方法知道的一個對象」的引用時,為引用類型使用out/ref才有意義(也就是說,引用對象是在方法內完成對象實例化的,才有需要使用out/ref)
-
class Program { static void Main(string[] args) { //引用類型 //Object tc; //Type must be exactly same TestClass tc; StartProcessinFile(out tc); if(tc != null) { ContinueProcessingFiles(ref tc); tc.OutputValue(); } } static void StartProcessinFile(out TestClass tester) { tester = new TestClass(); } static void ContinueProcessingFiles(ref TestClass tester) { tester.Close(); tester = null; tester = new TestClass(100); } } -
通過方法構造對象,處理後再返回一個新的對象
-
-
引用方式的參數傳遞的限制
- 參數類型與方法簽名中聲明的類型必須嚴格相同
可變數量參數
-
static int Add(params int[] values) { int sum = 0; for (int i = 0; i < values.Length; i++) { sum += values[i]; } return sum; } -
params T[] varName
-
必須應用於方法簽名的最後一個參數
-
params關鍵字告訴編譯器向參數應用定制特性「System.ParamArrayAttribute」
-
.method private hidebysig static int32 Add(int32[] values) cil managed { .param [1] .custom instance void [System.Runtime]System.ParamArrayAttribute::.ctor() = ( 01 00 00 00 ) // 程式碼大小 35 (0x23) //... } // end of method Program::Add
-
-
C#編譯器檢測到方法調用時,會先檢查所有具有指定名稱,同時參數沒有應用ParamArrayAttribute特性的方法
-
如果找不到,再找應用了ParamArrayAttribute特性的方法
- 找到後,編譯器會構造一個數組,填補元素,再生代碼調用方法
-
該參數只能標識一維數組
-
可為其傳null值
-
-
調用可變數量參數方法對性能有所影響
-
數組對象必須在堆上分配
-
數組元素必須初始化
-
數組內存需要GC
-
參數與返回類型的設置
-
參數最好是設置「弱類型」,如:
-
接口(優先考慮)
- 實現了接口就可以當參數傳
-
基類
-
-
返回值最好是設置「強類型」
-
派生類
-
接收返回值時,就可以用類型本身或其基類接收
-
返回基類,就只能用基類接收
-
-
-
考慮協變性和逆變性
-
更靈活、泛用
參考書目
- 《CLR via C#》(第4版) Jeffrey Richter
- 《深入理解C#》(第3版) Jon Skeet