(轉(zhuǎn))C#集合類型大揭秘

集合是.NET FCL(Framework Class Library)的重要組成部分,我們平常擼C#代碼時(shí)免不了和集合打交道,F(xiàn)CL提供了豐富易用的集合類型,給我們擼碼提供了極大的便利。正是因?yàn)檫@種與生俱來(lái)的便利性,使得我們對(duì)集合既熟悉又陌生。很多同學(xué)可能一直還是停留在使用的層面上,那么今天我們一起來(lái)深入學(xué)習(xí)一下C#語(yǔ)言中的各種集合。

首先我們看一下 FCL 給我們提供的集合接口:


d7khhhKg0C.png

FCL提供了泛型和非泛型兩大類集合類型。因?yàn)榉欠盒图涎b箱和拆箱帶來(lái)的性能開(kāi)銷問(wèn)題,和泛型集合相比,已經(jīng)變得越來(lái)越雞肋。所以我們也側(cè)重于泛型集合的分析,但是兩者差別不大。

IEnumerable和IEnumerator

1CiFbkmDg6.png

IEnumerable接口是所有集合類型的祖宗接口,其作用相當(dāng)于Object類型之于其它類型。如果某個(gè)類型實(shí)現(xiàn)了IEnumerable接口,就意味著它可以被迭代訪問(wèn),也就可以稱之為集合類型(可枚舉)。IEnumerable接口定義非常簡(jiǎn)單,只有一個(gè)GetEnumerator()方法用于獲取IEnumerator類型的迭代器。


1CiFbkmDg6.png

我們可以將迭代器想象成數(shù)據(jù)庫(kù)的游標(biāo),即序列(集合)中的某個(gè)位置,迭代器只能在序列(集合)中向前移動(dòng)。每調(diào)用一次MoveNext(),如果序列(集合)中還有下一個(gè)元素,則迭代器移動(dòng)到下一個(gè)元素;Current用于獲取序列(集合)中的當(dāng)前元素;因?yàn)榈髡{(diào)用一次代碼只需要獲取一個(gè)元素,這意味著我們需要確定訪問(wèn)到了序列(集合)中的哪個(gè)位置。Reset()用于重置這種狀態(tài),但是基本上不會(huì)使用Reset()重置狀態(tài)。

同一個(gè)序列(集合)可能同時(shí)存在多個(gè)迭代器操作,相當(dāng)于同時(shí)對(duì)一個(gè)集合進(jìn)行多個(gè)遍歷。這種情況下可能會(huì)出現(xiàn)迭代彼此交錯(cuò)。那么如何解決呢?

集合類不直接支持 IEnumerator 和IEnumerator 接口。而是直接支持 IEnumerable接口,其唯一方法是 GetEnumerator,此方法用于返回支持 IEnumerator 的對(duì)象。每次調(diào)用GetEnumerator()方法時(shí)都需要?jiǎng)?chuàng)建一個(gè)新的對(duì)象,同時(shí)迭代器必須保存自身的狀態(tài),記錄此時(shí)已經(jīng)迭代到哪一個(gè)元素。這樣迭代器就像是序列中的游標(biāo)。可以有多個(gè)游標(biāo),移動(dòng)其中任何一個(gè)都可以枚舉集合,與其他迭代器互不影響。

foreach是怎么實(shí)現(xiàn)的?

for依賴對(duì) Length 屬性和索引運(yùn)算符 ([]) 的支持。借助 Length 屬性,C# 編譯器可以使用 for 語(yǔ)句迭代數(shù)組中的每個(gè)元素。for適用于長(zhǎng)度固定且始終支持索引運(yùn)算符的數(shù)組,但并不是所有類型集合的元素?cái)?shù)量都是已知的。此外,許多集合類(包括 Stack、Queue 和 Dictionary<TKey ,TValue>)都不支持按索引檢索元素。因此,需要使用一種更為通用的方法來(lái)迭代元素集合。假設(shè)可以確定第一個(gè)、第二個(gè)和最后一個(gè)元素,那么就沒(méi)有必要知道元素?cái)?shù)量,也沒(méi)有必要支持按索引檢索元素。foreach在這種背景下應(yīng)運(yùn)而生。實(shí)際上,foreach內(nèi)部使用迭代器的MoveNext和Current完成元素的遍歷。

List<int> list = new List<int>();
List<int>.Enumerator enumerator = list.GetEnumerator();
try
{
    int number;
    while (enumerator.MoveNext())
    {
        number = enumerator.Current;
        Console.WriteLine(number);
    }
}
finally
{
    enumerator.Dispose();
}

實(shí)現(xiàn)自定義集合

我們可以自己實(shí)現(xiàn)IEnumerable接口和IEnumerator接口實(shí)現(xiàn)自定義集合。

實(shí)現(xiàn)自定義可枚舉類型:

public class MySet : IEnumerable
{
    internal object[] values;

    public MySet(object[] values)
    {
        this.values = values;
    }

    public IEnumerator GetEnumerator()
    {
        return new MySetIterator(this);
    }
}

手寫(xiě)實(shí)現(xiàn)自定義迭代器:

public class MySetIterator : IEnumerator
{
    MySet set;
    /// <summary>
    /// 保存迭代到的位置
    /// </summary>
    int position;
    internal MySetIterator(MySet set)
    {
        this.set = set;
        position = -1;
    }

    public object Current
    {
        get
        {                   
            if(position==-1||position==set.values.Length)
            {
                throw new   InvalidOperationException();
             }
             int index = position;
             return set.values[index];
         }
    }

    public bool MoveNext()
    {
        if(position!=set.values.Length)
        {
            position++;
        }
        return position < set.values.Length;
    }

    public void Reset()
    {
        position = -1;
    }
}

測(cè)試程序:

object[] values = { "a", "b", "c", "d", "e" };
MySet mySet = new MySet(values);
foreach (var item in mySet)
{
    Console.WriteLine(item);
}

這個(gè)例子也證明了foreach內(nèi)部使用迭代器的MoveNext和Current完成遍歷。

上面的例子中手寫(xiě)實(shí)現(xiàn)迭代器是十分麻煩的,在c#1.0中這是唯一的方式。在c#2.0中,我們可以使用yield語(yǔ)法糖簡(jiǎn)化迭代器。

public IEnumerator GetEnumerator()
{
    for (int i = 0; i < values.Length; i++)
    {
        yield return values[i];
    }
}

IEnumerable和IEnumerator雖然實(shí)現(xiàn)簡(jiǎn)單,只有簡(jiǎn)單的幾個(gè)成員,但是卻支撐起了C#語(yǔ)言中集合這座高樓大廈。

ICollection和ICollection

從第一張圖中,我們可以得知ICollection繼承于IEnumerable接口,并且擴(kuò)展了IEnumerable接口。


1CiFbkmDg6.png

主要擴(kuò)展的功能有:

  • 新增了屬性Count,用于記錄集合元素個(gè)數(shù)
  • 支持添加元素和移除元素
  • 支持是否包含某元素
  • 支持清空集合等等

對(duì)于任何實(shí)現(xiàn)了ICollection接口的集合,我們都可以通過(guò)第1條Count屬性獲取當(dāng)前集合的元素?cái)?shù),所以這些集合也被稱為計(jì)數(shù)集合。

IList 和IList

1CiFbkmDg6.png

IList接口直接繼承于ICollection接口和IEnumerable接口,并且擴(kuò)展了通過(guò)索引操作集合的功能。

主要擴(kuò)展的功能有:

  • 通過(guò)索引獲取集合中某個(gè)元素
  • 通過(guò)元素獲取元素在集合中的索引值
  • 通過(guò)索引插入元素到集合指定位置
  • 移除集合指定索引處的元素

IDictionary<TKey, TValue>和IDictionary


1CiFbkmDg6.png

IDictionary接口直接繼承于ICollection接口和IEnumerable接口,存儲(chǔ)的元素是鍵值對(duì),擴(kuò)展了通過(guò)鍵操作鍵值對(duì)集合的功能。

主要擴(kuò)展的功能有:

  • 通過(guò)鍵KEY獲取值VALUE
  • 插入新的鍵值對(duì){KEY:VALUE}
  • 是否包含KEY
  • 通過(guò)KEY移除鍵值對(duì)元素

主要的集合的接口介紹完了,下面我們來(lái)看一下具體的集合類型。

關(guān)聯(lián)性泛型集合類

1.Dictionary<TKey,TValue>

Dictionary<TKey,TValue>的查詢數(shù)據(jù)所花費(fèi)的時(shí)間是所有集合類里面最快的,因?yàn)槠鋬?nèi)部使用了散列函數(shù)加雙數(shù)組來(lái)實(shí)現(xiàn),所以其查詢數(shù)據(jù)操作的時(shí)間復(fù)雜度可以認(rèn)為是O(1)。Dictionary<TKey,TValue>的實(shí)現(xiàn)是一種典型的犧牲空間換取時(shí)間(雙數(shù)組)的做法。


1CiFbkmDg6.png

Dictionary<TKey,TValue>添加新元素的實(shí)現(xiàn):


1CiFbkmDg6.png

1CiFbkmDg6.png

Dictionary<TKey,TValue>內(nèi)部有兩個(gè)數(shù)組,一個(gè)數(shù)組名為buckets,用于存放由多個(gè)同義詞組成的靜態(tài)鏈表頭指針(鏈表的第一個(gè)元素在數(shù)組中的索引號(hào),當(dāng)它的值為-1時(shí)表示此哈希地址不存在元素);另一個(gè)數(shù)組為entries,它用于存放哈希表中的實(shí)際數(shù)據(jù),同時(shí)這些數(shù)據(jù)通過(guò)next指針構(gòu)成多個(gè)單鏈表。entries數(shù)組中所存放的是Entry結(jié)構(gòu)體,Entry結(jié)構(gòu)體由4個(gè)部分組成,如下所示:
1CiFbkmDg6.png

Dictionary<TKey,TValue>計(jì)算key的哈希值使用的是取余法,這種方式可能會(huì)產(chǎn)生沖突,所以需要進(jìn)行沖突解決。Dictionary<TKey,TValue>解決沖突的方式是鏈接法。


1CiFbkmDg6.png

我們可以根據(jù)源碼來(lái)模擬推導(dǎo)一下這個(gè)過(guò)程:

當(dāng)添加第一個(gè)元素時(shí),此時(shí)會(huì)分配哈希表buckets數(shù)組和entries數(shù)組的空間和初始大小,默認(rèn)為3。對(duì)key=1進(jìn)行哈希求值,假設(shè)第一個(gè)元素的哈希值=9,然后targetBucket = 9%buckets.Length(3)的值為0,所以第一個(gè)元素應(yīng)該放在entries數(shù)組的第一位。最后對(duì)哈希表buckets數(shù)組賦值,數(shù)組索引為0,值為0。此時(shí)內(nèi)部結(jié)構(gòu)如圖所示:


1CiFbkmDg6.png

然后插入第二個(gè)元素,對(duì)key=2進(jìn)行哈希求值,假設(shè)第二個(gè)元素的哈希值=3,然后targetBucket = 3%buckets.Length (默認(rèn)是3)的值為0,所以第二個(gè)元素應(yīng)該放在entries數(shù)組的第一位。但是entries數(shù)組的第一位已經(jīng)存在元素了,這就發(fā)生了沖突。Dictionary<TKey,TValue>解決沖突的方式是鏈接法,把發(fā)生沖突的元素鏈接之前元素的后面,通過(guò)next屬性來(lái)指定沖突關(guān)系,最后更新哈希表buckets數(shù)組。此時(shí)內(nèi)部結(jié)構(gòu)如圖所示:


1CiFbkmDg6.png

我們可以通過(guò)Dictionary<TKey,TValue>查找元素的實(shí)現(xiàn)來(lái)證明我們上面的分析是正確的。

Dictionary<TKey,TValue>查找元素的實(shí)現(xiàn):


EB5B4ECAdI.png

EB5B4ECAdI.png

Dictionary<TKey,TValue>之所以能實(shí)現(xiàn)快速查找元素,其內(nèi)部使用哈希表來(lái)存儲(chǔ)元素對(duì)應(yīng)的位置,我們可以通過(guò)哈希值快速地從哈希表中定位元素所在的位置索引,從而快速獲取到key對(duì)應(yīng)的Value值。物極必反,Dictionary<TKey,TValue>的缺點(diǎn)也很明顯,就是里面的數(shù)據(jù)是無(wú)序排列的,所以按照一定順序遍歷查找數(shù)據(jù)效率是非常低的。

2.SortedDictionary<TKey,TValue>

SortedDictionary<TKey,TValue>和Dictionary<TKey,TValue>類似,至于區(qū)別我們從名稱上就可以看出來(lái),Dictionary<TKey,TValue>是無(wú)序的,SortedDictionary<TKey,TValue>則是有序的。key要保證唯一,而且還要有序排列,這讓我們很自然的就想到了搜索二叉樹(shù)。SortedDictionary<TKey,TValue>使用一種平衡搜索二叉樹(shù)——紅黑樹(shù),作為存儲(chǔ)結(jié)構(gòu)。因?yàn)榛诙植檎遥蕴砑印⒉檎?、刪除元素的時(shí)間復(fù)雜度是O(log n)。相對(duì)于下面提到的SortedList<TKey,TValue>來(lái)說(shuō),SortedDictionary<TKey,TValue>在添加和刪除元素時(shí)更快一些。如果想要快速查詢的同時(shí)又能很好的支持排序的話,并且添加和刪除元素也比較頻繁,可以使用SortedDictionary<TKey,TValue>。

SortedDictionary<TKey,TValue>添加新元素的實(shí)現(xiàn):


EB5B4ECAdI.png

EB5B4ECAdI.png

3.SortedList<TKey,TValue>

在既需要快速查找又需要順序排列的場(chǎng)景下,Dictionary<TKey,TValue>就無(wú)能為力了,因?yàn)镈ictionary<TKey,TValue>使用了散列函數(shù),并不支持線性排序。我們可以使用SortedList<TKey,TValue>集合類來(lái)應(yīng)對(duì)這種場(chǎng)景。

SortedList<TKey,TValue>集合內(nèi)部是使用數(shù)組實(shí)現(xiàn)的,添加和刪除元素的時(shí)間復(fù)雜度是O(n),查找元素利用了二分查找,所以查找元素的時(shí)間復(fù)雜度是O(log n)。所以SortedList<TKey,TValue>雖然支持了有序排列,但是卻是以犧牲查找效率為代價(jià)的。

SortedList<TKey,TValue>和SortedDictionary<TKey,TValue>同時(shí)支持快速查詢和排序,SortedList<TKey,TValue> 優(yōu)勢(shì)在于使用的內(nèi)存比 SortedDictionary<TKey,TValue> 少;但是SortedDictionary<TKey,TValue>可對(duì)未排序的數(shù)據(jù)執(zhí)行更快的插入和移除操作:它的時(shí)間復(fù)雜度為 O(log n),而 SortedList<TKey,TValue> 為 O(n)。所以SortedList<TKey,TValue>適用于既需要快速查找又需要順序排列但是添加和刪除元素較少的場(chǎng)景。

內(nèi)部實(shí)現(xiàn)結(jié)構(gòu):


EB5B4ECAdI.png

根據(jù)Key獲取Value的實(shí)現(xiàn):


EB5B4ECAdI.png

IndexOfKey實(shí)現(xiàn):
EB5B4ECAdI.png

添加新元素:


EB5B4ECAdI.png

添加操作:
EB5B4ECAdI.png

非關(guān)聯(lián)性泛型集合類

1.List

泛型的List 類提供了不限制長(zhǎng)度的集合類型,List內(nèi)部實(shí)現(xiàn)使用數(shù)據(jù)結(jié)構(gòu)是數(shù)組。我們都知道數(shù)組是長(zhǎng)度固定的,那么List不限制長(zhǎng)度必定需要維護(hù)這個(gè)數(shù)組。實(shí)際上List維護(hù)了一定長(zhǎng)度的數(shù)組(默認(rèn)為4),當(dāng)插入元素的個(gè)數(shù)超過(guò)4或初始長(zhǎng)度時(shí),會(huì)去重新創(chuàng)建一個(gè)新的數(shù)組,這個(gè)新數(shù)組的長(zhǎng)度是初始長(zhǎng)度的2倍,然后將原來(lái)的數(shù)組賦值到新的數(shù)組中。

我們可以通過(guò)ILSpy看一下List源碼證明我們上面所說(shuō)的:

List內(nèi)部重要變量:


EB5B4ECAdI.png

EB5B4ECAdI.png

新增元素操作:


EB5B4ECAdI.png

新增元素確認(rèn)數(shù)組容量:
EB5B4ECAdI.png

真正的數(shù)組擴(kuò)容操作:
EB5B4ECAdI.png

數(shù)組擴(kuò)容的場(chǎng)景涉及到對(duì)象的創(chuàng)建和賦值,是比較消耗性能的。所以如果能指定一個(gè)合適的初始長(zhǎng)度,能避免頻繁的對(duì)象創(chuàng)建和賦值。再者,因?yàn)閮?nèi)部的數(shù)據(jù)結(jié)構(gòu)是數(shù)組,插入和刪除操作需要移動(dòng)元素位置,所以不適合頻繁的進(jìn)行插入和刪除操作;但是可以通過(guò)數(shù)組下標(biāo)查找元素。所以List適合讀多寫(xiě)少的場(chǎng)景。

2.LinkedList

上面我們提到List適合讀多寫(xiě)少的場(chǎng)景,那么必定有一個(gè)List適合寫(xiě)多讀少的場(chǎng)景,就是這貨了——LinkedList。至于為什么適合寫(xiě)多讀少,熟悉數(shù)據(jù)結(jié)構(gòu)的同學(xué)應(yīng)該已經(jīng)猜到了。因?yàn)長(zhǎng)inkedList的內(nèi)部實(shí)現(xiàn)使用的是鏈表結(jié)構(gòu),而且還是雙向鏈表。直接看源碼:


EB5B4ECAdI.png

因?yàn)閮?nèi)部實(shí)現(xiàn)結(jié)構(gòu)是鏈表,所以可以在某一個(gè)節(jié)點(diǎn)前或節(jié)點(diǎn)后插入新的元素。

鏈表節(jié)點(diǎn)定義:


EB5B4ECAdI.png

我們以在某個(gè)節(jié)點(diǎn)前插入新元素為例:


EB5B4ECAdI.png

具體的插入操作,注意操作步驟不能顛倒:
EB5B4ECAdI.png

3.HashSet

HashSet是一個(gè)無(wú)序的能夠保持唯一性的集合。我們可以將HashSet看作是簡(jiǎn)化的Dictionary<TKey,TValue>,只不過(guò)Dictionary<TKey,TValue>存儲(chǔ)的鍵值對(duì)對(duì)象,而HashSet存儲(chǔ)的是普通對(duì)象。其內(nèi)部實(shí)現(xiàn)也和Dictionary<TKey,TValue>基本一致,也是散列函數(shù)加雙數(shù)組實(shí)現(xiàn)的,區(qū)別是存儲(chǔ)的Slot結(jié)構(gòu)體不再有key。

內(nèi)部實(shí)現(xiàn)數(shù)據(jù)結(jié)構(gòu):


EB5B4ECAdI.png

m_slots中所存放的是Slot結(jié)構(gòu)體,Slot結(jié)構(gòu)體由3個(gè)部分組成,如下所示:


EB5B4ECAdI.png

添加新元素的具體實(shí)現(xiàn):

和Dictionary<TKey,TValue>添加新元素的實(shí)現(xiàn)基本一致。


EB5B4ECAdI.png

4.SortedSet

SortedSet和HashSet,就像SortedDictionary<TKey,TValue>和Dictionary<TKey,TValue>一樣。SortedSet支持元素按順序排列,內(nèi)部實(shí)現(xiàn)也是紅黑樹(shù),并且SortedSet對(duì)于紅黑樹(shù)的操作方法和SortedDictionary<TKey,TValue>完全相同。所以不再做過(guò)多的分析。

5.Stack

棧是一種后進(jìn)先出的結(jié)構(gòu),C#的棧是借助數(shù)組實(shí)現(xiàn)的,考慮到棧后進(jìn)先出的特性,使用數(shù)組來(lái)實(shí)現(xiàn)貌似是水到渠成的事。


EB5B4ECAdI.png

入棧操作:


EB5B4ECAdI.png

彈棧操作:
EB5B4ECAdI.png

6.Queue

隊(duì)列是一種先進(jìn)先出的結(jié)構(gòu),C#的隊(duì)列也是借助數(shù)組實(shí)現(xiàn)的,有了前面的經(jīng)驗(yàn),借助數(shù)組實(shí)現(xiàn)必然會(huì)有數(shù)組擴(kuò)容。C#的隊(duì)列實(shí)現(xiàn)其實(shí)是循環(huán)隊(duì)列的方式,可以簡(jiǎn)單的理解為將隊(duì)列的頭尾相接。至于為什么要這么做?為了節(jié)省存儲(chǔ)空間和減少元素的移動(dòng)。因?yàn)樵爻鲫?duì)列時(shí)后面的元素跟著前移是非常消耗性能的,但是不跟著向前移動(dòng)的話,前面就會(huì)一直存在空閑的空間浪費(fèi)內(nèi)存。所以使用循環(huán)隊(duì)列來(lái)解決這種問(wèn)題。


EB5B4ECAdI.png

入隊(duì)操作:


EB5B4ECAdI.png
EB5B4ECAdI.png

出隊(duì)操作:


EB5B4ECAdI.png

線程安全的集合類

需要我們注意的是,上面我們所介紹的集合并不是線程安全的,在多線程環(huán)境下,可能會(huì)出現(xiàn)線程安全問(wèn)題。在多線程讀的情況下,我們使用普通集合即可。在多線程添加/更新/刪除時(shí),我們可以采用手動(dòng)鎖定的方式確保線程安全,但是應(yīng)該注意加鎖的范圍和粒度,加鎖不當(dāng)可能會(huì)導(dǎo)致程序性能低下甚至產(chǎn)生死鎖。

更好的選擇的是使用的C#提供的線程安全集合(命名空間:System.Collections.Concurrent)。線程安全集合使用幾種算法來(lái)最小化線程阻塞。


EB5B4ECAdI.png

總結(jié)

寫(xiě)著寫(xiě)著突然發(fā)現(xiàn)跑到數(shù)據(jù)結(jié)構(gòu)上來(lái)了。程序=數(shù)據(jù)結(jié)構(gòu)+算法。上面提到的集合類型,我們需要在不同的場(chǎng)景進(jìn)行合適的選擇,其實(shí)本質(zhì)上就是選擇合適的數(shù)據(jù)結(jié)構(gòu)。

參考:
https://www.cnblogs.com/jesse2013/p/CollectionsInCSharp.html
https://www.c-sharpcorner.com/article/concurrent-collections-in-net-concurrentdictionary-part-one/
http://www.cnblogs.com/jeffwongishandsome/archive/2012/09/09/2677293.html
http://www.cnblogs.com/edisonchou/p/4706253.html

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一、數(shù)組數(shù)組是一組使用數(shù)字索引的對(duì)象,這些對(duì)象屬于同一種類型。雖然C#為創(chuàng)建數(shù)組提供了直接的語(yǔ)言支持,但通用類型系...
    CarlDonitz閱讀 792評(píng)論 0 1
  • 一、常用集合類型及概念 1.基本關(guān)系 許多泛型集合類型均為非泛型類型的直接模擬。 Dictionary< TKey...
    aslbutton閱讀 1,486評(píng)論 0 49
  • C#集合 有兩種主要的集合類型:泛型集合和非泛型集合。 泛型集合被添加在 .NET Framework 2.0 中...
    OctOcean閱讀 933評(píng)論 0 3
  • 數(shù)據(jù)結(jié)構(gòu) 數(shù)據(jù)結(jié)構(gòu)是計(jì)算機(jī)存儲(chǔ)、組織、管理數(shù)據(jù)的方式 數(shù)據(jù)結(jié)構(gòu)是指相互之間存在一種或多種特定關(guān)系的數(shù)據(jù)元素的集合 ...
    JunChow520閱讀 3,833評(píng)論 0 4
  • 9yue6 集合(Collection 一、集合的作用: 有兩種方式可以將對(duì)象分組: 1、創(chuàng)建對(duì)象數(shù)組 2、創(chuàng)建對(duì)...
    cGunsNRoses閱讀 2,544評(píng)論 0 0

友情鏈接更多精彩內(nèi)容