方法參數
根據預設,C# 中的引數會依值 傳遞至函式 。 這表示變數的複本會傳遞至 方法。 針對 value ( struct
) 類型,值 複本 會傳遞至 方法。 針對 reference ( class
) 類型,參考的 複本會傳遞至 方法。 參數修飾詞可讓您以傳址 方式傳遞引數 。 下列概念可協助您瞭解這些區別,以及如何使用參數修飾詞:
- 依值 傳遞表示 將變數 的複本傳遞至 方法。
- 以傳址 方式傳遞,表示 將變數 的存取權傳遞至 方法。
- 參考類型的 變數 包含其資料的參考。
- 實數值型別的 變數 會直接包含其資料。
因為結構是 實值型 別,因此當您將結構以傳值方式傳遞至方法時,方法會接收並操作結構引數的複本。 方法無法存取呼叫方法中的原始 struct,因此無法以任何方式變更它。 方法只能變更複本。
類別實例是 參考型 別,而不是實值型別。 當以傳值方式傳遞參考型別給方法時,方法會收到 class 執行個體的參考複本。 這兩個變數都參考相同的物件。 參數是參考的複本。 呼叫的方法無法重新指派呼叫方法中的實例。 不過,呼叫的方法可以使用參考的複本來存取實例成員。 如果呼叫的方法變更實例成員,呼叫方法也會看到這些變更,因為它參考相同的實例。
下例的輸出會說明其間的差異。 方法會變更欄位的值 willIChange
,因為 方法 ClassTaker
會使用 參數中的位址來尋找類別實例的指定欄位。 willIChange
呼叫方法中結構欄位不會從呼叫 StructTaker
變更,因為 引數的值是結構本身的複本,而不是其位址的複本。 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
參數,傳遞參考類型的執行個體。
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 。
- 安全內容 會定義任何運算式可以安全地存取的範圍。
- ref-safe-coNtext 定義可以安全地存取或修改任何運算式參考 的範圍 。
非正式地,您可以將這些範圍視為機制,以確保程式碼永遠不會存取或修改不再有效的參考。 只要參考參考至有效的物件或結構,參考即有效。 安全 內容 定義何時可以指派或重新指派變數。 ref-safe-coNtext 定義何時可以 指派 或 重新指派 變數。 指派會將變數指派給新的值; ref 指派 會指派變數來 參考 不同的儲存位置。
傳址參數
您可以將下列其中一個修飾詞套用至參數宣告,以傳址方式傳遞引數,而不是依值傳遞引數:
ref
:在呼叫 方法之前,必須先初始化 引數。 方法可以將新值指派給 參數,但不需要這麼做。out
:呼叫 方法不需要先初始化 引數,再呼叫 方法。 方法必須將值指派給 參數。readonly ref
:在呼叫 方法之前,必須先初始化 引數。 方法無法將新的值指派給 參數。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
表示方法會讀取,但無法寫入引數的值。 引數會以傳址方式傳遞,或透過暫存變數傳遞。
屬性不是變數。 它們是方法,無法傳遞至 ref
參數。 您無法在下列方法類型中使用先前的參數修飾詞:
- 使用 async 修飾詞定義的 async 方法。
- iterator 方法,其包括 yield return 或
yield break
陳述式。
擴充方法 也有使用這些引數關鍵字的限制:
- 關鍵字
out
無法在擴充方法的第一個引數上使用。 ref
當引數不是struct
時,無法在擴充方法的第一個引數上使用 關鍵字,或泛型型別不受限制為結構。ref readonly
除非第一個struct
引數是 ,否則無法使用 和in
關鍵字。ref readonly
和in
關鍵字不能用於任何泛型型別,即使限制為結構也一樣。
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
呼叫 Int32.TryParse 方法時定義 變數。
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
}
您可以使用 或 in
修飾詞呼叫 方法 ref
。 如果您省略 修飾詞,編譯器會發出警告。 當引數是運算式而非變數時,您無法新增 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
使用的引數必須代表可以直接參考的位置。 和ref
引數的out
相同一般規則適用:您無法使用常數、一般屬性或其他產生值的運算式。 否則,在呼叫月臺省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`
現在,假設有另一個使用 by-value 引數的方法可供使用。 結果的變更如下列程式碼所示:
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
宣告類型不是單一維度陣列,則會發生編譯器錯誤 CS0225 。
當您使用 params
參數呼叫方法時,可以傳入:
- 陣列專案類型的引數逗號分隔清單。
- 指定型別的引數陣列。
- 沒有引數。 如果不傳送任何引數,
params
清單的長度為零。
下例示範將引數傳送至 params
參數的各種方式。
public class MyClass
{
public static void UseParams(params int[] list)
{
for (int i = 0; i < list.Length; i++)
{
Console.Write(list[i] + " ");
}
Console.WriteLine();
}
public static void UseParams2(params object[] list)
{
for (int i = 0; i < list.Length; i++)
{
Console.Write(list[i] + " ");
}
Console.WriteLine();
}
static void Main()
{
// You can send a comma-separated list of arguments of the
// specified type.
UseParams(1, 2, 3, 4);
UseParams2(1, 'a', "test");
// A params parameter accepts zero or more arguments.
// The following calling statement displays only a blank line.
UseParams2();
// 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 };
UseParams(myIntArray);
object[] myObjArray = { 2, 'b', "test", "again" };
UseParams2(myObjArray);
// The following call causes a compiler error because the object
// array cannot be converted into an integer array.
//UseParams(myObjArray);
// The following call does not cause an error, but the entire
// integer array becomes the first element of the params array.
UseParams2(myIntArray);
}
}
/*
Output:
1 2 3 4
1 a test
5 6 7 8 9
2 b test again
System.Int32[]
*/
意見反應
提交並檢視相關的意見反應