方法參數
根據預設,C# 中的引數會以藉傳值方式傳遞至函式。 這表示變數的複本會傳遞至方法。 針對值 (struct
) 型別,會將值的複本傳遞至方法。 針對參考 (class
) 型別,會將參考的複本傳遞至方法。 參數修飾元可讓您以藉傳址方式傳遞引數。 下列概念可協助您了解這些區別,以及如何使用參數修飾元:
- 「依值傳遞」表示將變數複本傳遞至方法。
- 「依參考傳遞」表示將變數存取傳遞至方法。
- 「參考型別」的變數包含其資料的參考。
- 「實值型別」的變數直接包含其值。
因為結構是實值型別,所以當您以藉傳值方式傳遞結構給方法時,方法會收到結構引數的複本並在其上運作。 方法無法存取呼叫方法中的原始 struct,因此無法以任何方式變更它。 方法只能變更複本。
類別執行個體是參考型別,不是實值型別。 當以傳值方式傳遞參考型別給方法時,方法會收到 class 執行個體的參考複本。 這兩個變數都參考相同的物件。 參數是參考的複本。 被呼叫的方法無法重新指派呼叫方法中的執行個體。 不過,被呼叫的方法可以使用參考的複本來存取執行個體成員。 如果被呼叫的方法變更執行個體成員,呼叫方法也會看到那些變更,因為它參考相同的執行個體。
下例的輸出會說明其間的差異。 因為方法使用參數中的位址來尋找指定的類別執行個體欄位,所以方法 willIChange
會變更 ClassTaker
欄位的值。 因為引數值是結構本身的複本,不是其位址的複本,所以呼叫方法中之結構的 StructTaker
欄位不會從呼叫 willIChange
變更。 StructTaker
變更複本,而複本在完成對 StructTaker
的呼叫時遺失。
class TheClass
{
public string? willIChange;
}
struct TheStruct
{
public string willIChange;
}
class TestClassAndStruct
{
static void ClassTaker(TheClass c)
{
c.willIChange = "Changed";
}
static void StructTaker(TheStruct s)
{
s.willIChange = "Changed";
}
public static void Main()
{
TheClass testClass = new TheClass();
TheStruct testStruct = new TheStruct();
testClass.willIChange = "Not Changed";
testStruct.willIChange = "Not Changed";
ClassTaker(testClass);
StructTaker(testStruct);
Console.WriteLine("Class field = {0}", testClass.willIChange);
Console.WriteLine("Struct field = {0}", testStruct.willIChange);
}
}
/* Output:
Class field = Changed
Struct field = Not Changed
*/
參數型別和引數模式的組合
引數的傳遞方式,及引數為參考型別或實值型別,均控制呼叫者可以看到對引數進行了哪些修改:
- 當您「依值」傳遞「實值」型別時:
- 如果方法指派參數以參考不同的物件,則呼叫者「看不到」這些變更。
- 如果方法修改參數所參考的物件狀態,則呼叫者「看不到」這些變更。
- 當您「依值」傳遞「參考」型別時:
- 如果方法指派參數以參考不同的物件,則呼叫者「看不到」這些變更。
- 如果方法修改參數所參考的物件狀態,則呼叫者「看得到」這些變更。
- 當您「依參考」傳遞「實值」型別時:
- 如果方法指派 參數來參考使用
ref =
的不同物件,則呼叫端看不到這些變更。 - 如果方法修改參數所參考的物件狀態,則呼叫者「看得到」這些變更。
- 如果方法指派 參數來參考使用
- 當您「依參考」傳遞「參考」型別時:
- 如果方法指派參數以參考不同的物件,則呼叫者「看得到」這些變更。
- 如果方法修改參數所參考的物件狀態,則呼叫者「看得到」這些變更。
以傳參考方式傳遞參考型別,可讓已呼叫方法取代參考參數在呼叫者中所參考的物件。 物件的儲存位置會以參考參數值的方式,傳遞至方法。 如果您變更參數儲存位置中的值 (指向新的物件),則也會變更呼叫端所參考的儲存位置。 下列範例會以 ref
參數,傳遞參考類型的執行個體。
class Product
{
public Product(string name, int newID)
{
ItemName = name;
ItemID = newID;
}
public string ItemName { get; set; }
public int ItemID { get; set; }
}
private static void ChangeByReference(ref Product itemRef)
{
// Change the address that is stored in the itemRef parameter.
itemRef = new Product("Stapler", 12345);
}
private static void ModifyProductsByReference()
{
// Declare an instance of Product and display its initial values.
Product item = new Product("Fasteners", 54321);
System.Console.WriteLine("Original values in Main. Name: {0}, ID: {1}\n",
item.ItemName, item.ItemID);
// Pass the product instance to ChangeByReference.
ChangeByReference(ref item);
System.Console.WriteLine("Calling method. Name: {0}, ID: {1}\n",
item.ItemName, item.ItemID);
}
// This method displays the following output:
// Original values in Main. Name: Fasteners, ID: 54321
// Calling method. Name: Stapler, ID: 12345
參考和值的安全內容
方法可以將參數的值儲存在欄位中。 當以藉傳值方式傳遞時,通常都很安全。 值會經過複製,而且參考型別儲存在欄位中,因此可以取得。 若要安全地依參考傳遞參數,編譯器則需要定義何時可以安全地將參考指派給新的變數。 針對每個運算式,編譯器會定義將存取權限制於運算式或變數的「安全內容」。 編譯器使用兩個範圍:safe-context 與 ref-safe-context。
- safe-context 會定義可安全存取任何運算式的範圍。
- ref-safe-context 會定義可安全存取或修改任何運算式「參考」的範圍。
您可以非正式地將這些範圍視為一種機制,可確保您的程式碼永遠不會存取或修改不再有效的參考。 只要參考的是有效物件或結構,該參考就有效。 safe-context 會定義變數何時可以指派或重新指派。 ref-safe-context 會定義變數何時可以「指派參考」 或「重新指派參考」。 指派會將變數指派給新值;「指派參考」會指派變數,以「參考」不同的儲存位置。
傳址參數
您可以將下列其中一個修飾元套用至參數宣告,以藉傳址方式傳遞引數,而不是以藉傳值方式傳遞:
ref
:在呼叫方法之前,必須先初始化引數。 方法可以將新值指派給參數,但這並非必要。out
:發出呼叫的方法在呼叫方法之前不需要先初始化引數。 方法必須將值指派給參數。ref readonly
:在呼叫方法之前,必須先初始化引數。 方法無法將新的值指派給參數。in
:在呼叫方法之前,必須先初始化引數。 方法無法將新的值指派給參數。 編譯器可能會建立暫存變數,以將引數的複本保存至in
參數。
類別的成員的簽章,不能只有在 ref
、ref readonly
、in
或 out
部分不同。 如果型別的兩個成員之間,唯一的區別在於其中一個有 ref
參數,而另一個有 out
、ref readonly
或 in
參數,則會發生編譯器錯誤。 但如果一種方法有 ref
、ref readonly
、in
或 out
參數,而另一種方法有以藉傳值方式傳遞的參數,則可以對方法進行多載,如下列範例所示。 在其他需要簽章比對的情況 (例如隱藏或覆寫) 中,in
、ref
、ref readonly
與 out
是簽章的一部分,但彼此不相符。
當參數具有上述其中一個修飾元時,對應的引數可以有相容的修飾元:
- 參數的
ref
引數必須包含ref
修飾元。 - 參數的
out
引數必須包含out
修飾元。 - 參數的
in
引數可以選擇性地包含in
修飾元。 若ref
修飾元改為使用在引數上,編譯器會發出警告。 - 參數的
ref readonly
引數應該包含in
或ref
修飾元,但不能同時包含兩者。 若未包含任一修飾元,編譯器會發出警告。
當您使用這些修飾元時,其會描述如何使用引數:
ref
表示方法可以讀取或寫入引數的值。out
表示方法會設定引數的值。ref readonly
表示方法會讀取引數的值,但無法寫入引數的值。 引數「應該」以藉傳址方式傳遞。in
表示方法會讀取引數的值,但無法寫入引數的值。 引數會以藉傳址方式傳遞,或透過暫存變數傳遞。
您無法在下列方法類型中使用先前的參數修飾元:
- 使用 async 修飾詞定義的 async 方法。
- iterator 方法,其包括 yield return 或
yield break
陳述式。
擴充方法也具有使用這些引數關鍵字的限制:
- 擴充方法的第一個引數上不能使用
out
關鍵字。 - 當引數不是
struct
,或不限制為結構的泛型型別時,擴充方法的第一個引數上不能使用ref
關鍵字。 - 除非第一個引數是
struct
,否則無法使用ref readonly
與in
關鍵字。 - 任何泛型型別上都不能使用
ref readonly
與in
關鍵字,即使限制為結構時也一樣。
屬性不是變數。 它們是方法。 屬性不能是 ref
參數的引數。
ref
參數修飾元
若要使用 ref
參數,方法定義和呼叫方法都必須明確使用 ref
關鍵字,如下列範例所示。 (除了在進行 COM 呼叫時呼叫方法可以省略 ref
。)
void Method(ref int refArgument)
{
refArgument = refArgument + 44;
}
int number = 1;
Method(ref number);
Console.WriteLine(number);
// Output: 45
傳遞至 ref
參數的引數,在傳遞之前必須先初始化。
out
參數修飾元
若要使用 out
參數,方法定義和呼叫方法都必須明確地使用 out
關鍵字。 例如:
int initializeInMethod;
OutArgExample(out initializeInMethod);
Console.WriteLine(initializeInMethod); // value is now 44
void OutArgExample(out int number)
{
number = 44;
}
當作 out
引數傳遞的變數不必先初始化,就能在方法呼叫中傳遞。 不過,需要先指派值給被呼叫的方法,方法才能傳回。
解構方法 (部分機器翻譯) 會使用 out
修飾元宣告其參數,以傳回多個值。 其他方法可以針對多個傳回值傳回值元組。
您必須先在其他陳述式中宣告變數,再將它以 out
引數形式傳遞。 您也可以在方法呼叫的引數清單中宣告 out
變數,而不在其他變數宣告中進行。 out
變數宣告會產生更精簡、更容易閱讀的程式碼,也可避免不小心在方法呼叫前先將值指派給變數。 下列範例會在對 Int32.TryParse 方法的呼叫中定義 number
變數。
string numberAsString = "1640";
if (Int32.TryParse(numberAsString, out int number))
Console.WriteLine($"Converted '{numberAsString}' to {number}");
else
Console.WriteLine($"Unable to convert '{numberAsString}'");
// The example displays the following output:
// Converted '1640' to 1640
您可以宣告隱含型別區域變數。
ref readonly
修飾元
ref readonly
修飾元必須存在於方法宣告中。 呼叫位置的修飾元是選擇性的。 可以使用 in
或 ref
修飾元。 ref readonly
修飾元在呼叫位置無效。 您在呼叫網站使用的修飾元可協助描述引數的特性。 只有當引數是變數且可寫入時,您才能使用 ref
。 只有當引數是變數時,您才能使用 in
。 它可能是可寫入或唯讀的。 若引數不是變數,但為運算式,則無法新增任一修飾元。 下列範例顯示這些情況。 下列方法會使用 ref readonly
修飾元來指出基於效能考慮,應該以藉傳址方式傳遞大型結構:
public static void ForceByRef(ref readonly OptionStruct thing)
{
// elided
}
您可以使用 ref
或 in
修飾元來呼叫方法。 若省略修飾元,編譯器會發出警告。 當引數是運算式而非變數時,您無法新增 in
或 ref
修飾元,因此您應該隱藏警告:
ForceByRef(in options);
ForceByRef(ref options);
ForceByRef(options); // Warning! variable should be passed with `ref` or `in`
ForceByRef(new OptionStruct()); // Warning, but an expression, so no variable to reference
若變數是 readonly
變數,您必須使用 in
修飾元。 若改為使用 ref
修飾元,編譯器會發出錯誤。
ref readonly
修飾元指出方法預期引數為變數,而不是非變數的運算式。 非變數的運算式範例包括常數、方法傳回值與屬性。 如果引數不是變數,編譯器會發出警告。
in
參數修飾元
方法宣告中需要 in
修飾元,但在呼叫位置則不需要。
int readonlyArgument = 44;
InArgExample(readonlyArgument);
Console.WriteLine(readonlyArgument); // value is still 44
void InArgExample(in int number)
{
// Uncomment the following line to see error CS8331
//number = 19;
}
in
修飾元可讓編譯器建立引數的暫存變數,並將唯讀參考傳遞至該引數。 編譯器一律會在引數必須轉換時、有來自引數型別的隱含轉換時,或引數值不是變數時,建立暫存變數。 例如,當引數是常值,或從屬性存取子傳回的值時。 當您的 API 要求以藉傳址方式傳遞引數時,請選擇 ref readonly
修飾元,而不是 in
修飾元。
使用 in
參數定義的方法可能會提升效能最佳化。 某些 struct
型別引數可能大小很大,在緊密迴圈或關鍵程式碼路徑中呼叫方法時,複製那些結構的成本便很重要。 方法會宣告 in
參數,以指定能以藉傳址方式安全地傳遞引數,因為被呼叫的方法不會修改該引數的狀態。 以傳址方式傳遞那些引數,可避免 (可能) 相當耗費資源的複製。 在呼叫位置明確地新增 in
修飾詞,可以確保引數是以傳址方式傳遞,而非以傳值方式傳遞。 明確地使用 in
有下列兩個效果:
- 在呼叫位置指定
in
會強制編譯器選取定義了符合之in
參數的方法。 否則,當兩個方法的差異只在於in
是否存在時,傳值方式的多載是較佳的相符項目。 - 透過指定
in
,可宣告以藉傳址方式傳遞引數的意圖。 搭配in
使用的引數必須代表可以直接參考的位置。out
與ref
引數的相同一般規則同樣適用:您無法使用常數、一般屬性或其他會產生值的運算式。 否則,在呼叫位置省略in
會通知編譯器,可以建立暫存變數,藉唯讀傳址方式傳遞給方法。 編譯器會建立暫存變數,以克服in
引數的幾項限制:- 暫存變數允許編譯時期常數作為
in
參數。 - 暫存變數允許屬性或其他運算式作為
in
參數。 - 暫存變數允許隱含從引數型別轉換成參數型別的引數。
- 暫存變數允許編譯時期常數作為
在所有先前的情況下,編譯器會建立暫存變數,儲存常數、屬性或其他運算式的值。
下列程式碼說明這些規則:
static void Method(in int argument)
{
// implementation removed
}
Method(5); // OK, temporary variable created.
Method(5L); // CS1503: no implicit conversion from long to int
short s = 0;
Method(s); // OK, temporary int created with the value 0
Method(in s); // CS1503: cannot convert from in short to in int
int i = 42;
Method(i); // passed by readonly reference
Method(in i); // passed by readonly reference, explicitly using `in`
現在,假設可以使用另一個使用傳值引數的方法。 結果的變更如下列程式碼所示:
static void Method(int argument)
{
// implementation removed
}
static void Method(in int argument)
{
// implementation removed
}
Method(5); // Calls overload passed by value
Method(5L); // CS1503: no implicit conversion from long to int
short s = 0;
Method(s); // Calls overload passed by value.
Method(in s); // CS1503: cannot convert from in short to in int
int i = 42;
Method(i); // Calls overload passed by value
Method(in i); // passed by readonly reference, explicitly using `in`
以傳址方式傳遞引數的唯一方法呼叫,是最終的方法呼叫。
注意
為簡單起見,上述程式碼使用 int
作為引數型別。 因為 int
在大多數新型電腦中,不會比參考大,所以將單一 int
以唯讀傳址方式傳遞並沒有好處。
params
修飾元
在方法宣告中,params
關鍵字後面不允許任何其他參數,而且方法宣告中只允許一個 params
關鍵字。
params
參數的宣告類型必須是集合類型。 辨識的集合類型包括:
- 單一維度陣列類型
T[]
,在此案例中,元素類型為T
。 - 範圍類型:
System.Span<T>
System.ReadOnlySpan<T>
在這裡,元素類型為T
。
- 具有可存取之建立方法的類型,具有對應的元素類型。 會使用於集合運算式所用的相同屬性來識別建立方法。
- 實作 System.Collections.Generic.IEnumerable<T> 的結構或類別類型,其中:
- 此類型擁有的建構函式,可在不使用引數的情況下叫用,而且建構函式至少會像宣告成員一樣是可存取的。
- 此類型具有執行個體 (而非擴充功能) 方法
Add
,其中:- 可以使用單一值引數來叫用此方法。
- 如果是泛型方法,則可以從引數推斷型別引數。
- 此方法至少會像宣告成員一樣是可存取的。 在這裡,元素類型是類型的反覆項目類型。
- 介面類型:
在 C# 13 之前,參數必須是單一維度陣列。
當您使用 params
參數呼叫方法時,可以傳入:
- 陣列元素型別引數的逗號分隔清單。
- 指定之類型的引數集合。
- 無引數。 如果不傳送任何引數,
params
清單的長度為零。
下例示範將引數傳送至 params
參數的各種方式。
public static void ParamsModifierExample(params int[] list)
{
for (int i = 0; i < list.Length; i++)
{
System.Console.Write(list[i] + " ");
}
System.Console.WriteLine();
}
public static void ParamsModifierObjectExample(params object[] list)
{
for (int i = 0; i < list.Length; i++)
{
System.Console.Write(list[i] + " ");
}
System.Console.WriteLine();
}
public static void TryParamsCalls()
{
// You can send a comma-separated list of arguments of the
// specified type.
ParamsModifierExample(1, 2, 3, 4);
ParamsModifierObjectExample(1, 'a', "test");
// A params parameter accepts zero or more arguments.
// The following calling statement displays only a blank line.
ParamsModifierObjectExample();
// An array argument can be passed, as long as the array
// type matches the parameter type of the method being called.
int[] myIntArray = { 5, 6, 7, 8, 9 };
ParamsModifierExample(myIntArray);
object[] myObjArray = { 2, 'b', "test", "again" };
ParamsModifierObjectExample(myObjArray);
// The following call causes a compiler error because the object
// array cannot be converted into an integer array.
//ParamsModifierExample(myObjArray);
// The following call does not cause an error, but the entire
// integer array becomes the first element of the params array.
ParamsModifierObjectExample(myIntArray);
}
/*
Output:
1 2 3 4
1 a test
5 6 7 8 9
2 b test again
System.Int32[]
*/