方法參數

根據預設,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 參數,傳遞參考類型的執行個體。

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-contextref-safe-context

  • safe-context 會定義可安全存取任何運算式的範圍。
  • ref-safe-context 會定義可安全存取或修改任何運算式「參考」的範圍。

您可以非正式地將這些範圍視為一種機制,可確保您的程式碼永遠不會存取或修改不再有效的參考。 只要參考的是有效物件或結構,該參考就有效。 safe-context 會定義變數何時可以指派或重新指派。 ref-safe-context 會定義變數何時可以「指派參考」 或「重新指派參考」。 指派會將變數指派給新值;「指派參考」會指派變數,以「參考」不同的儲存位置。

傳址參數

您可以將下列其中一個修飾元套用至參數宣告,以藉傳址方式傳遞引數,而不是以藉傳值方式傳遞:

  • ref:在呼叫方法之前,必須先初始化引數。 方法可以將新值指派給參數,但這並非必要。
  • out:發出呼叫的方法在呼叫方法之前不需要先初始化引數。 方法必須將值指派給參數。
  • readonly ref:在呼叫方法之前,必須先初始化引數。 方法無法將新的值指派給參數。
  • in:在呼叫方法之前,必須先初始化引數。 方法無法將新的值指派給參數。 編譯器可能會建立暫存變數,以將引數的複本保存至 in 參數。

類別的成員的簽章,不能只有在 refref readonlyinout 部分不同。 如果型別的兩個成員之間,唯一的區別在於其中一個有 ref 參數,而另一個有 outref readonlyin 參數,則會發生編譯器錯誤。 但如果一種方法有 refref readonlyinout 參數,而另一種方法有以藉傳值方式傳遞的參數,則可以對方法進行多載,如下列範例所示。 在其他需要簽章比對的情況 (例如隱藏或覆寫) 中,inrefref readonlyout 是簽章的一部分,但彼此不相符。

當參數具有上述其中一個修飾元時,對應的引數可以有相容的修飾元:

  • 參數的 ref 引數必須包含 ref 修飾元。
  • 參數的 out 引數必須包含 out 修飾元。
  • 參數的 in 引數可以選擇性地包含 in 修飾元。 若 ref 修飾元改為使用在引數上,編譯器會發出警告。
  • 參數的 ref readonly 引數應該包含 inref 修飾元,但不能同時包含兩者。 若未包含任一修飾元,編譯器會發出警告。

當您使用這些修飾元時,其會描述如何使用引數:

  • ref 表示方法可以讀取或寫入引數的值。
  • out 表示方法會設定引數的值。
  • ref readonly 表示方法會讀取引數的值,但無法寫入引數的值。 引數「應該」以藉傳址方式傳遞。
  • in 表示方法會讀取引數的值,但無法寫入引數的值。 引數會以藉傳址方式傳遞,或透過暫存變數傳遞。

屬性不是變數。 它們都是方法,且無法傳遞給 ref 參數。 您無法在下列方法類型中使用先前的參數修飾元:

  • 使用 async 修飾詞定義的 async 方法。
  • iterator 方法,其包括 yield returnyield break 陳述式。

擴充方法也具有使用這些引數關鍵字的限制:

  • 擴充方法的第一個引數上不能使用 out 關鍵字。
  • 當引數不是 struct,或不限制為結構的泛型型別時,擴充方法的第一個引數上不能使用 ref 關鍵字。
  • 除非第一個引數是 struct,否則無法使用 ref readonlyin 關鍵字。
  • 任何泛型型別上都不能使用 ref readonlyin 關鍵字,即使限制為結構時也一樣。

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 修飾元必須存在於方法宣告中。 呼叫位置的修飾元是選擇性的。 可以使用 inref 修飾元。 ref readonly 修飾元在呼叫位置無效。 您在呼叫網站使用的修飾元可協助描述引數的特性。 只有當引數是變數且可寫入時,您才能使用 ref。 只有當引數是變數時,您才能使用 in。 它可能是可寫入或唯讀的。 若引數不是變數,但為運算式,則無法新增任一修飾元。 下列範例顯示這些情況。 下列方法會使用 ref readonly 修飾元來指出基於效能考慮,應該以藉傳址方式傳遞大型結構:

public static void ForceByRef(ref readonly OptionStruct thing)
{
    // elided
}

您可以使用 refin 修飾元來呼叫方法。 若省略修飾元,編譯器會發出警告。 當引數是運算式而非變數時,您無法新增 inref 修飾元,因此您應該隱藏警告:

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 使用的引數必須代表可以直接參考的位置。 outref 引數的相同一般規則同樣適用:您無法使用常數、一般屬性或其他會產生值的運算式。 否則,在呼叫位置省略 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 參數的宣告型別不是單一維度陣列,就會發生編譯器錯誤 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[]
*/