C#集合--數組

Array類是全部一維和多維數組的隱式基類,同時也是實現標準集合接口的最基本的類型。Array類實現了類型統一,所以它爲全部數組提供了一組通用的方法,不論這些數組元素的類型,這些通用的方法均適用。算法

正由於數組如此重要,因此C#爲聲明數組和初始化數組提供了明確的語法。在使用C#語法聲明一個數組時,CLR隱式地構建Array類--合成一個僞類型以匹配數組的維數和數組元素的類型。並且這個僞類型實現了generic集合接口,好比IList<string>接口。數組

CLR在建立數組類型實例時會作特殊處理--在內存中爲數組分配連續的空間。這就使得索引數組很是高效,但這卻阻止了對數組的修改或調正數組大小。dom

Array類實現了IList<T>接口和IList接口。Array類顯示地實現了IList<T>接口,這是爲了保證接口的完整性。可是在固定長度集合好比數組上調用IList<T>接口的Add或Remove方法時,會拋出異常(由於數組實例一旦聲明以後,就不能更改數組的長度)。Array類提供了一個靜態的Resize方法,使用這個方法建立一個新的數組實例,而後複製當前數組的元素到新的實例。此外,在程序中,當在任何地方引用一個數組都會執行最原始版本的數組實例。所以若是但願集合大小能夠調整,那麼你最好使用List<T>類。post

數組元素能夠是值類型也能夠是引用類型。值類型數組元素直接存在數組中,好比包含3個整數的數組會佔用24個字節的連續內存。而引用類型的數組元素,佔用的空間和一個引用佔用的空間同樣(32位環境中4個字節,64爲環境中8個字節)。咱們先來看下面的代碼:性能

int[] numbers =new int[3];
numbers[0] =1;
numbers[1] = 7;

StringBuilder[] builders = new StringBuilder[5];
builders[0] = new StringBuilder("Builder1");
builders[1] = new StringBuilder("Builder2");
builders[2] = new StringBuilder("Builder3");

對應的內存分配變化以下面幾張圖所示:ui

image

執行完int[] numbers=new int[3]以後,在內存中分配了8×3=24個字節,每一個字節都爲0。this

image

執行完numbers[0]=1以後,數組聲明後的第一個8字節變爲00 00 00 01。spa

image 

一樣地,執行完numbers[1]=7以後,第二段8個字節變爲00 00 00 07。設計

對應引用類型的數組,咱們用一張圖來講明內存分配:3d

image

看起來分複雜,其實內存分配示意圖以下

image

 

經過Clone方法能夠克隆一個數組,好比arrayB = arrayA.Clone()。可是,克隆數組執行淺拷貝,也就是說數組本身包含的那部份內容纔會被克隆。簡單說,若是數組包含的是值類型對象,那麼克隆了這些對象的值。而數組的子元素是引用類型,那麼僅僅克隆引用類型的地址。

StringBuilder[] builders2 = builders;
ShtringBuilder[] shallowClone = (StringBuilder[])builders.Clone();

與之對應的內存分配示意圖:

image

若是須要執行深拷貝,即克隆引用類型的子對象;那麼你須要遍歷數組並手動的克隆每一個數組元素。深克隆規則也適用於.NET其餘集合類型。

儘管數組在設計時,主要使用32位的索引,它也在必定程度上支持64位索引,這須要使用那些既接收Int32又接收Int64類型參數的方法。這些重載方法在實際中並無意義,由於CLR不容許任何對象--包括數組在內--的大小超過2G(不管32位的系統仍是64位的系統)

 

構造數組和索引數組

建立數組和索引數組的最簡單方式就是經過C#語言的構建器

Int[] myArray={1,2,3};
int first=myArray[0];
int last = myArray[myArray.Length-1];

或者,你能夠經過調用Array.CreateInstance方法動態地建立一個數組實例。你能夠經過這種方式指定數組元素的類型和數組的維度。而GetValue和SetValue方法容許你訪問動態建立的數組實例的元素。

Array a = Arrat.CreateInstance(typeof(string), 2);
a.SetValue('hi", 0);
a.SetValue("there",1);
string s  = (string)a.getValue(0);

string[] cSharpArray = (string[])a;
string s2  = cSharpArray[0];

動態建立的零索引的數組能夠轉換爲一個匹配或兼容的C#數組。好比,若是Apple是Fruit的子類,那麼Apple[]能夠轉換成Fruit[]。這也是爲何object[]沒有做爲統一的數組類型,而是使用Array類;答案在於object[]不只與多維數組不兼容,並且還與值類型數組不兼容。所以咱們使用Array類做爲統一的數組類型。

GetValue和SetValue對編譯器生成的數組也起做用,若想編寫一個方法處理任何類型的數組和任意維度的數組,那麼這兩個方法很是有用。對於多維數組,這兩個方法能夠把一個數組看成索引參數。

public object GetValue(params int[] indices)
public void SetValue(object value, params int[] indices)

下面的方法在屏幕打印任意數組的第一個元素,不管數組的維度

void WriteFirstValue (Array a)
{
Console.Write (a.Rank + "-dimensional; ");
 
int[] indexers = new int[a.Rank];
Console.WriteLine ("First value is " + a.GetValue (indexers));
}
void Demo()
{
int[] oneD = { 1, 2, 3 };
int[,] twoD = { {5,6}, {8,9} };
WriteFirstValue (oneD); // 1-dimensional; first value is 1
WriteFirstValue (twoD); // 2-dimensional; first value is 5
}

在使用SetValue方法時,若是元素與數組類型不兼容,那麼會拋出異常。

不管採起哪一種方式實例化一個數組以後,那麼數組元素自動初始化了。對於引用類型元素的數組而言,初始化數組元素就是把null值賦給這些數組元素;而對於值類型數組元素,那麼會把值類型的默認值賦給數組元素。此外,調用Array類的Clear方法也能夠完成一樣的功能。Clear方法不會影響數組大小。這和常見的Clear方法(好比ICollection<T>.Clear方法)不同,常見的Clear方法會清除集合的全部元素。

 

遍歷數組

經過foreach語句,能夠很是方便地遍歷數組:

int[] myArray = { 1, 2, 3};
foreach (int val in myArray)
    Console.WriteLine (val);

你還可使用Array.ForEach方法來遍歷數組

public static void ForEach<T> (T[] array, Action<T> action);

該方法使用Action代理,此代理方法的簽名是(接收一個參數,不返回任何值):

public delegate void Action<T> (T obj);

下面的代碼顯示瞭如何使用ForEach方法

Array.ForEach (new[] { 1, 2, 3 }, Console.WriteLine);

你可能會很好奇Array.ForEach是如何執行的,它就是這麼執行的

public static void ForEach<T>(T[] array, Action<T> action) {
    if( array == null) {
        throw new ArgumentNullException("array");
    }

    if( action == null) {
        throw new ArgumentNullException("action");
    }
    Contract.EndContractBlock();

    for(int i = 0 ; i < array.Length; i++) {
        action(array[i]);
    }
}

在內部執行for循環,並調用Action代理。在上面的實例中,咱們調用Console.WriteLine方法,因此能夠在屏幕上輸出1,2,3。

 

獲取數組的長度和維度

Array提供了下列方法或屬性以獲取數組的長度和數組的維度:

public int GetLength (int dimension);
public long GetLongLength (int dimension);

public int Length { get; }
public long LongLength { get; }

public int GetLowerBound (int dimension);
public int GetUpperBound (int dimension);

public int Rank { get; }
GetLength和GetLongLength返回指定維度的長度(0表示一維數組),Length和LongLength返回數組中全部元素的總數(包含了全部維度)。

GetLowerBound和GetUpperBound對於多維數組很是有用。GetUpperBound返回的結果等於指定維度的GetLowerBound+指定維度的GetLength

 

搜索數組

Array類對外提供了一系列方法,以在一個維度中查找元素。好比:

  • BinarySearch方法:在一個排序後的數組中快速找到指定元素;
  • IndexOf/LastIndex方法:在未排序的數組中搜索指定元素;
  • Find/FindLast/FindIndex/FindLastIndex/FindAll/Exists/TrueForAll方法:根據指定的Predicated<T>(代理)在未排序的數組中搜索一個或多個元素。

若是沒有找到指定的值,數組的這些搜索方法不會拋出異常。相反,搜索方法返回-1(假定數組的索引都是以0開始),或者返回generic類型的默認值(int返回0,string返回null)。

二進制搜索速度很快,可是它僅僅適用於排序後的數組,並且數組的元素是根據大小排序,而不是根據相等性排序。正由於如此,因此二進制搜索方法能夠接收IComparer或IComparer<T>對象以對元素進行排序。傳入的IComparer或IComparer<T>對象必須和當前數組所使用的排序比較器一致。若是沒有提供比較器參數,那麼數組會使用默認的排序算法。

IndexOf和LastIndexOf方法對數組進行簡單的遍歷,而後根據指定的值返回第一個(或最後一個)元素的位置。

以判定爲基礎(predicate-based)的搜索方法接受一個方法代理或lamdba表達式判斷元素是否知足「匹配」。一個判定(predicate)是一個簡單的代理,該代理接收一個對象並返回bool值:

public delegate bool Precicate<T>(T object);

下面的例子中,咱們搜索字符數組中包含字母A的字符:

static void Main(string[] args)
{
    string[] names = { "Rodney", "Jack", "Jill" };
    string match = Array.Find(names, ContainsA);
    Console.WriteLine(match);

    Console.ReadLine();
}

static bool ContainsA(string name)
{
    return name.Contains("a");
}

上面的代碼能夠簡化爲:

static void Main(string[] args)
{
    string[] names = { "Rodney", "Jack", "Jill" };
    string match = Array.Find(names, delegate(string name) { return name.Contains("a"); });
    Console.WriteLine(match);

    Console.ReadLine();
}

若是使用lamdba表達式,那麼代碼還能夠更簡潔:

static void Main(string[] args)
{
    string[] names = { "Rodney", "Jack", "Jill" };
    string match = Array.Find(names, name=>name.Contains("a"));
    Console.WriteLine(match);

    Console.ReadLine();
}

FindAll方法則從數組中返回知足斷言(predicate)的全部元素。實際上,該方法等同於Enumerable.Where方法,只不過數組的FindAll是從數組中返回匹配的元素,而Where方法從IEnumerable<T>中返回。

若是數組成員知足指定的斷言(predicate),那麼Exists方法返回True,該方法等同於Enumerable.Any方法。

因此數組的全部成員都知足指定的斷言(predicate),那麼TrueForAll方法返回True,該方法等同於Enumerable.All方法。

 

對數組排序

數組有下列自帶的排序方法:

public static void Sort<T>(T[] array);
public static void Sort(Array array);
public static void Sort(TKey, TValue)(TKey[] keys, TValue[] items);
public static void Sort(Array keys[], Array items);

上面的方法都有重載的版本,重載方法接受下面這些參數:

  • int index,從指定索引位置開始排序
  • int length,從指定索引位置開始,須要排序的元素的個數
  • ICompare<T> comparer,用於排序決策的對象
  • Comparison<T> comparison,用於排序決策的代理

下面的代碼演示瞭如何實現一個簡單的排序:

static void Main(string[] args)
{
    int[] numbers = { 3,2,1};
    Array.Sort(numbers);
    foreach (int number in numbers)
        Console.WriteLine(number);

    Console.ReadLine();
}

Sort方法還能夠接收兩個兩個數組類型的參數,而後基於第一個數組的排序結果,對每一個數組的元素進行排序。下面的例子中,數字數組和字符數組都按照數字數組的順序進行排序。

static void Main(string[] args)
{
    int[] numbers = { 3,2,1};
    string[] names = { "C", "B", "E" };
    Array.Sort(numbers, names);
    foreach (int number in numbers)
        Console.WriteLine(number); // 1, 2,3

    foreach (string name in names)
        Console.WriteLine(name); // E, B, C

    Console.ReadLine();
}

Array.Sort方法要求數組實現了IComparer接口。這就是說C#的大多數類型均可以排序。若是數組元素不能進行比較,或你但願重載默認的排序,那麼你須要在調用Sort方法時,需提供自定義的Comparison。因此自定義排序算法有下面兩種實現方式:

1)經過一個幫助對象實現IComparer或IComparer<T>接口

public static void Sort(Array array, IComparer comparer)
public static void Sort<T>(T[] array, System.Collections.Generic.IComparer<T> comparer)

2)經過Comparison接口

public static void Sort<T>(T[] array, Comparison<T> comparison)

Comparison代理遵循IComparer<T>.CompareTo語法:

public delegate int Comparison<T> (T x, T y);
若是x的位置在y以前,那麼返回-1;若是x在y以後,返回1,若是位置相同,那麼返回0。

咱們來看一下Array的Sort<T>(T[]array, Comparison<T> comparison)方法的源代碼:

public static void Sort<T>(T[] array, Comparison<T> comparison) {
    ......

    IComparer<T> comparer = new FunctorComparer<T>(comparison);
    Array.Sort(array, comparer);
}

由此,可內部,Comparison<T>轉換成IComparer<T>,所以在實際中,須要實現自定義排序時,若是須要考慮性能,那麼推薦使用第一種方式。此外,咱們分析Sort<T>(T[] array, System.Collections.Generic.IComparer<T> comparer)的源代碼,

public static void Sort<T>(T[] array, int index, int length, System.Collections.Generic.IComparer<T> comparer)
{
    ...

    if (length > 1)
    {
        if (comparer == null || comparer == Comparer<T>.Default)
        {
            if (TrySZSort(array, null, index, index + length - 1))
            {
                return;
            }
        }

        ArraySortHelper<T>.Default.Sort(array, index, length, comparer);
    }
}

咱們能夠看到,首先嚐試調用調用非託管代碼的Sort方法,若是成功排序,直接返回。不然調用非託管代碼(C#的ArraySortHelper)的Sort方法進行排序:

public void Sort(T[] keys, int index, int length, IComparer<T> comparer)
{
    …
try { if (comparer == null) { comparer = Comparer<T>.Default; } if (BinaryCompatibility.TargetsAtLeast_Desktop_V4_5) { IntrospectiveSort(keys, index, length, comparer); } else { DepthLimitedQuickSort(keys, index, length + index - 1, comparer, IntrospectiveSortUtilities.QuickSortDepthThreshold); } } catch (IndexOutOfRangeException) { IntrospectiveSortUtilities.ThrowOrIgnoreBadComparer(comparer); } catch (Exception e) { throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_IComparerFailed"), e); } }

若是有興趣,能夠繼續分析IntrospectiveSort方法和DepthLimitedQuickSort,不過MSDN已經給出了總結,

image

意識是說,排序算法有三種:

  • 若是分區大小小於16,那麼使用插入排序算法
  • 若是分區的大小超過2*LogN,N是數組的範圍,那麼使用堆排序
  • 其餘狀況,則使用快排

 

反轉數組的元素

使用下面的方法,能夠反轉數組的全部元素或部分元素

public static void Reverse (Array array);
public static void Reverse (Array array, int index, int length);

若是你在意性能,那麼請不要直接調用Array的Reverse方法,而是應該建立一個自定義的RerverseComparer。好比下面的例子中,調用Array.Reverse和CustomReverse在個人電腦上二者的性能高差距爲20%左右

class Program
{
    static void Main(string[] args)
    {
        int seeds = 100000;

        Staff[] staffs = new Staff[seeds];
        Random r = new Random();
        for (int i = 0; i < seeds; i++)
            staffs[i] = new Staff { StaffNo = r.Next(1, seeds).ToString() };


        ArrayReverse(staffs); 
        CustomReverse(staffs); 

        Console.ReadLine();
    }

    static void ArrayReverse(Staff[] staffs)
    {
        DateTime t1 = DateTime.Now;
        Array.Sort(staffs);
        Array.Reverse(staffs);
        DateTime t2 = DateTime.Now;
        Console.WriteLine("Array Reverse: " + (t2 - t1).Milliseconds + "ms");
    }

    static void CustomReverse(Staff[] staffs)
    {
        DateTime t1 = DateTime.Now;
        Array.Sort(staffs, new StaffComparer());
        DateTime t2 = DateTime.Now;
        Console.WriteLine("Custom Reverse: " + (t2 - t1).Milliseconds + "ms");
    }

    internal class Staff : IComparable
    {
        public string StaffNo { get; set; }
        public string Name { get; set; }

        public int CompareTo(object obj)
        {
            Staff x = obj as Staff;

            return this.StaffNo.CompareTo(x.StaffNo);
        }
    }

    internal class StaffComparer : IComparer<Staff>
    {
        public int Compare(Staff x, Staff y)
        {
            return y.StaffNo.CompareTo(x.StaffNo);
        }
    }
}

執行結果:

image

 

複製數組

Array提供了四個方法以實現淺拷貝:Clone,CopyTo,Copy和ConstrainedCopy。前兩個方法是實例方法,後面兩個是靜態方法。

Clone方法返回一個全新的(淺拷貝)數組 。CopyTo和Copy方法複製數組的連續子集。複製一個多維矩形數組須要你的多維索引映射到一個線性索引。好比,一個3×3的數組position,那麼postion[1,1]對應的線性索引爲1*3+1=4。原數組和目標數組的範圍能夠交換,不會帶來任何問題。

ConstrainedCopy執行原子操做,若是所要求的元素不能所有成功地複製,那麼操做回滾。

Array還提供AsReadOnly方法,它返回一個包裝器,以防止數組元素的值被更改。

最後,Clone方法是由外部的非託管代碼實現

protected extern Object MemberwiseClone()

一樣地,Copy,CopyTo, ConstraintedCopy也都是調用外部實現

internal static extern void Copy(Array sourceArray, int sourceIndex, Array destinationArray, int destinationIndex, int length, bool reliable);

 

轉換數組和縮減數組大小

Array.ConvertAll建立並返回一個類型爲TOutput的新數組,調用Converter代理以複製元素到新的數組中。Converter的定義以下:

public delegate TOutput Converter<TInput,TOutput>(TInput input)

下面的代碼展現了若是把一個浮點數數組轉換成int數組

float[] reals = { 1.3f, 1.5f, 1.8f };

int[] wholes = Array.ConvertAll(reals, f => Convert.ToInt32(f));

foreach (int a in wholes)
    Console.WriteLine(a);  //->1,2,2
Resize方法建立一個新數組,而後複製元素到新數組,而後返回新數組;原數組不發生改變。
相關文章
相關標籤/搜索