C#筆記 – 參數

可選參數與命名參數
  • 可選參數

    • 聲明可選參數:

      • void Dump(int x, int y = 20, int z = 30)
      • x為必備參數;y和z為可選參數,20和30分別為y和z的默認值

    • 動機:降低重載的方法數量
    • 可選參數就是在定義方法/有參屬性的參數時,為它們賦默認值,如果調用參數時沒有傳值,C#編譯器就自動嵌入參數的默認值

      • 委托的一部分參數也可以指定默認值

    • 有默認值的參數需放在沒默認值的參數之後,參數數組(如有)之前

      • 參數數組不能聲明為可選的

      • 如果調用者沒有指定值,將使用空數組代替

    • 默認值約束:

      • 必須為C#認定的基元類型/枚舉/可空的引用類型,如:

        • 數字/字符串字面量

        • null

        • const成員

        • 枚舉成員

        • default(T)操作符

      • 對於值類型可以使用與default等價的無參構造函數

    • 如果參數使用了ref/out進行標識,就不能設置默認值

    • 一旦為參數分配了默認值,編譯器會向該參數應用定制特性「Optional」,並持久性地保存到元數據中

      • 此外,編譯器向參數應用DefaultParameterValue特性,並向該特性構造器傳遞開發者指定的參數默認值。同樣,持久性地保存到元數據中。

      • Method #2 (06000002) 
        -------------------------------------------------------
        MethodName: DefaultParam (06000002)
        Flags     : [Private] [Static] [HideBySig] [ReuseSlot] (00000091)
        RVA       : 0x000020c1
        ImplFlags : [IL] [Managed] (00000000)
        CallCnvntn: [DEFAULT]
        ReturnType: Void
        1 Arguments
        Argument #1: I4
        1 Parameters
        (1) ParamToken : (08000002) Name : i flags: [Optional] [HasDefault] (00001010) Default: (I4) 10
      • 當編譯器發現某個方法調用缺失了部分實參,就會嘗試從元數據中提取默認值,將值自動嵌入到調用中。

    • 調用含有可選參數的方法時,編譯器默認我們傳遞的實參順序是與形參聲明順序是一致的

  • 命名參數 

    • 在指定實參的值時,可以同時指定相應參數的名稱。編譯器將判斷參數的名稱是否正確,並將指定的值賦給這個參數。

    • 命名實參可使編譯器幫我們判斷該實參具體是要傳給哪個形參,調用方法時的參數順序變得不再重要

      • void Main()
        {
           Show("SB", "HEHE");
           Show(caption:"HEHE", text:"SB");
        }

        void Show(string text, string caption)
        {
           Console.WriteLine($"{text}: {caption}");
        }
    • 編譯器會根據名稱進行識別

      • 如果要對包含ref或out的參數指定名稱,需要將ref或out修飾符放在名稱之後,實參之前

      • int.TryParse("10", result: out number);
    • 命名實參與位置實參的混合使用

      • 未命名的實參稱為「位置實參」

        • 位置實參總是指向方法聲明中相應的參數,我們不能跳過參數之後,再通過命名相應位置的實參來指定

      • 所有命名實參必須放在「位置實參」之後。

        • 除非所有實參的位置與形參聲明位置保持一致,但這樣也沒必要用命名實參了

      • void Main()
        {
        Show("SB", name: "GO", caption:"HEHE"); //Valid
        Show(name: "GO", "SB", caption:"HEHE"); //InValid
           Show(text: "GO", "SB", name:"HEHE"); //Valid
        }
        void Show(string text, string caption, string name)
        {
           Console.WriteLine($"{name}({text}): {caption}");
        }
    • 實參求值順序

      • 按編寫順序求值,即使這個順序不同於參數的聲明順序

命名實參與可選參數混合使用
  • void Main()
    {
       //省略了encoding參數,直接使用timestamp的命名實參
       AppendTimestamp("utf8.txt", "Message in the future", timestamp: new DateTime(2030, 1, 1));
    }

    void AppendTimestamp(string filename, string message, Encoding encoding = null, DateTime? timestamp = null)
    {
       
    }
  • 不易變性和對象初始化

    • 命名實參和可選參數還可以用在不易變的對象初始化上

    • void Main()
      {
         Message msg = new Message(
             from: "skeet@pobox.com",
             to: "csharp-in-depth-readers@everywhere.com",
             body: "I hope you like the third edition",
             subject: "A quick message"
        );
      }

      public class Message
      {
         string from;
         string to;
         string body;
         string subject;
         byte[] attachment;

         public Message() { }
         public Message(string from, string to, string body,
                        string subject = null, byte[] attachment = null)
        {
             this.from = from;
             this.to = to;
             this.body = body;
             this.subject = subject;
             this.attachment = attachment;
        }
      }
  • 重載決策

    • 可選參數會增加適用方法(如果方法參數數量多於指定的實參數量)

    • 命名實參會減少適用方法的數量(排除那些沒有適當參數名稱的方法)

    • 為了檢查是否存在特定的適用方法,編譯器會使用位置參數的順序構建一個傳入實參的列表

      • 然後對命名實參和剩餘的參數進行匹配。如果沒有指定某個必備參數,或某個命名實參不能與剩餘的參數相匹配,那這個方法就是不適用的

    • 如果兩個方法都是適用的,其中一個方法的所有實參都顯式指定,而另一個方法使用了某個可選參數的默認值,則未使用默認值的方法勝出

      • 但這不適用於僅比較所使用的默認值數量的情況——它是嚴格按照「是否使用了默認值」來劃分的

      • static void Foo(int x = 10) { }
        static void Foo(int x = 10, int y = 20) { }

        Foo(); //Error:兩個方法的參數都是可選的,編譯器無法判斷到底要調用哪一個方法
        Foo(1); //Foo(int x = 10):使用這個方法不涉及默認值
        Foo(y: 2); //Foo(int x = 10, int y = 20):命名了y實參,只有第二個方法有y的形參
        Foo(1, 2); //Foo(int x = 10, int y = 20):傳了兩個參數,只有第二個方法有2個參數
    • 如果某些方法聲明在基類中,而派生類中包含適用方法,那派生類中的方法勝出

    • 命名實參可以代替強制轉換

      • void Method(int x, object y) { }
        void Method(object x, int y) { }

        Method(10, 10); //有歧義:兩個方法都適用,哪一個都不比另一個更優
        //消除歧義方法
        Method(10, (object)10); //強制轉換
        Method(x: 10, y: 10); //命名實參
引用方式的參數傳遞(out/ref)
  • static void GetVal(out int v)
    {
       v = 20;
    }

    static void AddVal(ref int v)
    {
       v += 10;
    }
  • CLR不區分out和ref,兩者生成的IL代碼是一樣的

    • 元數據幾乎完全一致,只有一個標記不同,用於記錄聲明方法時指定的是out還是ref

      • .method private hidebysig static void  GetVal([out] int32& v) cil managed

        .method private hidebysig static void AddVal(int32& v) cil managed
  • C#編譯器把兩者區分開,這個區別決定了由哪個方法負責初始化引用的對象

    • 如果參數用out標記,則不需要調用者在調用方法前初始化好對象

      • 被調用的方法不能讀取參數的值

      • 在返回前必須向這個值寫入

    • 如果參數用ref標記,則需要在調用方法前把參數初始化完成

      • 被調用的方法可以讀/寫值

  • 值類型的out/ref

    • class Program
      {
         static void Main(string[] args)
        {
             //值類型
             int x;
             GetVal(out x);
             Console.WriteLine(x);
             int y = 5;
             AddVal(ref y);
             Console.WriteLine(y);
        }

         static void GetVal(out int v)
        {
             v = 20;
        }

         static void AddVal(ref int v)
        {
             v += 10;
        }
      }
    • 「out」在該代碼中:

      1. x在Main的棧幀中聲明

      2. x的地址傳給GetVal

      3. GetVal的v是一個指針,指向Main棧幀中的int32值

      4. GetVal內部把v指向的值修改為20

      5. 返回(x = 20)

      • 為大的值類型使用out可提升代碼執行效率,因為它避免了進行方法調用時複製值類型實例的字段

    • 「ref」在該代碼中:

      1. y在Main的棧幀中聲明,並初始化為5

      2. y的地址傳給AddVal

      3. AddVal的v是一個指針,指向Main棧幀中的int32值

      4. 由於v指向的值必然是已經初始化過的,因此AddVal內部可以隨意修改v指向的值(+=10)

      5. 返回(y=15)

    • 從IL和CLR看來,out和ref是一樣的:導致傳遞指向實例的一個指針

      • 編譯器則會辨別兩者,並根據區別,用不同的標準去驗證代碼的正確性

      • CLR雖然允許使用out和ref來對一般參數傳遞類型的方法進行重載

        • 但由於CLR認為out和ref是一樣的,因此兩個重載方法之間不能只有out和ref的區別

  • 引用類型的out/ref

    • out/ref對於值類型而言,就是允許方法操縱單一的值類型實例

      • 調用者必須為實例分配內存,被調用者則操縱該內存中的內容

    • out/ref對於引用類型而言,則是調用代碼為一個指針(指向一個引用類型對象)分配內存,被調用者則操縱這個指針

      • 因此,只有方法「返回」對「方法知道的一個對象」的引用時,為引用類型使用out/ref才有意義(也就是說,引用對象是在方法內完成對象實例化的,才有需要使用out/ref

    • class Program
      {
         static void Main(string[] args)
        {
             //引用類型
             //Object tc; //Type must be exactly same
             TestClass tc;
             StartProcessinFile(out tc);
             if(tc != null)
            {
                 ContinueProcessingFiles(ref tc);
                 tc.OutputValue();
            }
        }

         static void StartProcessinFile(out TestClass tester)
        {
             tester = new TestClass();
        }

         static void ContinueProcessingFiles(ref TestClass tester)
        {
             tester.Close();

             tester = null;

             tester = new TestClass(100);            
        }
      }
    • 通過方法構造對象,處理後再返回一個新的對象

  • 引用方式的參數傳遞的限制

    • 參數類型與方法簽名中聲明的類型必須嚴格相同

可變數量參數
  • static int Add(params int[] values)
    {
       int sum = 0;
       for (int i = 0; i < values.Length; i++)
      {
           sum += values[i];
      }
       return sum;
    }
  • params T[] varName

    • 必須應用於方法簽名的最後一個參數

    • params關鍵字告訴編譯器向參數應用定制特性「System.ParamArrayAttribute」

      • .method private hidebysig static int32  Add(int32[] values) cil managed
        {
        .param [1]
        .custom instance void [System.Runtime]System.ParamArrayAttribute::.ctor() = ( 01 00 00 00 )
        // 程式碼大小       35 (0x23)
        //...
        } // end of method Program::Add
    • C#編譯器檢測到方法調用時,會先檢查所有具有指定名稱,同時參數沒有應用ParamArrayAttribute特性的方法

    • 如果找不到,再找應用了ParamArrayAttribute特性的方法

      • 找到後,編譯器會構造一個數組,填補元素,再生代碼調用方法

    • 該參數只能標識一維數組

    • 可為其傳null值

  • 調用可變數量參數方法對性能有所影響

    • 數組對象必須在堆上分配

    • 數組元素必須初始化

    • 數組內存需要GC

參數與返回類型的設置
  • 參數最好是設置「弱類型」,如:

    • 接口(優先考慮)

      • 實現了接口就可以當參數傳

    • 基類

  • 返回值最好是設置「強類型」

    • 派生類

      • 接收返回值時,就可以用類型本身或其基類接收

      • 返回基類,就只能用基類接收

  • 考慮協變性和逆變性

  • 更靈活、泛用

參考書目

  • 《CLR via C#》(第4版) Jeffrey Richter
  • 《深入理解C#》(第3版) Jon Skeet