C# ref/out 關鍵字與傳遞參考型別參數

C# ref/out 關鍵字與傳遞參考型別參數

C# 有 ref/out 關鍵字可以用來改變方法參數的傳遞機制,將原本的傳值(by value)改為傳址(by reference),因為有時候會碰到這樣的需求,提供給某方法的引數會希望輸出處理過的結果並回存到原本的變數上,此時就得用傳址參數 -- ref 或 out 參數來完成,兩者極為相似但有些許不同和需要注意的地方,以下摘錄自 MSDN Library:
  • ref 參數傳遞的引數必須先被初始化,out 則不需要。
  • out 參數要在離開目前的方法之前至少有一次指派值的動作。
  • 若兩個方法僅有 ref、out 關鍵字的差異,在編譯期會視為相同方法簽章,無法定義為多載方法。

參考底下程式碼可能會更容易瞭解:
class MethodParameter
{
    static void MyMethod(ref string p)
    {
        p = "Hello World";
    }

    // 不能只以 ref 或 out 區分多載方法
    //static void MyMethod(out string p)
    //{
    //    p = "Hello World";
    //}

    static void SampleMethod(ref int refParam, out int outParam)
    {
        outParam = 44;
    }

    static void Main(string[] args)
    {
        // ref 參數傳遞前必須初始化;
        // out 參數不需初始化,但必須於方法內給值。
        int p1 = 0;
        int p2;

        SampleMethod(ref p1, out p2);
        Console.WriteLine(p1 + ", " + p2);
    }
}

## 提示

參數引數這兩個名詞很多時候都被混用了,這邊稍做區分:宣告方法時定義要傳遞的值叫參數;而呼叫方法時傳遞給參數的值叫引數。因此以上面的程式碼來說,我們會說 SampleMethod 定義了名為 refParam、outParam 的兩個參數,至於在 Main 方法裡 p1、p2 則是傳遞給 SampleMethod 的引數。


目前為止談到的仍只是在用法上打轉,我猜雖然很多人應該早就用過,但有個容易讓人感到困惑或產生誤解的地方:在實值型別來看或許傳值或傳址很容易理解,但對於參考型別仍然有意義嗎?…參閱 ref (C# Reference) 文件裡有這麼一段:

Note:

Do not confuse the concept of passing by reference with the concept of reference types. The two concepts are not related; a method parameter can be modified by ref regardless of whether it is a value type or a reference type. Therefore, there is no boxing of a value type when it is passed by reference.

譯:傳址參數的概念以及參考型別的概念不要搞混了,兩者毫無關係;方法參數可用 ref 關鍵字修飾無關乎其為實值或參考型別。因此,實值型別用傳址的方式傳遞時並不會發生封裝(意指不是真正轉型為參考型別)。


雖然沒有直接挑明著說(),但其實可以利用前面寫的 MyMethod 來驗證:
MyMethod(ref s);
Console.WriteLine(s);
// Output: Hello World

可知對參考型別來說傳址參數仍然有作用!我這樣說好了,實值型別與參考型別的最大不同點是存放資料的方式,但如何傳遞資料這又是另一個課題,所以不該混為一談。

為了瞭解傳遞資料的機制,我另外找到兩篇文章對此加以說明:
很重要的一個前提就是預設傳遞物件的方式是採用傳值(無論其型別是實值或參考型別)。更進一步來說,預設情況下呼叫一個方法時,所有引數都會複製一份存放的資料到堆疊(stack)裡,具體行為是將複製的內容指派給方法參數 -- 實值型別複製其擁有的值,參考型別則複製其擁有的指標 (pointer),因為是多 copy 一份,因此在方法內所做的任何修改僅是對這副本作用,不會影響原始傳入的變數。

那麼傳址又是怎麼一回事?…說穿了就是強制參考回堆疊上的記憶體位址(不再複製一份),因此傳址參數在程序內所做的修改都將直接更改堆疊上配置位址所擁有的資料(這篇 圖解 - Parameter passing in C# 值得一看,相信應該可以幫助理解才是)。

理論說完了,最後整理各種參數的確切行為如下:
  • 傳值參數 - 這是預設行為,傳遞時會將引數的副本指派給方法參數,很明顯只有輸入
  • ref 參數 - 傳遞引數的參考位址給方法參數,在程序內所做的變動都將反映到原輸入的變數上,既有輸入也會輸出
  • out 參數 - 傳遞行為亦屬傳址,但因為在離開作用方法前必定得重新指派值,等同無視輸入(一定會被覆寫掉),以結果論只有輸出

因為最近有機會再度碰上,加上自己之前的觀念也沒有很正確,花時間研究後順勢做個整理分享出來,希望在實際的開發上可以幫助你選擇真正需要的!


備註

我在找文件時看到:請勿以傳址方式傳遞型別避免 Out 參數 這兩篇,大意是說 Visual Studio Team System 針對 Managed 程式碼分析會對 ref 或 out 參數提出警告,主張大多數情況下應重新設計該方法,以替代作法達到相同目的;若考慮到團隊裡不見得每一位開發人員都能熟練運用傳址參數,不鼓勵使用雖然保守卻也能避免出錯…,這或許可以解釋為何 MSDN 文件在這方面寫得語焉不詳吧。


相關連結