陣列(ArrayPool陣列池、Span<T>結構)

小世界的野孩子發表於2019-07-26

前言

  如果需要使用相同的型別的多個物件,就可以使用集合和陣列,這一節主要講解陣列,其中會重點涉及到Span<T>結構和ArrayPool陣列池。我們也會先涉及到簡單的陣列、多維陣列、鋸齒陣列、Array類。

簡單的陣列、多維陣列、鋸齒陣列

  簡單的陣列介紹

  陣列的宣告:

Int [] myArray;

  初始化:

myArray=new int[4];

  還可以:

Int [] myArray=new int []{1,2,3,4};

  訪問陣列:

myArray[0];

 

  多維陣列介紹

  一般的陣列(也稱一維陣列)是用一個數字來索引,多維陣列用兩個或兩個以上的數字進行索引。

  宣告多維陣列時中間以,隔開,我們下面宣告一個二維陣列。

 int  [,]  twodim=new  int [3,3]
 int[,] twodim = {
                { 1,2,3},
                { 4,5,6},
                { 7,8,9}
            };

 

  一個三維陣列。

int[,,] threedim = {
                { { 1,2},{ 3,4} },
                {{ 5,6},{ 7,8} },
                { { 9,10},{ 11,12} }
            };
 Console.WriteLine(threedim[0,1,1]);

 

  鋸齒陣列

  二維陣列圖形:

1

2

3

4

5

6

7

8

9

 

  

 

 

 

  鋸齒陣列

1

2

3

4

5

6

7

8

9

 

 

 

 

 

  在宣告鋸齒陣列的時候要依次放置左右括號。在初始化鋸齒陣列時,只對第一對方括號中設定該陣列包含的行數,定義各行中元素個數的第二個方括號設為空,因為這類陣列的每一行包含不同的元素個數。

   int[][] jagged = new int[3][];
    jagged[0] = new int[2] { 1, 2 };
    jagged[1] = new int[4] { 3, 4, 5, 6 };
    jagged[2] = new int[3] { 7, 8, 9 };

 

Array類

  建立陣列:

Array intArray1 = Array.CreateInstance(typeof(int), 5);
for (int i = 0; i < intArray1.Length; i++)
    {
         intArray1.SetValue(33, i);
  }

  上面這段程式碼,描述了Array陣列的建立以及設定值。CreateInstance()方法第一個引數為元素的型別,第二個引數為定義陣列的大小。SetValue()方法設定值第一個引數為設定IDE值,第二個引數為設定的索引。

  複製陣列:

int[] intArray1 = { 1,2};
int[] intArray2 = (int[])intArray1.Clone();

  因為陣列是引用型別的,所以將一個陣列的變數賦予另一個陣列變數,就會得到兩個引用同一個陣列的變數,這是使用的是Clone()方法建立陣列的淺表副本。使用Copy()方法也可以建立淺表副本,Clone()方法會建立一個陣列,而Copy()方法必須傳遞階數相同且有足夠元素的已有陣列。

  排序:

class Program
    {
        static void Main(string[] args)
        {
            int[] list = { 34, 72, 13, 44, 25, 30, 10 };

 
            Console.Write("原始陣列: ");
            foreach (int i in list)
            {
                Console.Write(i + " ");
            }
            Console.WriteLine();

 
            // 逆轉陣列
            Array.Reverse(list);
            Console.Write("逆轉陣列: ");
            foreach (int i in list)
            {
                Console.Write(i + " ");
            }
            Console.WriteLine();

 
            // 排序陣列
            Array.Sort(list);
            Console.Write("排序陣列: ");
            foreach (int i in list)
            {
                Console.Write(i + " ");
            }
            Console.WriteLine();
        }


    }

 

  輸出:

原始陣列: 34 72 13 44 25 30 10

逆轉陣列: 10 30 25 44 13 72 34

排序陣列: 10 13 25 30 34 44 72

  在上述方法中Array.Sort()方法實現了陣列的排序,而Array.Reverse()實現了陣列的逆轉 。

ArrayPool陣列池

  接下來重點來了,本文的重點一,ArrayPool陣列池。如果一個應用需要建立和銷燬許多的陣列,垃圾收集器就要花費很多的功夫來做這些工作,為了較少垃圾收集器的工作,這裡我們可以使用ArrayPool類來使用陣列池。ArrayPool管理一個陣列池,陣列可以再這裡租借記憶體,並且返回到這裡。需要引用using System.Buffers;

  建立陣列池:

ArrayPool<int> arrayPool = ArrayPool<int>.Create(maxArrayLength:40000, maxArraysPerBucket: 10);

  maxArrayLength的預設值是1024*10224位元組(陣列的長度),maxArraysPerBucket預設值是50(陣列的數量)。

  這裡還可以使用以下方法來使用預定義的共享池。

ArrayPool<int> sharePool = ArrayPool<int>.Shared;

  下面我們就一起看看如何去使用這個陣列池吧:

 

class Program
    {
        static void Main(string[] args)
        {
            //定義陣列池
            ArrayPool<int> arrayPool = ArrayPool<int>.Create(maxArrayLength: 10000, maxArraysPerBucket: 10);

            //定義使用陣列的長度
            int arrayLenght = 5;
            int[] array = arrayPool.Rent(arrayLenght);

 
            //輸出陣列的長度
            Console.WriteLine($"定義陣列長度:{arrayLenght},實際陣列長度:{array.Length}");
 

            //對陣列進行賦值
            array[0] = 0;
            array[1] = 1;
            array[2] = 2;
            array[3] = 3;
            array[4] = 4;

            
            //輸出陣列的值
            foreach (var item in array)
            {
                Console.WriteLine(item);
            }

            //將記憶體返回給陣列池,clearArray設定True清除陣列,下次呼叫時為空,設定False,保留陣列,下次呼叫還是現在的值
            arrayPool.Return(array, clearArray: true);
            foreach (var item in array)
            {
                Console.WriteLine(item);
            }

        }
    }

 

  在上面事例中,我們使用Rent()方法請求池中的記憶體,Rent方法返回一個陣列,其中至少包含所請求的元素個數。返回的陣列可能會用到更多的記憶體,池中最少的請求為16個元素,緊接著是32,64,128以此類推。所以在上述例子中我們請求的長度為5,但是實際使用的元素個數為16個,多餘的將根據型別對其賦值0或者null。

  我們使用Return()方法將陣列返回到池中,這裡使用了一個可選引數clearArray,指定是否清除該陣列,不清除的話下一個從池中租用這個陣列的人可以讀取到其中的資料。清除資料可以避免這種情況,但是會消耗更多的CPU時間。

Span<T>

  Span<T>介紹

  為了快速訪問託管或非託管的連續記憶體,可以使用Spam<T>結構。一個可以使用Span<T>結構的例子就是陣列,Span<T>結構在後臺儲存在連續的記憶體中,另一個例子就是長字串。

  使用Span<T>結構,可以直接訪問陣列元素。陣列的元素沒有複製,但是它們可以直接呼叫,並且比複製還快。

class Program
    {
        static void Main(string[] args)
        {
            int[] arr1 = { 3, 5, 7, 9, 11, 13, 15, 18, 19 };
            var span1 = new Span<int>(arr1);
            span1[1] = 11;
            Console.WriteLine(arr1[1]);
        }
    }

  輸出:

 

 

  這裡將建立的arr1陣列傳遞給Span<T>,同時Span<T>型別提供了一個索引器,這裡直接修改span1的第二個值,然後再輸出arr1陣列中的第二個值,也是被其修改過得值。

  Span<T>切片

  Span<T>它一個強大的特性是,可以使用它訪問陣列的部分或者切片,使用切片的時候不會複製陣列元素,他們是從Span中直接訪問的。下面程式碼介紹了建立切片的兩種方法:

class Program
    {
        static void Main(string[] args)
        {
            //定義簡單的陣列
            int[] arr2 = { 3, 5, 7, 9, 11, 13, 15, 18, 19, 20, 30, 40, 50, 60 };

            
            //Span<T>對陣列進行切片,訪問arr2陣列,從第三個開始,取長度6個的一個陣列。
            var span3 = new Span<int>(arr2, start: 3, length: 6);
 

            //輸出切片中的值
            foreach (var item in span3)
            {
                Console.WriteLine(item);
            }
            Console.WriteLine("");
            Console.WriteLine("");
            Console.WriteLine("");


            //對span3進行切片處理,從第二個開始,去長度4個的一個陣列
            var span4 = span3.Slice(start: 2, length: 4);

            foreach (var item in span4)
            {
                Console.WriteLine(item);
            }
        }
    }

 

  輸出:

 

 

  使用Span<T>改變值

  前面介紹瞭如何使用Span<T>的索引器,更改陣列的元素,下面介紹的將會有更多的選項,關於修改元素的值及複製。

class Program
    {
        static void Main(string[] args)
        {
            //定義簡單的陣列
            int[] arr = { 3, 5, 7, 9, 11, 13, 15, 18, 19, 20, 30, 40, 50, 60 };

            
            //將陣列傳遞給span
            var span = new Span<int>(arr);
            var span2 = new Span<int>(arr);

 

            //對span進行切片處理,從第四個開始
            var span3 = span.Slice(start: 4 );
 

            //呼叫clear方法,用0填充span3
            span3.Clear();
            foreach (var item in span3)
            {
                Console.WriteLine("span3的值:"+item);
            }
            Console.WriteLine("span3的長度:"+span3.Length);

            //建立新的切片span4,從span2開始,長度3
            Span<int> span4 = span2.Slice(start: 3, length: 3);


            //呼叫Fill方法,用傳入的值填充span4
            span4.Fill(42);

 
            foreach (var item in span4)
            {
                Console.WriteLine("span4的值"+item);
            }
            Console.WriteLine("span4的長度"+span4.Length);

            //將span4複製給span,複製失敗
            span4.CopyTo(span);


            //將span複製給span3 複製失敗
            if (!span.TryCopyTo(span3))
            {
                Console.WriteLine("複製不了");
            }
        }
    }

 

  輸出:

 

 

  上面事例中,顯示呼叫clear()方法,該方法用0填充Span,然後呼叫了Fill()方法,該方法用傳遞給Fill方法的值來填充Span,同時也可以將一個Span<T>複製給另一個Span<T>,這裡先是採用的CopyTo,在這個方法中,如果另一個目標span不夠大,就會複製失敗,這裡可以使用TryCopyTo來優化此功能,如果目標不夠大,將會返回false。以此來判斷是否複製成功。上面例子中span4長度為3,而span長度為14,這裡是複製成功了,然後其下面的操作,因為span3的長度是10,span複製給span3失敗了。因為span3不夠大。

  上面事例中提供了改變值的一些方法,當我們不需要對值進行改變,只需要對陣列進行讀訪問的時候,我們可以使用ReadOnlySpan<T>。這裡定義了只讀的Span

 ReadOnlySpan<int> readonlySpan = new ReadOnlySpan<int>(arr);

 

總結

  在本篇文章中,重點介紹了ArrayPool陣列池和Span<T>結構,通過使用陣列池,來降低陣列建立和銷燬時消耗的效能,減少垃圾回收器的工作,使用Span<T>可以快速的訪問託管及非託管程式碼,建立切片來對陣列和長字串進行一定的操作。更加高效的運算元組。

 

  青少年是一個美好而又是一去不可再得的時期,是將來一切光明和幸福的開端。——加裡寧

 


 

                      c#基礎知識詳解系列

 

  歡迎大家掃描下方二維碼,和我一起學習更多的C#知識

 

 

 

  

 

相關文章