c# 數組和集合精講



本文內容來自我寫的開源電子書《WoW C#》,現在正在編寫中,可以去 WOW-Csharp/學習路徑總結.md at master · sogeisetsu/WOW-Csharp (github.com)來查看編寫進度。預計2021年年底會完成編寫,2022年2月之前會完成所有的校對和轉制電子書工作,爭取能夠在2022年將此書上架亞馬遜。編寫此書的目的是因為目前.NET市場相對低迷,很多優秀的書都是基於.NET framework框架編寫的,與現在的.NET 6相差太大,正規的.NET 5學習教程現在幾乎只有MSDN,可是MSDN雖然准確優美但是太過瑣碎,沒有過閱讀開發文檔的同學容易一頭霧水,於是,我就編寫了基於.NET 5的《WoW C#》。本人水平有限,歡迎大家去本書的開源倉庫 sogeisetsu/WOW-Csharp關注、批評、建議和指導。

數組與集合的概念

數組是一種指定長度和數據類型的對象,在實際應用中有一定的局限性。

集合正是為這種局限性而生的,集合的長度能根據需要更改,也允許存放任何數據類型的值。

Array,ArrayList and List<T>

Array、ArrayList和List都是從IList派生出來的,它們都實現了IEnumerable接口

從某種意義上來說,ArrayList和List屬於集合的范疇,因為他們都來自程序集System.Collections,但是因為它們都是儲存了多個變量的數據結構,並且都不是類似鍵值對的組合,並且沒有先進先出或者先進后出的機制,故而稱為數組。

我們一般稱呼Array,ArrayList and List<T>為數組。

Array

Array必須在定義且不初始化賦值的時候(不初始化的情況下聲明數組變量除外)必須定義數組最外側的長度。比如:

int[] vs = new int[10];
int[,] duoWei = new int[3, 4];
int[][] jiaoCuo = new int[3][]; // 該數組是由三個一維數組組成的

一維數組

定義

用類似於這種方式定義一個數組

int[] array = new int[5];

初始化賦值

用類似於這種方式初始化

int[] array1 = new int[] { 1, 3, 5, 7, 9 };

也可以進行隱式初始化

int[] array2 = { 1, 3, 5, 7, 9 };
string[] weekDays2 = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };

用類似於下面這種方式先聲明,再賦值

int[] array3;
array3 = new int[] { 1, 3, 5, 7, 9 };   // OK
//array3 = {1, 3, 5, 7, 9};   // Error

多維數組

數組可具有多個維度。多維數組的每個元素是聲明時的數組所屬類型的元素。比如說int[,]的每個元素都是int類型而不是int[]類型。換種說法就是多維數組不能算做“數組組成的數組”

定義

用類似下面這種方式聲明一個二維數組的長度

int[,] array = new int[4, 2];

初始化賦值

用類似於下面的方式初始化多維數組:

// Two-dimensional array.
int[,] array2D = new int[,] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } };
// The same array with dimensions specified.
int[,] array2Da = new int[4, 2] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } };
// A similar array with string elements.
string[,] array2Db = new string[3, 2] { { "one", "two" }, { "three", "four" },
                                        { "five", "six" } };

// Three-dimensional array.
int[,,] array3D = new int[,,] { { { 1, 2, 3 }, { 4, 5, 6 } },
                                 { { 7, 8, 9 }, { 10, 11, 12 } } };

還可在不指定級別的情況下初始化數組

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

不初始化的情況下聲明數組變量:

int[,] array5;
array5 = new int[,] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 } };   // OK
//array5 = {{1,2}, {3,4}, {5,6}, {7,8}};   // Error

元素賦值和獲取元素

可以用類似於array[1,2]的方式來獲取數組的值和為數組賦值。

GetLength(0)可以獲取最外圍數組的長度,GetLength(1)可以獲得第二層數組的長度。以此類推。為一個二維數組duoWei循環賦值的方式如下:

Console.WriteLine("二維數組賦值");
for (int i = 0; i < duoWei.GetLength(0); i++)
{
    for (int j = 0; j < duoWei.GetLength(1); j++)
    {
        duoWei[i, j] = i + j;
    }
}

如何獲取二維數組中的元素個數呢?

int[,] array = new int[,] {{1,2,3},{4,5,6},{7,8,9}};//定義一個3行3列的二維數組
int row = array.Rank;//獲取維數,這里指行數
int col = array.GetLength(1);//獲取指定維度中的元素個數,這里也就是列數了。(0是第一維,1表示的是第二維)
int col = array.GetUpperBound(0)+1;//獲取指定維度的索引上限,在加上一個1就是總數,這里表示二維數組的行數
int num = array.Length;//獲取整個二維數組的長度,即所有元的個數

來源:C#中如何獲取一個二維數組的兩維長度,即行數和列數?以及多維數組各個維度的長度? - jack_Meng - 博客園 (cnblogs.com)

交錯數組

交錯數組是一個數組,其元素是數組,大小可能不同。 交錯數組有時稱為“數組的數組”。

交錯數組不初始化就聲明的方式如下:

int[][] ccf;
ccf = new int[3][];

交錯數組類似於python的多維數組,比較符合人類的直覺,一個交錯數組里面包含了多個數組。

定義

可以采用類似下面的方式來聲明一個交錯數組:

// 定義多維數組要求每個維度的長度都相同 下面定義交錯數組
int[][] jiaoCuo = new int[3][]; // 該數組是由三個一維數組組成的

上面聲明的數組是具有三個元素的一維數組,其中每個元素都是一維整數數組。

可使用初始化表達式通過值來填充數組元素,這種情況下不需要數組大小。 例如:

jaggedArray[0] = new int[] { 1, 3, 5, 7, 9 };
jaggedArray[1] = new int[] { 0, 2, 4, 6 };
jaggedArray[2] = new int[] { 11, 22 };

初始化賦值

可在聲明數組時將其初始化,如:

int[][] jaggedArray2 = new int[][]
{
new int[] { 1, 3, 5, 7, 9 },
new int[] { 0, 2, 4, 6 },
new int[] { 11, 22 }
};

獲取元素和單個賦值

可以用類似於jiaoCuo[1][1]來獲取單個元素的值,也可以用類似於jiaoCuo[1][1] = 2;來為單個元素賦值。

可以采取類似於下面的方式來進行循環賦值:

Console.WriteLine("交錯數組循環賦值");
// 先聲明交錯數組中每一個數組的長度
for (int i = 0; i < 3; i++)
{
    jiaoCuo[i] = new int[i + 1];
}
// 然后對交錯數組中的每一個元素賦值
for (int i = 0; i < jiaoCuo.Length; i++)
{
    Console.WriteLine($"交錯數組的第{i + 1}層");
    for (int j = 0; j < jiaoCuo[i].Length; j++)
    {
        jiaoCuo[i][j] = i + j;
        Console.WriteLine(jiaoCuo[i][j]);
    }
}

方法和屬性

像數組這種儲存多個變量的數據結構,最重要的就是增查刪改、獲取長度和數據類型轉換Array因為數組的特性,長度不可改變,所以增查刪改只能有查和改。

Array類型用用類似於下面的方式進行改操作:

vs[0] = 12; //一維數組
duoWei[1, 2] = 3; //多維數組
jiaoCuo[1][1] = 2; //交錯數組

Array類型用類似於下面的方式進行查操作:

int[] vs = new int[10];
vs[0] = 12;
Console.WriteLine(Array.IndexOf(vs, 12)); //0
Console.WriteLine(vs.Contains(12)); // True

獲取長度

可以用類似於下面這種方式來獲取:

Console.WriteLine(vs.Length);
Console.WriteLine(vs.Count());

交錯數組的Length是獲取所包含數組的個數,多維數組的Length是獲取數組的元素的總個數,多維數組GetLength(0)可以獲取最外圍數組的長度,GetLength(1)可以獲得第二層數組的長度。以此類推。

Array.ConvertAll() 數據類型轉換

可以用Array.ConvertAll<TInput,TOutput>(TInput[], Converter<TInput,TOutput>) 來進行數組類型的轉換。

參數如下:

  • array

    TInput[]

要轉換為目標類型的從零開始的一維 Array

用於將每個元素從一種類型轉換為另一種類型的 Converter


來源:[Array.ConvertAll(TInput], Converter) 方法 (System) | Microsoft Docs

demo如下:

double[] vs3 = Array.ConvertAll(vs, item => (double)item);

切片

默認狀態下只能對一維數組進行切片,或者通過交錯數組獲取的一維數組也可以進行切片。

切片的方式類似於vs[1..5],表示vs數組從1到5,左閉右開。^1表示-1,即最后一個元素。[^3..^1]表示倒數第三個元素到倒數第一個元素,左閉右開。

獲取單個元素和賦值

可以采用下面的方式來獲取單個元素和為單個元素單獨賦值:

// 一維數組
Console.WriteLine(vs[1]);
vs[1] = 2;
// 多維數組
Console.WriteLine(duoWei[1, 2]);
duoWei[1, 2] = 3;
// 交錯數組
Console.WriteLine(jiaoCuo[1][0]);
jiaoCuo[1][0] = 0;

Array.ForEach 循環

System.Array里面也有ForEach方法,這是用於Array的。

demo:

Array.ForEach(vs, item => Console.WriteLine(item));

ArrayList

定義

用類似於下面的三種方式中的任意一種來聲明ArrayList:

ArrayList() 初始化 ArrayList 類的新實例,該實例為空並且具有默認初始容量。
ArrayList(ICollection) 初始化 ArrayList 類的新實例,該類包含從指定集合復制的元素,並具有與復制的元素數相同的初始容量。
ArrayList(Int32) 初始化 ArrayList 類的新實例,該實例為空並且具有指定的初始容量。

可以將Arraylist看作是一種長度可以自由變換,可以包含不同數據類型元素的數組。

初始化賦值

可以采用類似於下面的方式來初始化賦值:

ArrayList arrayList1 = new ArrayList() { 12, 334, 3, true };

循環

循環可以用for和foreach。

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

方法和屬性

list<T>類似,但是沒有ConvertAll方法。ArrayList本身沒有ForEach方法,但是也可以用傳統的foreach方法(就像前面提到的ArrayList的循環那樣)。

具體的方法和屬性請查看List 部分的方法和屬性

List<T>

定義

用類似於下面的三種方式中的任意一種來聲明List<T>

List() 初始化 List 類的新實例,該實例為空並且具有默認初始容量。
List(IEnumerable) 初始化 List 類的新實例,該實例包含從指定集合復制的元素並且具有足夠的容量來容納所復制的元素。
List(Int32) 初始化 List 類的新實例,該實例為空並且具有指定的初始容量。

初始化

用類似於下面的方式在聲明時初始化:

List<string> listA = new List<string>() { "hello", " ", "wrold" };

循環

List<T>有一個名稱為ForEach的方法:

public void ForEach (Action<T> action);

該方法的本質是要對 List 的每個元素執行的 Action 委托。Action 的參數即為List<T>在循環過程中的每個元素。

demo如下:

// 聲明
List<string> listA = new List<string>() { "hello", " ", "wrold" };
// 循環
var i = 0;
listA.ForEach(item =>
              {
                  Console.WriteLine($"第{i + 1}個");
                  Console.WriteLine(item);
                  i++;
              });

方法和屬性

從獲取長度、增查刪改、數據類型轉換、切片和循環來解析。其中除了數據類型轉換和List<T>類型本身就擁有的ForEach方法外,都適用於ArrayList。

先聲明一個List<string>作為演示的基礎:

List<string> listA = new List<string>() { "hello", " ", "wrold" };

屬性 長度

Count屬性可以獲取長度

Console.WriteLine(listA.Count);

屬性 取值

Console.WriteLine(listA[0]);

即增加元素,可以用Add方法:

listA.Add("12");

IndexOf獲取所在位置,Contains獲取是否包含。

Console.WriteLine(listA.IndexOf("12"));
Console.WriteLine(listA.Contains("12"));

Remove根據數據刪除,RemoveAt根據位置刪除。

listA.Remove("12");
listA.RemoveAt(1);

可以用類似於listA[1] = "改變";的方式來修改元素內容。

切片

可以用GetRange(int index, int count)來進行切片操作,第一個參數是切片開始的位置,第二個參數是切片的數量,即從index開始往后數幾個數。

Console.WriteLine(listA.GetRange(1, 1).Count);

循環

List<T>有一個名稱為ForEach的方法,該方法的本質是要對 List 的每個元素執行的 Action 委托。Action 的參數即為List<T>在循環過程中的每個元素。

demo如下:

// 聲明
List<string> listA = new List<string>() { "hello", " ", "wrold" };
// 循環
var i = 0;
listA.ForEach(item =>
              {
                  Console.WriteLine($"第{i + 1}個");
                  Console.WriteLine(item);
                  i++;
              });

數據類型轉換

可以用ConvertAll來對數組的數據類型進行轉換,這是List<T>自帶的方法。System.Array里面也有ConvertAll方法,這是用於Array的。

List<object> listObject = listA.ConvertAll(s => (object)s);

區別

成員單一類型 長度可變 切片友好 方法豐富 增查刪改 ConvertAll
一維數組 查、改
多維數組 查、改
交錯數組 查、改
ArrayList 增查刪改
List<T> 增查刪改

Array最大的好處就是切片友好,可以使用類似於[1..3]的方式切片,這是比GetRange更加直觀的切片方式。List<T>類型可以通過ToArray的方法來轉變成Array。

Array,ArrayList and List<T>之間的轉換

關於這一部分的demo代碼詳情可從Array,ArrayList and List之間的轉換 · sogeisetsu/Solution1@88f27d6 (github.com)獲得。

先分別聲明這三種數據類型。

// 聲明數組
int[] a = new int[] { 1,3,4,5,656,-1 };
// 聲明多維數組
int[,] aD = new int[,] { { 1, 2 }, { 3, 4 } };
// 聲明交錯數組
int[][] aJ = new int[][] {
    new int[]{ 1,2,3},
    new int[]{ 1}
};
// 聲明ArrayList
ArrayList b = new ArrayList() { 1, 2, 344, "233", true };
// 聲明List<T>
List<int> c = new List<int>();

Array轉ArrayList

// 數組轉ArrayList
ArrayList aToArrayList = new ArrayList(a);

Array轉List<T>

List<int> aToList = new List<int>(a);
List<int> aToLista = a.ToList();

List<T>轉Array

int[] cToList = c.ToArray();

List<T>轉ArrayList

ArrayList cToArrayList = new ArrayList(c);

ArrayList轉Array

在轉換的過程中,會丟失數據類型的准確度,簡單來說就是轉換成的Array會變成object

// ArrayList轉Array
object[] bToArray = b.ToArray();

這種轉換的意義不大,如果轉換完之后再強行用Array.ConvertAll方法來進行數據類型的轉換,很有可能會出現諸如Unable to cast object of type 'System.String' to type 'System.Int32'.的錯誤,這是因為ArrayList本身成員就可以不是單一類型。

數組的打印

Array的打印

對於Array的打印,我找到了四種方式,如下:

  • 調用Array.ForEach

    Array.ForEach(a, item => Console.WriteLine(item));
    
  • 傳統forEach

    foreach (var item in a)
    {
    Console.WriteLine(item);
    }
    
  • 傳統for

    for (int i = 0; i < a.Count(); i++)
    {
    Console.WriteLine(a[i]);
    }
    
  • string.Join

    Console.WriteLine(string.Join("\t", a));
    

ArrayList的打印

ArrayList的打印我知道的就只有傳統的for和foreach兩種方式。

List<T>的打印

List<T>的打印除了傳統的for和foreach兩種方式之外,還有List<T>本身自帶的foreach:

var i = 0;
listA.ForEach(item =>
              {
                  Console.WriteLine($"第{i + 1}個");
                  Console.WriteLine(item);
                  i++;
              });

請注意:ArrayList和List<T>均沒有string.Join和調用Array.ForEach兩種方式來打印數組。

鍵值對集合

可以用鍵來訪問元素的集合稱之為鍵值對集合,這是一個筆者私自創造的名詞,他們屬於集合的一部分。在很多時候,我們所所稱的集合就是專指鍵值對集合。鍵值對集合每一項都有一個鍵/值對。鍵用於訪問集合中的項目。

HashTable

表示根據鍵的哈希代碼進行組織的鍵/值對的集合。

Hashtable的Key和Value都是object類型,所以在使用值類型的時候,必然會出現裝箱和拆箱的操作。所以性能會比較弱。

構造函數

HashTable的構造函數有很多,具體可以查看Hashtable 類 (System.Collections) | Microsoft Docs

最常見的就是:

Hashtable 對象名 = new Hashtable ();

Hashtable中key-value鍵值對均為object類型,所以Hashtable可以支持任何類型的keyvalue鍵值對,任何非 null 對象都可以用作鍵

用諸如下面這樣的方式來將null作為hashtable的一部分,會報System.ArgumentNullException:“Key cannot be null. Arg_ParamName_Name”的錯誤。

Hashtable hashtable = new Hashtable();
int? a = null;
hashtable.Add(a, "123");
Console.WriteLine(a);

方法和屬性

Hashtable 類中常用的屬性和方法如下表所示。

屬性或方法 作用
Count 集合中存放的元素的實際個數
void Add(object key,object value) 向集合中添加元素
void Remove(object key) 根據指定的 key 值移除對應的集合元素
void Clear() 清空集合
ContainsKey (object key) 判斷集合中是否包含指定 key 值的元素
ContainsValue(object value) 判斷集合中是否包含指定 value 值的元素

取值

每個元素都是存儲在對象中的鍵/值對 DictionaryEntry 。 鍵不能為 null ,但值可以為。

可以用類似於集合名.[鍵]的方式取值。HashTable的每一個元素都是DictionaryEntry類,這個類有兩個屬性分別是KeyValue,可以獲取每一個元素的鍵和值。因為HashTable是無序排列,所以只能通過foreach來獲取DictionaryEntry。

foreach (DictionaryEntry item in hashtable)
{

    Console.WriteLine(item);
    Console.WriteLine(item.Key);
    Console.WriteLine(item.Value);
    Console.WriteLine("-=-=-=-=-=-=-=-=-=");
}

不建議使用 Hashtable 類進行新的開發。 相反,我們建議使用泛型 Dictionary 類。 有關詳細信息,請參閱 GitHub 上 不應使用非泛型集合


來源:Hashtable 類 (System.Collections) | Microsoft Docs

SortedList

表示鍵/值對的集合,這些鍵值對按鍵排序並可按照鍵和索引訪問。

SortedList有兩種,一種是System.Collections.SortedList,一種是System.Collections.Generic.SortedList。后者使用了泛型,成員類型單一,更安全且性能更優秀。

不建議使用 SortedList 類進行新的開發。 相反,我們建議使用泛型 System.Collections.Generic.SortedList 類。 有關詳細信息,請參閱 GitHub 上 不應使用非泛型集合


來源:SortedList 類 (System.Collections) | Microsoft Docs

SortedList對象在內部維護兩個用於存儲列表元素的數組; 即,一個數組用於存儲鍵,另一個數組用於關聯值。 每個元素都是一個可作為對象進行訪問的鍵/值對 DictionaryEntry鍵不能為 null ,但值可以為。

SortedList 集合中所使用的屬性和方法與 Hashtable 比較類似,這里不再贅述。它的特點就是可以用索引來訪問。

構造函數

構造函數有很多,可以查看SortedList 類 (System.Collections) | Microsoft Docs。最常用的就是:

SortedList sortedList = new SortedList();

取值

既可以用類似於集合名.[鍵]的方式取值,也可以用GetKey(Int32)來獲取建和用GetByIndex(Int32)來獲取值。順序為加入元素的順序

for (int i = 0; i < sortedList.Count; i++)
{
    Console.WriteLine($"{sortedList.GetKey(i)}\t{sortedList.GetByIndex(i)}");
}

Dictionary

在單線程情況下,是所有集合類型中最快的。簡稱Dict

本質上是HsashTable的泛型類。因為Hashtable的Key和Value都是object類型,所以在使用值類型的時候,必然會出現裝箱和拆箱的操作,因此性能肯定是不如Dictionary的,在此就不做過多比較了。

只要對象用作中的鍵 Dictionary ,它就不得以任何影響其哈希值的方式進行更改。 根據字典的相等比較器,中的每個鍵都 Dictionary 必須是唯一的。 鍵不能為 null ,但如果其類型為引用類型,則值可以為 TValue

構造方法

最常見的構造方法就是下面這個,其他的請參考Dictionary 類 (System.Collections.Generic) | Microsoft Docs

Dictionary<int, string> dictionary = new Dictionary<int, string>();

其他的方法、屬性和取值方式和HashTable一致,唯一的區別就是由於 Dictionary 是鍵和值的集合,因此元素類型不是鍵的類型或值的類型。 相反,元素類型是 KeyValuePair 鍵類型和值類型的。而HashTable每個元素都是一個可作為對象進行訪問的鍵/值對 DictionaryEntry

foreach (KeyValuePair<int, string> item in dictionary)
{
    Console.WriteLine($"{item.Key}\t{item.Value}\tover");
}

單線程程序中推薦使用 Dictionary, 有泛型優勢, 且讀取速度較快, 容量利用更充分。多線程程序中推薦使用 Hashtable, 默認的 Hashtable 允許單線程寫入, 多線程讀取, 對 Hashtable 進一步調用 Synchronized() 方法可以獲得完全線程安全的類型. 而 Dictionary 非線程安全, 必須人為使用 lock 語句進行保護, 效率大減。

區別

直接取值方式 泛型 特點(用途) 元素類型
HashTable 鍵值對 用於多線程存儲鍵值對 DictionaryEntry
HashSet 無序,無鍵值對,無法直接取值。 用於存儲不重復的元素,並且高效的進行set操作。 泛型類型
SortedList 鍵值對、索引 用於有按順序索引的需求。有泛型需求時可以使用SortedDictionary<TKey,TValue> DictionaryEntry
Dictionary 鍵值對 單線程程序中推薦使用 Dictionary, 有泛型優勢, 且讀取速度較快, 容量利用更充分。 KeyValuePair

互相轉換

我認為集合的類型轉換意義不大,因為集合的意義就是一個儲存數據的引用類型。程序一般在設計之初就已經通過未來可能的使用場景確定了集合的類型。就像一個類有一個int類型的屬性,一般不應該在后面使用的時候去把它轉為Double。

這里只講HashTable和Dictionary之間的轉換

像非泛型類和泛型類之間的轉換,必須在轉換之初就有類型安全的考慮,否則可能出現很多錯誤。

HashTable 轉 Dict

// hashtable 轉 Dict
Dictionary<int, int> dictionary = new Dictionary<int, int>();
foreach (DictionaryEntry item in hashtable)
{
    dictionary.Add((int)item.Key, (int)item.Value);
}

Dict 轉 HashTable

// Dict轉Hashtable
Hashtable hashtable1 = new Hashtable(dictionary);

集合的打印

使用傳統的foreach可以非常方便地進行打印。在打印的時候需要考慮到不同類型的集合元素類型的不同。雖然var可以幫助我們不用去記憶那些討厭的類型名稱,但是在某些時候IDE會因為使用了var而無法讓代碼自動提醒功能正常工作。

foreach (var item in dictionary)
{
    Console.WriteLine($"key:{item.Key}\tvalue:{item.Value}");
}

建議使用泛型集合

微軟官方建議使用泛型集合,因為non-generic collectionsError proneLess performant的問題,微軟提供了用以替換non-generic collections的類,現在摘錄如下:

Type Replacement
ArrayList List
CaseInsensitiveComparer StringComparer.OrdinalIgnoreCase
CaseInsensitiveHashCodeProvider StringComparer.OrdinalIgnoreCase
CollectionBase Collection
Comparer Comparer
DictionaryBase Dictionary or KeyedCollection
DictionaryEntry KeyValuePair
Hashtable Dictionary
Queue Queue
ReadOnlyCollectionBase ReadOnlyCollection
SortedList SortedList
Stack Stack

來源:platform-compat/DE0006.md at master · dotnet/platform-compat (github.com)

HashSet<T>

HashSet 是一個優化過的無序集合,提供對元素的高速查找和高性能的set集合操作,而且 HashSet 是在 .NET 3.5 中被引入的,在 System.Collection.Generic 命名空間下。

只要泛型類允許null,HashSet就允許元素為null。

HashSet會在添加元素的過程中自動去除重復的值,並且不會報錯。

HashSet類提供高性能的設置操作。 集是不包含重復元素的集合,其元素無特定順序。

HashSet 的一些特性如下:

​ a. HashSet 中的值不能重復且沒有順序。

​ b. HashSet 的容量會按需自動添加。

HashSet 並非鍵值對集合。

HashSet 只能包含唯一的元素,它的內部結構也為此做了專門的優化,值得注意的是,HashSet 也可以存放單個的 null 值,可以得出這么一個結論:如何你想擁有一個具有唯一值的集合,那么 HashSet 就是你最好的選擇,何況它還具有超高的檢索性能。

構造函數

比較常見的構造函數就像下面這樣,更多的構造函數請看HashSet 類 (System.Collections.Generic) | Microsoft Docs

HashSet<int> hashSet = new HashSet<int>();

方法和屬性

具體的方法和屬性請查看HashSet 類 (System.Collections.Generic) | Microsoft Docs

像常見的clear、remove、add、count之類的方法和屬性不再贅述。

HashSet 的 set操作

HashSet 類主要是設計用來做高性能集運算的,例如對兩個集合求 交集、並集、差集等。

為了方便理解,先畫一個圖,A和B為兩個淺藍色的正圓,C為A和B的交集。后面會用到這張圖里面的內容。

定義兩個HashSet,命名為setAsetB,分別代表圖中的A和B。

IsProperSubsetOf 真子集

確定 HashSet 對象是否為指定集合的真子集

// 確定setA是否為setB的真子集
Console.WriteLine(setA.IsProperSubsetOf(setB)); // True
Console.WriteLine(setC.IsProperSubsetOf(setB)); // False

UnionWith 並集

修改當前 HashSet 對象以包含存在於該對象中、指定集合中或兩者中的所有元素。其實就是修改當前集合為兩個集合的並集。求圖中的A+B

// 求兩個的並集
setA.UnionWith(setB);
// 現在setA就是兩個集合的並集
foreach (var item in setA)
{
    Console.WriteLine(item);
}

IntersectWith 交集

將當前集合變為兩個集合的交集,求圖中的C。

// 求交集
setA.IntersectWith(setB);
// 現在setA就是兩個的交集
foreach (var item in setA)
{
    Console.WriteLine(item);
}

ExceptWith 差集

去除交集,從當前 HashSet 對象中移除指定集合中的所有元素。本質上是求圖中的 A-C

// 去除交集,從當前 HashSet<T> 對象中移除指定集合中的所有元素。
setA.ExceptWith(setB);
foreach (var item in setA)
{
    Console.WriteLine(item);
}

SymmetricExceptWith

僅包含存在於該對象中或存在於指定集合中的元素(但並非兩者)。本質上是(A-C)+(B-C)

//僅包含存在於該對象中或存在於指定集合中的元素(但並非兩者)。
setA.SymmetricExceptWith(setB);
foreach (var item in setA)
{
    Console.WriteLine(item);
}

Json解析

json是一種類似於通過鍵值對來儲存數據的格式,在對數據庫進行操作的時候,通常會把類數據轉為json格式,然后儲存在數據庫里面,使用的時候再將json轉為類的實例化對象。java的springboot框架的一整套解決方案里面可以通過mybatis和fastjson完成這個操作。在web的前后端數據傳輸中,一般也是用json作為數據的載體,JavaScript有着對json比較完備的支持。

Json格式概述

  • 基礎

    1. 概念: JavaScript Object Notation JavaScript對象表示法
    • json現在多用於存儲和交換文本信息的語法

    • 進行數據的傳輸

    • JSON 比 XML 更小、更快,更易解析。

    1. 語法:

    2. 基本規則

    -  數據在名稱/值對中:json數據是由鍵值對構成的
    
    -  鍵用引號(單雙都行)引起來,也可以不使用引號
    
    -  值得取值類型:
    
      1. 數字(整數或浮點數)
    
      2. 字符串(在雙引號中)
    
      3. 邏輯值(true 或 false)
    
      4. 數組(在方括號中)	{"persons":[{},{}]}
    
      5. 對象(在花括號中) {"address":{"province":"陝西"....}}
    
      6. null
    
    -  數據由逗號分隔:多個鍵值對由逗號分隔
    
    -  花括號保存對象:使用{}定義json 格式
    
    -  方括號保存數組:[]
    
    1. JavaScript獲取數據:
  1. json對象.鍵名

  2. json對象["鍵名"]

  3. 數組對象[索引]

  4. 遍歷img

解析

使用 C# 對 JSON 進行序列化和反序列化 - .NET | Microsoft Docs

會用到兩個名詞,序列化和反序列化,其中序列化是指將實例對象轉換成json格式的字符串,反序列化則是逆向前面序列化的過程。

在序列化的過程中,默認情況下會只序列化公共讀寫的屬性,可以通過System.Text.Json.SerializationJsonInclude特性或者JsonSerializerOptionsIncludeFields屬性來包含公有字段。通過System.Text.Json.SerializationJsonInclude特性可以來自定義可以序列化的非公共屬性訪問器(即屬性的訪問修飾符為public,但是set訪問器和get訪問器的任意一方為非public)。這可能對使用慣了java的人來說不適應,事實上這是一種很合理的序列化要求,默認狀況下,序列化器會序列化對象中的所有可讀屬性,反序列化所有可寫屬性,這種方式尊重了訪問修飾符的作用。也可用開源的Newtonsoft.Json來序列化非公有屬性。現在很多編程語言(包括.NET)能通過反射來獲取私有屬性本身就是不合理的,從.NET core能明顯的感覺到.NET團隊出於安全的考慮在限制反射的使用。

需要用到的namespace

using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Unicode;
  • System.Text.JsonJsonSerializer.Serialize方法可以進行序列化
  • System.Text.JsonJsonSerializer.Deserialize方法可以進行反序列化
  • System.Text.Json.Serialization可以要序列化的類添加必要的特性,比如JsonPropertyName為屬性序列化時重命名,再比如JsonInclude來定義序列化時要包含的字段。
  • System.Text.Encodings.WebSystem.Text.Unicode來讓特定的字符集在序列化的時候能夠正常序列化而不是被轉義成為 \uxxxx,其中 xxxx 為字符的 Unicode 代碼。事實上,默認情況下,序列化程序會轉義所有非 ASCII 字符。

序列化

只將實例化對象轉變成json字符串,假設有一個實例化對象weatherForecast,序列化方式如下:

string jsonString = JsonSerializer.Serialize(weatherForecast);

反序列化

指將json字符串序列化成實例化對象,書接前文,方式如下:

weatherForecast = JsonSerializer.Deserialize<WeatherForecastWithPOCOs>(jsonString);

JsonSerializerOptions

可以通過JsonSerializerOptions來指定諸如是否整齊打印和忽略Null值屬性等信息。使用方式為將JsonSerializerOptions實例化之后再當作JsonSerializer.SerializeJsonSerializer.Deserialize的參數。

關於JsonSerializerOptions的屬性可以查看如何使用 System.Text.Json 實例化 JsonSerializerOptions | Microsoft Docs

先實例化一個JsonSerializerOptions對象,在初始化器里面定義各種屬性

JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions()
{
    // 整齊打印
    WriteIndented = true,
    // 忽略值為Null的屬性
    IgnoreNullValues = true,
    // 設置Json字符串支持的編碼,默認情況下,序列化程序會轉義所有非 ASCII 字符。 即,會將它們替換為 \uxxxx,其中 xxxx 為字符的 Unicode
    // 代碼。 可以通過設置Encoder來讓生成的josn字符串不轉義指定的字符集而進行序列化 下面指定了基礎拉丁字母和中日韓統一表意文字的基礎Unicode 塊
    // (U+4E00-U+9FCC)。 基本涵蓋了除使用西里爾字母以外所有西方國家的文字和亞洲中日韓越的文字
    Encoder = JavaScriptEncoder.Create(UnicodeRanges.BasicLatin, UnicodeRanges.CjkUnifiedIdeographs),
    // 反序列化不區分大小寫
    PropertyNameCaseInsensitive = true,
    // 駝峰命名
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,

    // 對字典的鍵進行駝峰命名
    DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
    // 序列化的時候忽略null值屬性
    DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    // 忽略只讀屬性,因為只讀屬性只能序列化而不能反序列化,所以在以json為儲存數據的介質的時候,序列化只讀屬性意義不大
    IgnoreReadOnlyFields = true,
    // 不允許結尾有逗號的不標准json
    AllowTrailingCommas = false,
    // 不允許有注釋的不標准json
    ReadCommentHandling = JsonCommentHandling.Disallow,
    // 允許在反序列化的時候原本應為數字的字符串(帶引號的數字)轉為數字
    NumberHandling = JsonNumberHandling.AllowReadingFromString,
    // 處理循環引用類型,比如Book類里面有一個屬性也是Book類
    ReferenceHandler = ReferenceHandler.Preserve
};

然后在序列化和反序列化的時候jsonSerializerOptions對象當作參數傳給JsonSerializer.SerializeJsonSerializer.Deserialize

string jsonBookA = JsonSerializer.Serialize(bookA, jsonSerializerOptions);
// 反序列化
BookA bookA1 = JsonSerializer.Deserialize<BookA>(jsonBookA, jsonSerializerOptions);

JsonSerializerOptions 常用屬性概述

作用 值類型
WriteIndented 整齊打印,將此值設置為true后序列化的json字符串在打印的時候會進行自動縮進和換行。默認為false。 bool
IgnoreNullValues 忽略值為Null的屬性。默認為false。 bool
Encoder 設置Json字符串支持的編碼,默認情況下,序列化程序會轉義所有非 ASCII 字符。 即,會將它們替換為 \uxxxx,其中 xxxx 為字符的 Unicode代碼。 可以通過設置Encoder來讓生成的josn字符串不轉義指定的字符集而進行序列化。可設置為Encoder = JavaScriptEncoder.Create(UnicodeRanges.BasicLatin, UnicodeRanges.CjkUnifiedIdeographs)來包含除使用西里爾字母以外所有西方國家的文字和亞洲中日韓越的文字 JavaScriptEncoder
PropertyNameCaseInsensitive 反序列化不區分鍵的大小寫。默認為false。 bool
PropertyNamingPolicy 序列化時屬性的命名方式,常用的為JsonNamingPolicy.CamelCase設置成小寫字母開頭的駝峰命名。 JsonNamingPolicy
DictionaryKeyPolicy 序列化時對字典的string鍵進行小寫字母開頭的駝峰駝峰命名。 JsonNamingPolicy
DefaultIgnoreCondition 指定一個條件,用於確定何時在序列化或反序列化過程中忽略具有默認值的屬性。 默認值為 Never。常用值為JsonIgnoreCondition.WhenWritingDefault來忽略默認值屬性。 JsonIgnoreCondition
IgnoreReadOnlyProperties 序列化時忽略只讀屬性,因為只讀屬性只能序列化而不能反序列化,所以在以json為儲存數據的介質的時候,序列化只讀屬性意義不大。默認為false。 bool
AllowTrailingCommas 反序列化時,允許結尾有逗號的不標准json,默認為false。 bool
ReadCommentHandling 反序列化時,允許有注釋的不標准json,默認為false。 bool
NumberHandling 使用NumberHandling = JsonNumberHandling.AllowReadingFromString可允許在反序列化的時候原本應為數字的字符串(帶引號的數字)轉為數字 JsonNumberHandling
ReferenceHandler 配置在讀取和寫入 JSON 時如何處理對象引用。使用ReferenceHandler = ReferenceHandler.Preserve仍然會在序列化和反序列化的時候保留引用並處理循環引用。 ReferenceHandler
IncludeFields 確定是否在序列化和反序列化期間處理字段。 默認值為 false bool

System.Text.Json.Serialization 特性

可以為將要序列化和被反序列化而生成的類的屬性和字段添加特性。

JsonInclude 包含特定public字段和非公共屬性訪問器

在序列化或反序列化時,使用 JsonSerializerOptions.IncludeFields 全局設置或 [JsonInclude] 特性來包含字段(必須是public),當應用於某個屬性時,指示非公共的 getter 和 setter 可用於序列化和反序列化。 不支持非公共屬性。

demo:

/// <summary>
/// 時間戳
/// </summary>
[JsonInclude]
public long timestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds();

/// <summary>
/// 書的名稱
/// </summary>
[JsonInclude]
public string Name { private get; set; } = "《書名》";

JsonPropertyName 自定義屬性名稱

若要設置單個屬性的名稱,請使用 [JsonPropertyName] 特性。

此特性設置的屬性名稱:

  • 同時適用於兩個方向(序列化和反序列化)。
  • 優先於屬性命名策略。

demo:

/// <summary>
/// 作者
/// </summary>
[JsonPropertyName("作者")]
public string Author
{
    get { return _author; }
    set { _author = value; }
}

JsonIgnore 忽略單個屬性

阻止對屬性進行序列化或反序列化。

demo:

/// <summary>
/// 書的出版商
/// </summary>
[JsonIgnore]
public string OutCompany { get => _outCompany; set => _outCompany = value; }

JsonExtensionData 處理溢出 JSON

反序列化時,可能會在 JSON 中收到不是由目標類型的屬性表示的數據。可以將這些無法由目標類型的屬性表示的數據儲存在一個Dictionary<string, JsonElement>字典里面,方式如下:

/// <summary>
/// 儲存反序列化時候的溢出數據
/// </summary>
[JsonExtensionData]
public Dictionary<string, JsonElement> ExtensionData { get; set; }

筆者的選擇

在筆者的開發經驗當中,json用的最多的就是前后端數據傳輸和數據庫儲存數據。對jsonSerializerOptions往往會選擇這幾個選項:

JsonSerializerOptions jsonSerializerOptions = new JsonSerializerOptions()
{
    // 整齊打印
    WriteIndented = true,
    // 忽略值為Null的屬性
    IgnoreNullValues = true,
    // 設置Json字符串支持的編碼,默認情況下,序列化程序會轉義所有非 ASCII 字符。 即,會將它們替換為 \uxxxx,其中 xxxx 為字符的 Unicode
    // 代碼。 可以通過設置Encoder來讓生成的josn字符串不轉義指定的字符集而進行序列化 下面指定了基礎拉丁字母和中日韓統一表意文字的基礎Unicode 塊
    // (U+4E00-U+9FCC)。 基本涵蓋了除使用西里爾字母以外所有西方國家的文字和亞洲中日韓越的文字
    Encoder = JavaScriptEncoder.Create(UnicodeRanges.BasicLatin, UnicodeRanges.CjkUnifiedIdeographs, UnicodeRanges.CjkSymbolsandPunctuation),
    // 反序列化不區分大小寫
    PropertyNameCaseInsensitive = true,
    // 駝峰命名
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    // 對字典的鍵進行駝峰命名
    DictionaryKeyPolicy = JsonNamingPolicy.CamelCase,
    // 忽略只讀屬性,因為只讀屬性只能序列化而不能反序列化,所以在以json為儲存數據的介質的時候,序列化只讀屬性意義不大
    IgnoreReadOnlyProperties  = true,
    // 允許在反序列化的時候原本應為數字的字符串(帶引號的數字)轉為數字
    NumberHandling = JsonNumberHandling.AllowReadingFromString
};

盡量不使用JsonPropertyName特性,對有可能會用到json反序列化的類一定會用到JsonExtensionData特性來儲存可能存在的溢出數據。JsonIgnoreJsonInclude會廣泛的使用而不用JsonSerializerOptionsIncludeFields來序列化所有字段。

LICENSE

已將所有引用其他文章之內容清楚明白地標注,其他部分皆為作者勞動成果。對作者勞動成果做以下聲明:

copyright © 2021 蘇月晟,版權所有。

知識共享許可協議
作品蘇月晟采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM