C#筆記 – 類型基礎(三) 基元類型

基元類型
  • 基元類型為編譯器直接支持的數據類型(primitive type)

    • 基元類型直接映射到FCL中存在的類型

    • 如整數的聲明,int直接映射到System.Int32,兩者產生的IL完全一致

      • //沒有基元類型
        System.Int32 a = new System.Int32();
        //基元類型
        int a = 0;
    • 下表列出了C#基元類型對應的FCL類型,只要符合CLS的類型,其他語言都提供了類似的基元類型。

      • C#基元類型FCL類型符合CLS
        sbyteSystem.SByte
        byteSystem.Byte
        shortSystem.Int16
        ushortSystem.UInt16
        intSystem.Int32
        uintSystem.UInt32
        longSystem.Int64
        ulongSystem.UInt64
        charSystem.Char
        floatSystem.Single
        doubleSystem.Double
        boolSystem.Boolean
        decimalSystem.Decimal
        stringSystem.String
        objectSystem.Object
        dynamicSystem.Object
  • 雖然基元類型之間屬於不同的類型,相互之間沒有任何繼承關係,但是C#編譯器仍然會在編譯這些代碼時應用自己的特殊規則,並生成必要的IL。如:

    • 基元類型間的隱式或顯式轉換:

      • Int32 i = 5; //int32 => int32
        Int64 l = i; //int32 => int64
      • 只有在轉換「安全」(不會發生數據丟失)時,C#才允許隱式轉型。否則必須顯示轉型

    • 字面值可被看成是基元類型本身的實例

      • Console.WriteLine(123.ToString());
      • 如果表達式由字面值構成,編譯器在編譯時就能完成表達式的求值

dynamic基元類型
  • 類型標記dynamic是為了方便開發人員使用反射或與其他組件通信而存在的,代碼使用dynamic表達式/變量調用成員時,編譯器生成特殊IL代碼來描述所需的操作(payload

    • 根據dynamics表達式/變量引用的對象的實際類型來決定具體執行的操作
    • public static void Main()
      {
         dynamic value;
         for(int i = 0; i < 2; i++)
        {
             value = (i == 0) ? (dynamic)5 : (dynamic)"A";
             value = value + value;
             M(value);
        }
      }

      static void M(int n) { Console.WriteLine("M(int): " + n); }
      static void M(string s) { Console.WriteLine("M(string): " + s); }
    • 在該例子中,i = 0時會輸出10;i = 1時會輸出AA

      • +操作符兩邊的變量(value)的類型都是dynamic,因此,C#編譯器生成payload,使之在運行時檢查value的實際類型,決定+操作符是要進行「算術相加」還是「字符串連接」,並根據確定下來的類型調用M方法對應的重載版本

  • 如果字段/參數/返回值的類型是dynamic,編譯器會將他們轉換成System.Object,並在元數據中向它們應用System.Runtime.CompilerServices.DynamicAttribute實例。

    • class Program
      {
         static dynamic test;
         static void Main(string[] args)
        {
      ...
        }
      }
    • .field private static object test
      .custom instance void [System.Linq.Expressions]System.Runtime.CompilerServices.DynamicAttribute::.ctor() = ( 01 00 00 00 )
    • 如果局部變量被指定為dynamic,則其類型會成為Object,但不會應用DynamicAttribute,因為它只會在方法內部使用

      • class Program
        {
           static void Main(string[] args)
          {
               dynamic test2 = 10;
          }
        }
      • .method private hidebysig static void  Main(string[] args) cil managed
        {
        .entrypoint
        // 程式碼大小       16 (0x10)
        .maxstack 1
        .locals init (object V_0)
        IL_0000: nop
        IL_0001: ldc.i4.s   10
        IL_0003: box       [System.Runtime]System.Int32
        IL_0008: stloc.0
        IL_0009: call       string [System.Console]System.Console::ReadLine()
        IL_000e: pop
        IL_000f: ret
        } // end of method Program::Main
    • 同理,由於在編譯時dynamic == object,因此方法簽名不能僅靠dynamic和object的變化來區別,如:無法創建這樣的重載:

      • public void Hello(object obj){ ... }
        public void Hello(dynamic dy){ ... }
    • 泛型類型實參也可以是dynamic。同樣,編譯器把dynamic轉成object,向必要的元數據應用DynamicAttribute。

      • 由於泛型代碼是已經編譯好的,會將類型視為Object,編譯器不會在泛型代碼中生成payload代碼,因此不會執行動態調度

    • 所有表達式都能隱式轉型為dynamic,因為所有表達式最終都生成從Object派生的類型

      • 雖然編譯器不允許將表達式從Object隱式轉型為其他類型,但是卻可以從dynamic隱式轉型為其他類型

      • 但CLR依然會在運行時驗證轉型

    • dynamic != var

      • var依然要求編譯器根據表達式推斷具體數據類型,而且只能在方法內部聲明局部變量;dynamic可用於局部變量、字段和參數

      • 表達式不能被轉型為var;可以轉成dynamic

      • var需要顯式初始化;dynamic不用

    • 雖然能定義對Object的擴展方法,但是不能定義對dynamic的擴展方法;也不能把匿名方法/lambad表達式當作參數傳給dynamic方法調用,因為編譯器無法推斷要使用的類型

  • dynamic的一個重要功能就是簡化反射的使用,如下例:

    • 反射:

      • public static void Main()
        {
           Object target = "Dante";
           Object arg = "ff";

           Type[] argTypes = new Type[] { arg.GetType() };
           MethodInfo method = target.GetType().GetMethod("Contains", argTypes);

           Object[] arguments = new Object[] { arg };
           bool result = Convert.ToBoolean(method.Invoke(target, arguments));

           Console.WriteLine(result);
        }
    • dynamic:

      • public static void Main()
        {
           dynamic target = "Alan Draft";
           dynamic arg = "ff";
           bool result = target.Contains(arg);

           Console.WriteLine(result);
        }
    • C#生成的payload,使用了運行時綁定器(runtime binder),在Microsoft.CSharp.dll程序集中

      • 在運行時,該程序集必須被加載到AppDomain中,會損害性能,增大內存消耗

        • 該程序集還會加載System.dll和System.Core.dl

        • 如果要和COM組件互操作,還會加載System.Dynamic.dll

      • payload執行時,會在運行時生成動態代碼;這些代碼進入駐留內存的程序集(Anonymously Hosted DynamicMethods Assembly),作用是當特定callsite(發出調用的地方)使用具有相同運行時類型的動態實參發出大量調用時增強動態調度性能

      • 因此,如果程序中只是少數地方需要動態行為,使用反射可能會更高效

      • 運行時,runtime binder會檢查類型是否實現了IDynamicMetaObjectProvider接口

        • 如果是,就調用其GetMetaObject方法,該方法返回一個DynamicMetaObject的派生類,由該類型處理對象成員、方法、操作符的綁定

        • 如果不是,C#編譯器會將對象視為用C#定義的普通類型實例,利用反射在對象上執行操作

checked 和 unchecked基元類型操作
  • 對基元類型的算術運算可能造成溢出(值超過了類型可容納的大小)

    • Byte b = 100;
      b = (Byte)(b + 200); //b = 44
    • 溢出時一般有兩種處理

      • 讓值「回滾」到其最小值

      • 拋出異常

    • 在代碼的特定區域中,可以使用checked 和 unchecked操作符

      • UInt32 invalid = unchecked((UInt32)(-1)); //不檢查溢出,會進行回滾
        Byte b = 100;
        b = checked((Byte)(b + 200)); //(b+200)會被自動轉換成一個int32,然後再嘗試把300強轉為Byte,使用checked檢查溢出,拋出異常
        b = (Byte)checked(b + 200); //(b+200)自動轉成一個int32,然後checked int32 b = 300通過,再轉成Byte,發生回滾
      • checked唯一的作用就是決定生成哪個版本的算術運算/數據轉換的IL指令,因此在checked裡面調用方法,不會對方法造成任何影響

  • 避免數據溢出:

    • 盡量使用有符號的值類型,允許編譯器檢測更多的上溢/下溢錯誤。

    • checked塊和OverflowException異常捕捉結合使用

    • 把允許溢出的代碼寫在unchecked塊中

  • Decimal的特殊性

    • 雖然C#將Decimal視為基元類型,但CLR並沒有,因此CLR沒有知道如何處理Decimal值的IL指令

    • Decimal類型提供了一系列的public static方法,如Add/Substract等;以及一系列的操作符重載

    • 編譯使用了Decimal值的程序時,編譯器會生成代碼來調用Decimal的成員,通過這些成員來執行實際運算,因此Decimal的處理速度慢於CLR基元類型的值

    • 另一方面,由於沒有IL指令來處理Decimal值,checked和unchecked都失去了作用,如果對Decimal的運算不安全,肯定會拋出錯誤

參考書目

  • 《CLR via C#》(第4版) Jeffrey Richter