可選參數與命名參數
可選參數
聲明可選參數:
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