1. 程式人生 > 其它 >c# 陣列和集合精講

c# 陣列和集合精講

精講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. 遍歷

解析

使用 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
IgnoreReadOnlyFields 序列化時忽略只讀屬性,因為只讀屬性只能序列化而不能反序列化,所以在以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為儲存資料的介質的時候,序列化只讀屬性意義不大
    IgnoreReadOnlyFields = true,
    // 允許在反序列化的時候原本應為數字的字串(帶引號的數字)轉為數字
    NumberHandling = JsonNumberHandling.AllowReadingFromString
};

儘量不使用JsonPropertyName特性,對有可能會用到json反序列化的類一定會用到JsonExtensionData特性來儲存可能存在的溢位資料。JsonIgnoreJsonInclude會廣泛的使用而不用JsonSerializerOptionsIncludeFields來序列化所有欄位。

LICENSE

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

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


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

本文作者:蘇月晟,轉載請註明原文連結:https://www.cnblogs.com/sogeisetsu/p/15618843.html