方法參數和修飾符號
根據預設,C# 中的引數會以藉傳值方式傳遞至函式。 這表示變數的複本會傳遞至方法。 針對值 (struct
) 型別,會將值的複本傳遞至方法。 針對參考 (class
) 型別,會將參考的複本傳遞至方法。 參數修飾元可讓您以藉傳址方式傳遞引數。
因為結構是 實值型別,因此當您將結構以值類型傳遞至方法時,該方法會接收並操作參數的副本。 方法無法存取呼叫方法中的原始 struct,因此無法以任何方式變更它。 方法只能變更複本。
類別執行個體是參考型別,不是實值型別。 當參考型別以傳值方式傳遞至方法時,方法會接收實例的參考複本。 這兩個變數都參考相同的物件。 參數是參考的複本。 被呼叫的方法無法重新指派呼叫方法中的執行個體。 不過,被呼叫的方法可以使用參考的複本來存取執行個體成員。 如果被呼叫的方法變更執行個體成員,呼叫方法也會看到那些變更,因為它參考相同的執行個體。
值傳遞和引用傳遞
本節中的所有範例都使用下列兩個 record
類型來說明 class
類型和 struct
類型之間的差異:
public record struct Point(int X, int Y);
// This doesn't use a primary constructor because the properties implemented for `record` types are
// readonly in record class types. That would prevent the mutations necessary for this example.
public record class Point3D
{
public int X { get; set; }
public int Y { get; set; }
public int Z { get; set; }
}
下列範例的輸出說明依實值傳遞結構類型與依值傳遞類別類型之間的差異。 這兩個 Mutate
方法都會變更其自變數的屬性值。 當參數是 struct
類型時,這些變更會應用在引數資料的複本上。 當參數是 class
型別時,這些變更會影響由參數所參考的實例:
public class PassTypesByValue
{
public static void Mutate(Point pt)
{
Console.WriteLine($"\tEnter {nameof(Mutate)}:\t\t{pt}");
pt.X = 19;
pt.Y = 23;
Console.WriteLine($"\tExit {nameof(Mutate)}:\t\t{pt}");
}
public static void Mutate(Point3D pt)
{
Console.WriteLine($"\tEnter {nameof(Mutate)}:\t\t{pt}");
pt.X = 19;
pt.Y = 23;
pt.Z = 42;
Console.WriteLine($"\tExit {nameof(Mutate)}:\t\t{pt}");
}
public static void TestPassTypesByValue()
{
Console.WriteLine("===== Value Types =====");
var ptStruct = new Point { X = 1, Y = 2 };
Console.WriteLine($"After initialization:\t\t{ptStruct}");
Mutate(ptStruct);
Console.WriteLine($"After called {nameof(Mutate)}:\t\t{ptStruct}");
Console.WriteLine("===== Reference Types =====");
var ptClass = new Point3D { X = 1, Y = 2, Z = 3 };
Console.WriteLine($"After initialization:\t\t{ptClass}");
Mutate(ptClass);
Console.WriteLine($"After called {nameof(Mutate)}:\t\t{ptClass}");
// Output:
// ===== Value Types =====
// After initialization: Point { X = 1, Y = 2 }
// Enter Mutate: Point { X = 1, Y = 2 }
// Exit Mutate: Point { X = 19, Y = 23 }
// After called Mutate: Point { X = 1, Y = 2 }
// ===== Reference Types =====
// After initialization: Point3D { X = 1, Y = 2, Z = 3 }
// Enter Mutate: Point3D { X = 1, Y = 2, Z = 3 }
// Exit Mutate: Point3D { X = 19, Y = 23, Z = 42 }
// After called Mutate: Point3D { X = 19, Y = 23, Z = 42 }
}
}
struct
型別所做的修改。 當參考型別以傳址方式傳遞時,不會有任何語意變更:
public class PassTypesByReference
{
public static void Mutate(ref Point pt)
{
Console.WriteLine($"\tEnter {nameof(Mutate)}:\t\t{pt}");
pt.X = 19;
pt.Y = 23;
Console.WriteLine($"\tExit {nameof(Mutate)}:\t\t{pt}");
}
public static void Mutate(ref Point3D pt)
{
Console.WriteLine($"\tEnter {nameof(Mutate)}:\t\t{pt}");
pt.X = 19;
pt.Y = 23;
pt.Z = 42;
Console.WriteLine($"\tExit {nameof(Mutate)}:\t\t{pt}");
}
public static void TestPassTypesByReference()
{
Console.WriteLine("===== Value Types =====");
var pStruct = new Point { X = 1, Y = 2 };
Console.WriteLine($"After initialization:\t\t{pStruct}");
Mutate(ref pStruct);
Console.WriteLine($"After called {nameof(Mutate)}:\t\t{pStruct}");
Console.WriteLine("===== Reference Types =====");
var pClass = new Point3D { X = 1, Y = 2, Z = 3 };
Console.WriteLine($"After initialization:\t\t{pClass}");
Mutate(ref pClass);
Console.WriteLine($"After called {nameof(Mutate)}:\t\t{pClass}");
// Output:
// ===== Value Types =====
// After initialization: Point { X = 1, Y = 2 }
// Enter Mutate: Point { X = 1, Y = 2 }
// Exit Mutate: Point { X = 19, Y = 23 }
// After called Mutate: Point { X = 19, Y = 23 }
// ===== Reference Types =====
// After initialization: Point3D { X = 1, Y = 2, Z = 3 }
// Enter Mutate: Point3D { X = 1, Y = 2, Z = 3 }
// Exit Mutate: Point3D { X = 19, Y = 23, Z = 42 }
// After called Mutate: Point3D { X = 19, Y = 23, Z = 42 }
}
}
上述範例已修改參數的屬性。 方法也可以將參數重新指派給新的值。 以傳值或傳址方式傳遞時,重新指派的行為會因結構與類別類型而不同。 下列範例顯示結構類型和類別類型在值傳遞的參數重新指派時的行為:
public class PassByValueReassignment
{
public static void Reassign(Point pt)
{
Console.WriteLine($"\tEnter {nameof(Reassign)}:\t\t{pt}");
pt = new Point { X = 13, Y = 29 };
Console.WriteLine($"\tExit {nameof(Reassign)}:\t\t{pt}");
}
public static void Reassign(Point3D pt)
{
Console.WriteLine($"\tEnter {nameof(Reassign)}:\t\t{pt}");
pt = new Point3D { X = 13, Y = 29, Z = -42 };
Console.WriteLine($"\tExit {nameof(Reassign)}:\t\t{pt}");
}
public static void TestPassByValueReassignment()
{
Console.WriteLine("===== Value Types =====");
var ptStruct = new Point { X = 1, Y = 2 };
Console.WriteLine($"After initialization:\t\t{ptStruct}");
Reassign(ptStruct);
Console.WriteLine($"After called {nameof(Reassign)}:\t\t{ptStruct}");
Console.WriteLine("===== Reference Types =====");
var ptClass = new Point3D { X = 1, Y = 2, Z = 3 };
Console.WriteLine($"After initialization:\t\t{ptClass}");
Reassign(ptClass);
Console.WriteLine($"After called {nameof(Reassign)}:\t\t{ptClass}");
// Output:
// ===== Value Types =====
// After initialization: Point { X = 1, Y = 2 }
// Enter Reassign: Point { X = 1, Y = 2 }
// Exit Reassign: Point { X = 13, Y = 29 }
// After called Reassign: Point { X = 1, Y = 2 }
// ===== Reference Types =====
// After initialization: Point3D { X = 1, Y = 2, Z = 3 }
// Enter Reassign: Point3D { X = 1, Y = 2, Z = 3 }
// Exit Reassign: Point3D { X = 13, Y = 29, Z = -42 }
// After called Reassign: Point3D { X = 1, Y = 2, Z = 3 }
}
}
上述範例顯示,當您將參數重新指派給新值時,不論類型是實值型別還是參考型別,呼叫方法都看不到該變更。 下列範例顯示當您重新賦值給以傳址方式傳遞的變數時的行為:
public class PassByReferenceReassignment
{
public static void Reassign(ref Point pt)
{
Console.WriteLine($"\tEnter {nameof(Reassign)}:\t\t{pt}");
pt = new Point { X = 13, Y = 29 };
Console.WriteLine($"\tExit {nameof(Reassign)}:\t\t{pt}");
}
public static void Reassign(ref Point3D pt)
{
Console.WriteLine($"\tEnter {nameof(Reassign)}:\t\t{pt}");
pt = new Point3D { X = 13, Y = 29, Z = -42 };
Console.WriteLine($"\tExit {nameof(Reassign)}:\t\t{pt}");
}
public static void TestPassByReferenceReassignment()
{
Console.WriteLine("===== Value Types =====");
var ptStruct = new Point { X = 1, Y = 2 };
Console.WriteLine($"After initialization:\t\t{ptStruct}");
Reassign(ref ptStruct);
Console.WriteLine($"After called {nameof(Reassign)}:\t\t{ptStruct}");
Console.WriteLine("===== Reference Types =====");
var ptClass = new Point3D { X = 1, Y = 2, Z = 3 };
Console.WriteLine($"After initialization:\t\t{ptClass}");
Reassign(ref ptClass);
Console.WriteLine($"After called {nameof(Reassign)}:\t\t{ptClass}");
// Output:
// ===== Value Types =====
// After initialization: Point { X = 1, Y = 2 }
// Enter Reassign: Point { X = 1, Y = 2 }
// Exit Reassign: Point { X = 13, Y = 29 }
// After called Reassign: Point { X = 13, Y = 29 }
// ===== Reference Types =====
// After initialization: Point3D { X = 1, Y = 2, Z = 3 }
// Enter Reassign: Point3D { X = 1, Y = 2, Z = 3 }
// Exit Reassign: Point3D { X = 13, Y = 29, Z = -42 }
// After called Reassign: Point3D { X = 13, Y = 29, Z = -42 }
}
}
上述範例展示如何更改以參考傳遞的參數值在呼叫上下文中的可見性。
參考和值的安全內容
方法可以將參數的值儲存在欄位中。 當以藉傳值方式傳遞時,通常都很安全。 值會經過複製,而且參考型別儲存在欄位中,因此可以取得。 若要安全地依參考傳遞參數,編譯器則需要定義何時可以安全地將參考指派給新的變數。 針對每個運算式,編譯器會定義將存取權限制於運算式或變數的「安全內容」。 編譯器使用兩個範圍:safe-context 與 ref-safe-context。
- safe-context 會定義可安全存取任何運算式的範圍。
- ref-safe-context 會定義可安全存取或修改任何運算式「參考」的範圍。
您可以非正式地將這些範圍視為一種機制,可確保您的程式碼永遠不會存取或修改不再有效的參考。 只要參考的是有效物件或結構,該參考就有效。 safe-context 會定義變數何時可以指派或重新指派。 ref-safe-context 會定義變數何時可以「指派參考」 或「重新指派參考」。 指派會將變數指派給新值;「指派參考」會指派變數,以「參考」不同的儲存位置。
傳址參數
您可以將下列其中一個修飾元套用至參數宣告,以藉傳址方式傳遞引數,而不是以藉傳值方式傳遞:
-
ref
:在呼叫方法之前,必須先初始化引數。 方法可以將新值指派給參數,但這並非必要。 -
out
:發出呼叫的方法在呼叫方法之前不需要先初始化引數。 方法必須將值指派給參數。 -
ref readonly
:在呼叫方法之前,必須先初始化引數。 方法無法將新的值指派給參數。 -
in
:在呼叫方法之前,必須先初始化引數。 方法無法將新的值指派給參數。 編譯器可能會建立暫存變數,以將引數的複本保存至in
參數。
以傳址方式傳遞的參數是 參考變數。 它沒有它自己的值。 相反地,它指向稱為 參照的不同變數。 參考變數可以 ref 重新指派,這會變更其引用對象。
類別的成員的簽章,不能只有在 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
關鍵字。 - 當引數不是
ref
,或不限制為結構的泛型型別時,擴充方法的第一個引數上不能使用struct
關鍵字。 - 除非第一個引數是
ref readonly
,否則無法使用in
與struct
關鍵字。 - 任何泛型型別上都不能使用
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
變數宣告會產生更精簡、更容易閱讀的程式碼,也可避免不小心在方法呼叫前先將值指派給變數。 下列範例會在對 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[]
*/
當參數的 params
自變數是集合類型時,多載解析可能會造成模棱兩可。 自變數的集合類型必須可轉換成參數的集合類型。 當不同的多載為該參數提供更好的轉換時,該方法可能更好。 不過,如果參數的 params
自變數是離散元素或遺漏,則具有不同 params
參數類型的所有多載都等於該參數。
如需詳細資訊,請參閱 C# 語言規格