ref 傳回值和 ref 區域變數

從 C# 7.0 開始,C# 支援參考傳回值 (ref 傳回值)。 參考傳回值允許方法將變數參考 (而非值) 傳回給呼叫者。 呼叫者接著可以選擇將傳回的變數視為以傳值方式或以傳址方式傳回。 呼叫者可建立本身為參考傳回值的新變數,稱為 ref 區域變數。

何謂參考傳回值?

大部分的開發人員都熟悉「以傳址方式」將引數傳遞給已呼叫方法。 所呼叫方法的引數清單包含以傳址方式傳遞的變數。 呼叫者會觀察到所呼叫方法對其值進行的任何變更。 「參考傳回值」表示方法會將「參考」 (或別名) 傳回給某個變數。 該變數的範圍必須包含方法。 該變數的存留期必須延伸到傳回方法。 呼叫者對方法的傳回值所進行的修改,是針對方法傳回的變數進行。

宣告方法傳回「參考傳回值」,表示方法會將別名傳回給變數。 此設計通常用於呼叫的程式碼應可透過別名來存取該變數 (包括進行修改) 時。 這樣一來,以傳址方式傳回的方法就不能有傳回型別 void

方法可傳回為參考傳回值的運算式有一些限制。 限制包含:

  • 傳回值必須有超過方法執行期間的存留期。 換句話說,它不能是將它傳回之方法中的區域變數。 它可以是類別的執行個體或靜態欄位,也可以是傳遞給方法的引數。 嘗試傳回區域變數會產生編譯器錯誤 CS8168「無法以傳址方式傳回本機 'obj',因為其非參考本機」。

  • 傳回值不能是常值 null。 傳回 null 會產生編譯器錯誤 CS8156「無法在此內容中使用運算式,因為其可能不會以傳址方式傳回」。

    具有 ref 傳回的方法可以將別名傳回給變數,其值目前為 null (未具現化的) 值,或實數值型別的 可為 null 實值型 別。

  • 傳回值不能是常數、列舉成員、屬性的以傳值方式傳回值,或是 classstruct 的方法。 違反此規則會產生編譯器錯誤 CS8156「無法在此內容中使用運算式,因為其可能不會以傳址方式傳回」。

此外,非同步方法上不允許參考傳回值。 在非同步方法完成執行之前,可能會傳回非同步方法,但其傳回值仍然為未知。

定義 ref 傳回值

傳回 參考傳回值 的方法必須滿足下列兩個條件:

  • 方法簽章在傳回型別前包含 ref 關鍵字。
  • 方法主體中的每個 return 陳述式在所傳回執行個體前包含 ref 關鍵字。

下列範例顯示滿足那些條件並傳回參考給名為 pPerson 物件的方法:

public ref Person GetContactInformation(string fname, string lname)
{
    // ...method implementation...
    return ref p;
}

使用 ref 傳回值

ref 傳回值是在已呼叫方法的範圍中,另一個變數的別名。 您可以將任何使用 ref 傳回值的用法,解譯為使用別名所代表的變數:

  • 當您指派其值時,是將值指派給別名的變數。
  • 當您讀取其值時,是讀取別名的變數值。
  • 如果您以「傳址」方式傳回它,就是將別名傳回至同一個變數。
  • 如果您以「傳址」方式將它傳遞到另一個方法,就是將參考傳遞至別名的變數。
  • 當您建立 ref 區域變數別名時,就是對相同變數建立新的別名。

ref 區域變數

假設 GetContactInformation 方法是宣告為 ref 傳回值:

public ref Person GetContactInformation(string fname, string lname)

傳值方式指派會讀取變數值,並將它指派給新的變數:

Person p = contacts.GetContactInformation("Brandie", "Best");

上述的指派將 p 宣告為區域變數。 其初始值的複製來源,是讀取 GetContactInformation 所傳回的值。 對 p 的任何後續指派將不會變更 GetContactInformation 傳回的變數值。 變數 p 不再是所傳回之變數的別名。

您宣告「ref 區域變數」以將別名複製到原始的值。 在下列指派中,pGetContactInformation 所傳回之變數的別名。

ref Person p = ref contacts.GetContactInformation("Brandie", "Best");

後續 p 的使用方式與使用 GetContactInformation 傳回的變數相同,因為 p 是該變數的別名。 變更 p 也會變更 GetContactInformation 傳回的變數。

ref 關鍵字是用於區域變數宣告的前面「和」方法呼叫的前面。

您可透過相同方式以參考存取值。 在某些情況下,以參考存取值會避免潛在過度浪費資源的複製作業,進而增加效能。 例如,下列陳述式示範了如何定義用於參考值的區域變數值。

ref VeryLargeStruct reflocal = ref veryLargeStruct;

區域變數宣告的前面「和」第二個範例中值的前面都會使用 ref 關鍵字。 無法在這兩個範例內的變數宣告和指派中同時包含 ref 關鍵字,會導致編譯器錯誤 CS8172「無法使用值將傳址變數初始化」。

在 C# 7.3 之前,無法重新指派 ref 區域變數,以在初始化之後參照不同的儲存體。 已移除該限制。 下列範例示範重新指派:

ref VeryLargeStruct reflocal = ref veryLargeStruct; // initialization
refLocal = ref anotherVeryLargeStruct; // reassigned, refLocal refers to different storage.

ref 區域變數在宣告後仍然必須予以初始化。

ref 傳回值和 ref 區域變數:範例

下列範例定義 NumberStore 類別,以儲存整數值的陣列。 FindNumber 方法會以傳址方式傳回第一個數字,而此數字大於或等於傳遞為引數的數字。 如果數字未大於或等於引數,則方法會在索引 0 傳回數字。

using System;

class NumberStore
{
    int[] numbers = { 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023 };

    public ref int FindNumber(int target)
    {
        for (int ctr = 0; ctr < numbers.Length; ctr++)
        {
            if (numbers[ctr] >= target)
                return ref numbers[ctr];
        }
        return ref numbers[0];
    }

    public override string ToString() => string.Join(" ", numbers);
}

下列範例會呼叫 NumberStore.FindNumber 方法來擷取大於或等於 16 的第一個值。 呼叫者接著會將方法所傳回的值加倍。 範例輸出顯示 NumberStore 執行個體之陣列項目值中所反映的變更。

var store = new NumberStore();
Console.WriteLine($"Original sequence: {store.ToString()}");
int number = 16;
ref var value = ref store.FindNumber(number);
value *= 2;
Console.WriteLine($"New sequence:      {store.ToString()}");
// The example displays the following output:
//       Original sequence: 1 3 7 15 31 63 127 255 511 1023
//       New sequence:      1 3 7 15 62 63 127 255 511 1023

如果不支援參考傳回值,則是透過傳回陣列項目和其值的索引來執行這類作業。 呼叫者接著可以使用這個索引,來修改不同方法呼叫中的值。 不過,呼叫者也可以修改要存取的索引,也可能修改其他陣列值。

下列範例示範在 C# 7.3 之後如何重寫 FindNumber 方法以使用 ref 本機重新指派:

using System;

class NumberStore
{
    int[] numbers = { 1, 3, 7, 15, 31, 63, 127, 255, 511, 1023 };

    public ref int FindNumber(int target)
    {
        ref int returnVal = ref numbers[0];
        var ctr = numbers.Length - 1;
        while ((ctr >= 0) && (numbers[ctr] >= target))
        {
            returnVal = ref numbers[ctr];
            ctr--;
        }
        return ref returnVal;
    }

    public override string ToString() => string.Join(" ", numbers);
}

如果搜尋的數位接近陣列結尾,則第二個版本會更有效率,因為陣列是從結尾開始算起,並會導致較少的專案被檢查。

另請參閱