C#中的数组与集合类型详解
C# 中的数组
数组基础
在 C# 中,数组是一种用于存储多个相同类型元素的固定大小的数据结构。它在内存中是连续存储的,这使得对数组元素的访问非常高效。数组的声明包括数据类型、数组名和方括号 []
。例如,声明一个整数数组:
int[] numbers;
这里只是声明了数组变量 numbers
,并没有为其分配内存来存储实际的元素。要分配内存并初始化数组,可以使用 new
关键字:
numbers = new int[5];
这行代码创建了一个可以容纳 5 个整数的数组。数组的索引从 0 开始,所以可以通过索引来访问和赋值数组元素:
numbers[0] = 10;
numbers[1] = 20;
numbers[2] = 30;
numbers[3] = 40;
numbers[4] = 50;
也可以在声明数组的同时进行初始化:
int[] numbers = new int[] { 10, 20, 30, 40, 50 };
或者更简洁地:
int[] numbers = { 10, 20, 30, 40, 50 };
多维数组
除了一维数组,C# 还支持多维数组。多维数组有两种类型:矩形数组和交错数组。
矩形数组
矩形数组的每一维都有相同数量的元素,就像一个矩形。声明和初始化一个二维矩形数组的示例如下:
int[,] matrix = new int[3, 4];
matrix[0, 0] = 1;
matrix[0, 1] = 2;
// 其他元素赋值
也可以在声明时初始化:
int[,] matrix = {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }
};
访问矩形数组元素时,需要提供每一维的索引,例如 matrix[1, 2]
表示第二行第三列的元素。
交错数组
交错数组是数组的数组,即每一维的数组大小可以不同。声明和初始化一个二维交错数组的示例如下:
int[][] jaggedArray = new int[3][];
jaggedArray[0] = new int[2];
jaggedArray[1] = new int[3];
jaggedArray[2] = new int[4];
jaggedArray[0][0] = 1;
jaggedArray[0][1] = 2;
// 其他元素赋值
初始化时也可以这样写:
int[][] jaggedArray = {
new int[] { 1, 2 },
new int[] { 3, 4, 5 },
new int[] { 6, 7, 8, 9 }
};
访问交错数组元素时,先指定外层数组的索引,再指定内层数组的索引,如 jaggedArray[1][2]
表示第二个内层数组的第三个元素。
数组的属性和方法
数组有一些有用的属性和方法。
Length 属性
Length
属性返回数组的总元素个数。对于一维数组,它就是数组的大小;对于多维矩形数组,它是所有维数大小的乘积;对于交错数组,它是外层数组的大小。
int[] numbers = { 1, 2, 3, 4, 5 };
Console.WriteLine(numbers.Length); // 输出 5
int[,] matrix = {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }
};
Console.WriteLine(matrix.Length); // 输出 12
int[][] jaggedArray = {
new int[] { 1, 2 },
new int[] { 3, 4, 5 },
new int[] { 6, 7, 8, 9 }
};
Console.WriteLine(jaggedArray.Length); // 输出 3
GetLength 方法
GetLength
方法用于获取指定维度的大小。对于一维数组,它等同于 Length
属性。对于多维数组,可以通过传入维度索引来获取相应维度的大小。
int[,] matrix = {
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8 },
{ 9, 10, 11, 12 }
};
Console.WriteLine(matrix.GetLength(0)); // 输出 3,第一维大小
Console.WriteLine(matrix.GetLength(1)); // 输出 4,第二维大小
Clone 方法
Clone
方法用于创建数组的浅表副本。浅表副本意味着新数组中的元素引用与原数组相同(对于引用类型),而对于值类型则是复制值。
int[] numbers = { 1, 2, 3, 4, 5 };
int[] clonedNumbers = (int[])numbers.Clone();
C# 中的集合类型
泛型集合基础
虽然数组在 C# 中很有用,但它们有固定的大小,并且缺乏一些高级的功能。这就是集合类型发挥作用的地方。C# 提供了丰富的泛型集合类型,这些集合类型在 System.Collections.Generic
命名空间中。泛型集合允许指定集合中元素的类型,从而提供类型安全。
List
List<T>
是最常用的泛型集合之一,它表示可动态调整大小的对象列表。
创建和初始化
可以通过以下方式创建和初始化 List<T>
:
List<int> numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);
也可以在创建时初始化:
List<int> numbers = new List<int> { 10, 20, 30 };
常用操作
- 添加元素:除了
Add
方法逐个添加元素外,还可以使用AddRange
方法添加多个元素:
List<int> numbers1 = new List<int> { 1, 2, 3 };
List<int> numbers2 = new List<int> { 4, 5, 6 };
numbers1.AddRange(numbers2);
- 访问元素:可以像数组一样通过索引访问
List<T>
中的元素:
List<int> numbers = new List<int> { 10, 20, 30 };
int firstNumber = numbers[0];
- 删除元素:
Remove
方法用于删除指定的元素,RemoveAt
方法用于删除指定索引处的元素:
List<int> numbers = new List<int> { 10, 20, 30 };
numbers.Remove(20);
numbers.RemoveAt(1);
- 查找元素:
Contains
方法用于检查集合中是否包含指定元素,IndexOf
方法用于查找元素的索引:
List<int> numbers = new List<int> { 10, 20, 30 };
bool contains = numbers.Contains(20);
int index = numbers.IndexOf(30);
Dictionary<TKey, TValue>
Dictionary<TKey, TValue>
表示键值对的集合,其中每个键是唯一的。
创建和初始化
Dictionary<string, int> ages = new Dictionary<string, int>();
ages.Add("Alice", 25);
ages.Add("Bob", 30);
或者在创建时初始化:
Dictionary<string, int> ages = new Dictionary<string, int> {
{ "Alice", 25 },
{ "Bob", 30 }
};
常用操作
- 添加和访问元素:通过键来添加和访问值。如果尝试添加已存在的键,会抛出
ArgumentException
。
Dictionary<string, int> ages = new Dictionary<string, int> {
{ "Alice", 25 },
{ "Bob", 30 }
};
int aliceAge = ages["Alice"];
ages["Charlie"] = 35;
- 删除元素:使用
Remove
方法通过键删除键值对:
Dictionary<string, int> ages = new Dictionary<string, int> {
{ "Alice", 25 },
{ "Bob", 30 }
};
ages.Remove("Bob");
- 检查键是否存在:使用
ContainsKey
方法检查字典中是否包含指定的键:
Dictionary<string, int> ages = new Dictionary<string, int> {
{ "Alice", 25 },
{ "Bob", 30 }
};
bool hasAlice = ages.ContainsKey("Alice");
HashSet
HashSet<T>
表示一个集合,其中的元素是唯一的,没有重复。
创建和初始化
HashSet<int> numbers = new HashSet<int>();
numbers.Add(10);
numbers.Add(20);
也可以在创建时初始化:
HashSet<int> numbers = new HashSet<int> { 10, 20, 30 };
常用操作
- 添加元素:
Add
方法用于添加元素,如果元素已存在,则添加操作会失败但不会抛出异常。
HashSet<int> numbers = new HashSet<int> { 10, 20 };
numbers.Add(30);
- 检查元素是否存在:使用
Contains
方法检查集合中是否包含指定元素:
HashSet<int> numbers = new HashSet<int> { 10, 20 };
bool contains = numbers.Contains(20);
- 集合操作:
HashSet<T>
支持一些集合操作,如并集、交集和差集。
HashSet<int> set1 = new HashSet<int> { 1, 2, 3 };
HashSet<int> set2 = new HashSet<int> { 3, 4, 5 };
HashSet<int> union = new HashSet<int>(set1);
union.UnionWith(set2);
HashSet<int> intersection = new HashSet<int>(set1);
intersection.IntersectWith(set2);
HashSet<int> difference = new HashSet<int>(set1);
difference.ExceptWith(set2);
Queue
Queue<T>
表示一个先进先出(FIFO)的集合。
创建和初始化
Queue<int> numbers = new Queue<int>();
numbers.Enqueue(10);
numbers.Enqueue(20);
也可以在创建时初始化:
Queue<int> numbers = new Queue<int>(new int[] { 10, 20, 30 });
常用操作
- 添加元素:使用
Enqueue
方法将元素添加到队列的末尾。
Queue<int> numbers = new Queue<int>();
numbers.Enqueue(10);
- 移除元素:
Dequeue
方法移除并返回队列开头的元素,如果队列为空则抛出InvalidOperationException
。Peek
方法返回队列开头的元素但不移除它。
Queue<int> numbers = new Queue<int> { 10, 20, 30 };
int firstNumber = numbers.Dequeue();
int peekedNumber = numbers.Peek();
Stack
Stack<T>
表示一个后进先出(LIFO)的集合。
创建和初始化
Stack<int> numbers = new Stack<int>();
numbers.Push(10);
numbers.Push(20);
也可以在创建时初始化:
Stack<int> numbers = new Stack<int>(new int[] { 10, 20, 30 });
常用操作
- 添加元素:使用
Push
方法将元素添加到栈的顶部。
Stack<int> numbers = new Stack<int>();
numbers.Push(10);
- 移除元素:
Pop
方法移除并返回栈顶的元素,如果栈为空则抛出InvalidOperationException
。Peek
方法返回栈顶的元素但不移除它。
Stack<int> numbers = new Stack<int> { 10, 20, 30 };
int topNumber = numbers.Pop();
int peekedNumber = numbers.Peek();
集合类型的选择
在选择使用哪种集合类型时,需要考虑以下因素:
性能
- List:适合需要频繁插入和删除元素,并且需要通过索引访问元素的场景。它的查找性能相对较低,尤其是对于大型集合,因为查找需要遍历整个列表。
- Dictionary<TKey, TValue>:适合通过键快速查找值的场景,其查找操作平均时间复杂度为 O(1)。但插入和删除操作可能会有一些开销,特别是在需要重新调整哈希表大小时。
- HashSet:适合需要确保元素唯一性的场景,添加、删除和查找操作的平均时间复杂度为 O(1)。
- Queue 和 Stack:适合实现特定的队列和栈数据结构,它们的操作(如入队、出队、入栈、出栈)时间复杂度为 O(1)。
内存使用
- List:在动态添加元素时,可能会多次重新分配内存,导致内存使用不太稳定。
- Dictionary<TKey, TValue>:由于使用哈希表,会有一些额外的内存开销用于存储哈希桶和相关的元数据。
- HashSet:类似
Dictionary<TKey, TValue>
,也使用哈希表,有一定的内存开销。 - Queue 和 Stack:内存使用相对较为直接,主要取决于存储的元素数量。
功能需求
- 如果需要按顺序存储元素并且通过索引访问,
List<T>
是一个好选择。 - 如果需要通过键快速查找值,
Dictionary<TKey, TValue>
是首选。 - 如果需要确保元素唯一,
HashSet<T>
是合适的。 - 如果需要实现先进先出或后进先出的逻辑,分别选择
Queue<T>
和Stack<T>
。
集合类型与数组的转换
数组转集合
C# 提供了方便的方法将数组转换为集合。例如,可以将数组转换为 List<T>
:
int[] numbersArray = { 1, 2, 3, 4, 5 };
List<int> numbersList = new List<int>(numbersArray);
也可以将数组转换为 HashSet<T>
:
int[] numbersArray = { 1, 2, 3, 4, 5 };
HashSet<int> numbersSet = new HashSet<int>(numbersArray);
对于 Dictionary<TKey, TValue>
,如果有合适的键值对数组,可以这样转换:
KeyValuePair<string, int>[] pairsArray = {
new KeyValuePair<string, int>("Alice", 25),
new KeyValuePair<string, int>("Bob", 30)
};
Dictionary<string, int> agesDictionary = new Dictionary<string, int>(pairsArray);
集合转数组
集合类型也可以很容易地转换为数组。List<T>
有 ToArray
方法:
List<int> numbersList = new List<int> { 1, 2, 3, 4, 5 };
int[] numbersArray = numbersList.ToArray();
HashSet<T>
同样有 ToArray
方法:
HashSet<int> numbersSet = new HashSet<int> { 1, 2, 3, 4, 5 };
int[] numbersArray = numbersSet.ToArray();
对于 Dictionary<TKey, TValue>
,可以将键或值转换为数组:
Dictionary<string, int> agesDictionary = new Dictionary<string, int> {
{ "Alice", 25 },
{ "Bob", 30 }
};
string[] keysArray = agesDictionary.Keys.ToArray();
int[] valuesArray = agesDictionary.Values.ToArray();
集合的排序与查找
排序
许多集合类型都支持排序操作。对于 List<T>
,可以使用 Sort
方法进行排序。如果 T
实现了 IComparable<T>
接口,Sort
方法会按照该接口定义的比较规则进行排序。例如,对于整数列表:
List<int> numbers = new List<int> { 3, 1, 4, 1, 5 };
numbers.Sort();
如果 T
没有实现 IComparable<T>
,可以提供一个实现了 IComparer<T>
接口的比较器。例如,假设有一个自定义的类 Person
:
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
class AgeComparer : IComparer<Person>
{
public int Compare(Person x, Person y)
{
return x.Age.CompareTo(y.Age);
}
}
List<Person> people = new List<Person> {
new Person { Name = "Alice", Age = 25 },
new Person { Name = "Bob", Age = 30 },
new Person { Name = "Charlie", Age = 20 }
};
people.Sort(new AgeComparer());
HashSet<T>
本身不支持直接排序,但可以将其转换为 List<T>
后再进行排序。Dictionary<TKey, TValue>
由于其基于键的无序性,一般不进行整体排序,但可以对其键或值进行排序。
查找
除了前面提到的 Contains
和 IndexOf
等方法,集合还支持更复杂的查找操作。例如,List<T>
的 Find
方法可以根据指定的条件查找第一个匹配的元素:
List<int> numbers = new List<int> { 10, 20, 30, 40, 50 };
int foundNumber = numbers.Find(n => n > 30);
FindAll
方法可以查找所有匹配条件的元素并返回一个新的 List<T>
:
List<int> numbers = new List<int> { 10, 20, 30, 40, 50 };
List<int> foundNumbers = numbers.FindAll(n => n > 30);
Dictionary<TKey, TValue>
主要通过键进行查找,但也可以通过遍历值来查找满足特定条件的值。
集合的遍历
foreach 循环
foreach
循环是遍历集合最常用的方式。对于 List<T>
:
List<int> numbers = new List<int> { 10, 20, 30 };
foreach (int number in numbers)
{
Console.WriteLine(number);
}
对于 Dictionary<TKey, TValue>
,可以遍历键值对:
Dictionary<string, int> ages = new Dictionary<string, int> {
{ "Alice", 25 },
{ "Bob", 30 }
};
foreach (KeyValuePair<string, int> pair in ages)
{
Console.WriteLine($"Name: {pair.Key}, Age: {pair.Value}");
}
对于 HashSet<T>
:
HashSet<int> numbers = new HashSet<int> { 10, 20, 30 };
foreach (int number in numbers)
{
Console.WriteLine(number);
}
使用迭代器
集合类型实现了 IEnumerable<T>
接口,该接口提供了 GetEnumerator
方法,返回一个迭代器。可以手动使用迭代器来遍历集合:
List<int> numbers = new List<int> { 10, 20, 30 };
IEnumerator<int> enumerator = numbers.GetEnumerator();
while (enumerator.MoveNext())
{
int number = enumerator.Current;
Console.WriteLine(number);
}
但一般情况下,foreach
循环更简洁和易用,它在编译时会被转换为使用迭代器的代码。
通过深入了解 C# 中的数组和各种集合类型,开发者可以根据具体的需求选择最合适的数据结构,从而提高程序的性能和可读性。无论是简单的数组,还是功能丰富的泛型集合,都在 C# 的编程世界中扮演着重要的角色。