← 筆記

C#筆記 – 數組

  • CLR支持一維、多維、交錯數組
  • 為了符合CLS的要求,所有數組都需要是0基數組(起始索引為0)
  • 所有數組類型隱式從System.Array抽象類派生,後者派生自System.Object
    • 數組為引用類型,在托管堆上分配內存
    • 數組元素為值類型的數組
      •   int[] intArr;
          intArr = new int[100];
      • 聲明時,intArr指向包含int值的一維數組,值為null

      • new int[100]時,為intArr分配了一個含有100個int值的數組,當中所有值被設置為0

        • 在托管堆上分配容納100個未裝箱int所需的內存塊
    • 數組元素為引用類型的數組
      •   string[] strArr;
          strArr = new string[3];
          strArr[2] = "HelloWorld";
      • 聲明時,strArr指向包含string引用的一維數組,值為null

      • new string[3]時,為strArr分配了一個含有3個string引用的數組,所有引用被設置為null

        • 由於string是引用類型,創建數組只是創建了一組引用,沒有創建實際的對象
        • 而當元素被賦值後,該索引上的元素則會指向對應的對象引用
    • 具體示意圖

      • 另外,每個數組都包含一個類型對象指針、同步塊索引和一些額外成員(overhead字段) - 這個overhead字段包含了 - 數組的秩 - 每一維的下限和長度 - 數組的元素類型
  • 二維數組/交錯數組
    • 二維數組

      •   int[,] twoD_Arr = new int[3, 4];
    • 交錯數組

      •   int[][] jaggedArr = new int[3][];  
    • 兩者差異

      • 長度指定

        • 二維數組初始化時需要指定每一維的長度
        • 交錯數組初始化時至少指定第一維的長度
      • 維度間的長度限制

        • 二維數組的每一維長度必須一致

          •   int[,] test = new int[,]
              {
                  {1, 2, 3 },
                  {1, 2, 3 },
                  {1, 2, 3 },
                  //{1, 3, 4, 5 } Length must be same
              };    
        • 交錯數組的每一維長度可以不一致

          • 交錯數組是由數組構成的數組,所以它的每一維實際上是一個數組的實例
          •   int[][] test2 = new int[3][]
              {
                  new int[]{ 1, 2, 3 },
                  new int[]{ 1, 2, 3, 4 },
                  new int[]{ 1, 2, 3, 4, 5 },
              };
      • 長度

        • 二維數組的Length是每維元素數量的乘積
          • 上例的test的長度為3 * 3 = 9
        • 交錯數組的Length是「第一維的數組實例個數」
          • 上例的test2的長度為 3
元素初始化
  •   string[] names = new string[] { "Alan", "Tom" };
      var names2 = new string[] { "Alan", "Tom" }; //使用了隱式類型,=號右邊的string[]將會告訴names2變量的類型
      var names3 = new[] { "Alan", "Tom" }; //不指定string[],編譯器直接選擇所有元素最接近的共同基類來作為數組的類型
      string[] names4 = { "Alan", "Tom" }; //初始化器語法糖,但須明確聲明數組類型
    • 大括號中以逗號分隔的數據項稱為「數組初始化器」
數組轉型
  • 只有引用類型的數組可以轉型,值類型數組不能

    • 轉型前後的維數需要一致
    • 存在從元素源類型至目標類型的隱式/顯式轉換
    •   //引用轉型,有效
        FileStream[] fs = new FileStream[10];
        object[] obj = fs;
        
        //值轉型,無效
        int[] n = new int[10];
        object[] objs = n;
  • Array.Copy

    • 如果數組因為是值類型而無法直接轉型,也可以通過Array.Copy將元素複製到另一個數組中
    • Array.Copy能正確處理內存的重疊區域
    • Array.Copy方法在複製元素時,還會進行必要的類型轉換
      •   Array.Copy(n, new object[10], 10);
      • 值類型元素裝箱/引用類型拆箱/加寬基元值類型

      • 數組複製時,如果數組類型無法驗證兼容性,如Object[] 轉型為 IFormattable[],就會對元素進行向下類型轉換

        • 只要Object[]中的每個元素都實現了IFormattable,就可以成功執行Copy
    • Array.ConstrainedCopy
      • 更可靠,不會破壞目標數組中的數據
      • 要求源數組元素類型與目標數組元素類型相同/派生自目標數組元素類型
      • 不執行任何裝/拆箱、向下類型轉換
    • System.Buffer.BlockCopy
      • 更快,只支持基元類型
      • 不提供轉型能力
      • 接受int參數,代表字節偏移量
      • 目的是按位兼容的數據從一個數組類型複製到另一個按位兼容的數組類型
數組內部原理
  • 所有數組隱式派生自System.Array
  • 所有數組隱式實現IEnumerable/ICollection/IList的非泛型接口
    • 由於涉及多維數組和非0基數組的問題,System.Array本身不會實現這些接口的泛型版本

      • 也就是說,多維數組/非0基數組不會自動實現這些接口的泛型版本
      •   FileStream[,] a = new FileStream[10, 20];            
          //a.CopyTo //沒有泛型版本
          
          FileStream[] f = new FileStream[10];
          //f.CopyTo //有泛型版本
    • CLR會在1維0基數組創建時,自動使數組實現這些接口的泛型版本

      • 如果數組元素是引用類型,還會為其基類實現這些泛型接口
        • 值類型數組不為基類實現泛型接口的原因
          • 值類型在內存的布局與引用類型不同
          • 值類型是密封的,不應有除了System.ValueType以外的基類
        • 層次:
          • Object
            • Array(非泛型IEnumerable, ICollection, IList)
              • Object[] (IEnumerable, ICollection, IList of Object)
              • string[] (IEnumerable, ICollection, IList of string)
              • Stream[] (IEnumerable, ICollection, IList of Stream)
                • FileStream[] (IEnumerable, ICollection, IList of FileStream)
  • 傳遞和返回
    • 數組作為實參傳遞時,傳的是對數組的引用
      • 被調用的方法可以修改數組中的元素
      • 如果不想被修改,需要生成數組的Copy並將Copy傳給方法
        • Array.Copy執行的淺Copy
          • 如果數組元素是引用類型,新數組仍然是指向當前對象
    • 數組被返回時,如果返回的也是其返回數組的引用
      • 如果返回的是一個內部維護的數組,該數組將可被外部所修改
  • 非0基數組
    • 使用CreateInstance動態創建
      • 可指定數組元素的類型/維數/每維下限/每維元素數目
      • CreateInstance為數組分配內存,將參數信息保存至overload部分,返回數組引用
        • 如果數組維數為2維或以上,可以轉型為ElementType[]來簡化訪問,否則必須通過GetValue/SetValue來訪問數組元素
    •   int[] lowerBound = { 1, 10001 };
        int[] length = { 4, 5 };
        var ins = (int[,])Array.CreateInstance(typeof(int), length, lowerBound);
        
        Console.WriteLine(ins.GetLowerBound(0)); //1
        Console.WriteLine(ins.GetUpperBound(0)); //4
        
        Console.WriteLine("++++++++++++++++++++++++++++");
        
        Console.WriteLine(ins.GetLowerBound(1)); //10001
        Console.WriteLine(ins.GetUpperBound(1)); //10005
CLR內部的兩種數組
  •   Array arr;
      
      //一維0基
      arr = new string[0];
      Console.WriteLine(arr.GetType()); //System.String[]
      
      //一維0基
      arr = Array.CreateInstance(typeof(string), new int[]{ 0 }, new int[]{ 0 });
      Console.WriteLine(arr.GetType()); //System.String[]
      
      //一維非0基
      arr = Array.CreateInstance(typeof(string), new int[] { 0 }, new int[] { 1 });
      Console.WriteLine(arr.GetType()); //System.String[*]
      
      Console.WriteLine();
      
      //二維0基
      arr = new string[0, 0];
      Console.WriteLine(arr.GetType()); //System.String[,]
      
      //二維0基
      arr = Array.CreateInstance(typeof(string), new int[]{ 0, 0 }, new int[]{ 0, 0 });
      Console.WriteLine(arr.GetType()); //System.String[,]
      
      //二維非0基
      arr = Array.CreateInstance(typeof(string), new int[] { 0, 0 }, new int[] { 1, 1 });
      Console.WriteLine(arr.GetType()); //System.String[,]
  • SZ數組(一維0基)

    • SZ數組的訪問往往更快
      • 有一些IL指令專用於處理SZ數組,並導致JIT生成優化代碼
      • JIT能將索引範圍檢查代碼從循環中取出,使之只執行一次
        •   int[] a = new int[5];
            for (int i = 0; i < a.Length; i++)
            {
                Console.WriteLine(a[i]);
            }
        • JIT知道for要訪問0 ~ Length - 1的數組元素,因此會生成代碼來檢查 (0 >= a.GetLowerBound(0)) && ((Length - 1) <= a.GetUpperBound(0))

          • 這個檢查在循環之前發生,不會再在循環內部生成代碼驗證
          • 而非0基/多維數組則需要在每次訪問時進行驗證
          • 建議使用交錯數組來取代多維數組
        • 另外可以注意的是,JIT知道Length是Array類對象的屬性(方法調用),因此只會調用該屬性一次並把結果存到一個臨時變量裡,循環檢查的也是這個臨時變量,加快了JIT編譯的速度

          • 不需要用局部變量來緩存數組的Length值,直接調用Length即可
  • 非0基的一/多維數組

    • 1維非0基數組的類型為 string[*]
      • *符號表明CLR知道該數組不是0基的
      • C#不允許聲明string[*]的變量,因此,不能使用C#語法來訪問非0基數組,只能使用Array.GetValue/SetValue來訪問,但是速度較慢
    • 二維0基和非0基數組的類一致 => string[,]
      • CLR將所有多維數組視為非0基的

參考書目

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