在.NET开发中,集合类型的选择是每个开发者都会面临的日常决策。从简单的数组到复杂的并发集合,.NET Framework和.NET Core提供了丰富的集合类型,每种都有其特定的使用场景、性能特征和潜在陷阱。选择不当不仅会导致性能问题,还可能引发线程安全问题、内存浪费甚至程序崩溃。本文将深入剖析.NET中各种集合类型的特点,通过详细的性能对比和实际代码示例,帮助开发者理解何时使用数组、何时选择List,何时需要并发集合,以及如何避免常见的数据结构陷阱。
数组:基础但常被误解的性能利器
数组是.NET中最基础的集合类型,也是理解其他集合性能特征的基石。许多开发者认为数组已经过时,应该总是使用List
数组的核心优势与性能特征
数组在内存中是连续存储的,这带来了两个关键优势:CPU缓存友好性和确定的内存布局。当遍历数组时,CPU可以高效地预取数据到缓存中,而不需要像链表那样进行指针跳转。此外,数组的内存开销最小——只有元素本身和数组头的开销。
// 数组创建与基本操作示例
int[] numbers = new int[1000];
// 初始化数组元素
for (int i = 0; i < numbers.Length; i++)
{
numbers[i] = i * 2;
}
// 数组的直接访问 - 无边界检查开销(在某些优化场景下)
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
{
sum += numbers[i]; // JIT可能优化为指针算术
}
// 多维数组 vs 交错数组
int[,] matrix = new int[100, 100]; // 连续内存块
int[][] jagged = new int[100][]; // 100个独立数组
for (int i = 0; i < 100; i++)
{
jagged[i] = new int[100];
}
数组的性能陷阱与解决方案
数组最大的陷阱是固定大小特性。许多开发者在需要动态增长时错误地使用数组,导致频繁的重新分配和复制。然而,即使在这种情况下,理解数组的底层行为也能帮助优化List
// 错误示例:手动实现动态数组(性能差)
class BadDynamicArray
{
private int[] data = new int[4];
private int count = 0;
public void Add(int item)
{
if (count == data.Length)
{
// 每次都只增加1个容量 - 导致O(n²)复杂度
int[] newData = new int[data.Length + 1];
Array.Copy(data, newData, data.Length);
data = newData;
}
data[count++] = item;
}
}
// 正确示例:理解List<T>的容量策略
class OptimizedListUsage
{
public List<int> CreateLargeList(int expectedSize)
{
// 预先设置容量,避免多次重新分配
var list = new List<int>(expectedSize);
// List<T>内部容量增长策略:当前容量不足时,容量翻倍
// 这确保了添加n个元素的均摊时间复杂度为O(1)
for (int i = 0; i < expectedSize; i++)
{
list.Add(i);
}
return list;
}
public void ArrayCopyVsListAdd()
{
// 性能对比:Array.Copy vs List.Add
int[] source = new int[10000];
// 方法1:使用Array.Copy(最快,但需要管理索引)
int[] dest1 = new int[10000];
Array.Copy(source, dest1, source.Length);
// 方法2:使用List<T>(方便,但有额外开销)
var list = new List<int>(10000);
foreach (var item in source)
{
list.Add(item);
}
// 方法3:使用LINQ(最慢,但代码最简洁)
var list2 = source.ToList();
}
}
何时应该坚持使用数组
数组在以下场景中仍然是最佳选择:
- 固定大小的集合:当你确切知道元素数量时
- 高性能计算:需要极致性能的数值计算、图像处理等
- 互操作场景:与非托管代码交互时
- 多维数据:真正的多维数组(而非交错数组)在某些算法中更自然
// 数组的最佳实践示例
public class ArrayBestPractices
{
// 场景1:固定大小的查找表
private static readonly string[] DayNames =
{
"Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday", "Sunday"
};
// 场景2:缓冲区池(避免频繁分配)
private readonly Stack<byte[]> bufferPool = new Stack<byte[]>();
public byte[] RentBuffer(int size)
{
lock (bufferPool)
{
if (bufferPool.Count > 0)
{
var buffer = bufferPool.Peek();
if (buffer.Length >= size)
return bufferPool.Pop();
}
}
return new byte[size];
}
public void ReturnBuffer(byte[] buffer)
{
if (buffer == null || buffer.Length == 0) return;
lock (bufferPool)
{
// 限制池大小防止内存泄漏
if (bufferPool.Count < 100)
bufferPool.Push(buffer);
}
}
}
List:动态数组的权衡艺术
List
List的内部机制与性能特征
List
// List<T>内部机制演示
public class ListInternalMechanism
{
public void DemonstrateCapacityBehavior()
{
var list = new List<int>();
// 初始容量为0,第一次添加时容量变为4
list.Add(1); // Capacity = 4
list.Add(2); // Capacity = 4
list.Add(3); // Capacity = 4
list.Add(4); // Capacity = 4
list.Add(5); // Capacity = 8 (扩容)
list.Add(6); // Capacity = 8
list.Add(7); // Capacity = 8
list.Add(8); // Capacity = 8
list.Add(9); // Capacity = 16 (再次扩容)
// 手动设置容量可以避免多次扩容
var optimizedList = new List<int>(1000000);
for (int i = 0; i < 1000000; i++)
{
optimizedList.Add(i);
}
}
// 性能对比:不同初始化策略
public void InitializationPerformanceComparison()
{
const int count = 1000000;
// 方法1:无预设容量(最差)
var list1 = new List<int>();
for (int i = 0; i < count; i++) list1.Add(i);
// 方法2:预设准确容量(最佳)
var list2 = new List<int>(count);
for (int i = 0; i < count; i++) list2.Add(i);
// 方法3:预设过大容量(浪费内存)
var list3 = new List<int>(count * 2);
for (int i = 0; i < count; i++) list3.Add(i);
// 方法4:使用AddRange(内部优化)
var list4 = new List<int>();
list4.AddRange(Enumerable.Range(0, count));
}
}
List的常见陷阱与解决方案
// 陷阱1:在循环中频繁插入/删除元素
public class ListPerformanceTraps
{
// 错误:在循环中插入导致O(n²)复杂度
public void BadInsertionPattern()
{
var list = new List<int> { 1, 2, 3, 4, 5 };
// 每次插入都需要移动后续所有元素
for (int i = 0; i < 1000; i++)
{
list.Insert(0, i); // O(n)操作,1000次就是O(n²)
}
}
// 正确:先收集数据再批量插入
public void GoodInsertionPattern()
{
var list = new List<int> { 1, 2, 3, 4, 5 };
var temp = new List<int>(1000);
for (int i = 0; i < 1000; i++)
{
temp.Add(i);
}
// 批量插入,内部使用Array.Copy优化
list.InsertRange(0, temp);
}
// 陷阱2:使用Remove导致多次数组复制
public void BadRemovalPattern()
{
var list = new List<int>(Enumerable.Range(0, 1000));
// 每次Remove都会移动数组元素
while (list.Count > 0)
{
list.Remove(list[0]); // O(n)操作,n次就是O(n²)
}
}
// 正确:使用RemoveAt或Clear
public void GoodRemovalPattern()
{
var list = new List<int>(Enumerable.Range(0, 1000));
// 如果需要保留顺序
list.RemoveAt(0); // 单次操作
// 如果不需要保留顺序,使用Swap-Remove模式
if (list.Count > 0)
{
list[0] = list[list.Count - 1];
list.RemoveAt(list.Count - 1);
}
// 或者直接清空
list.Clear(); // O(1)操作
}
// 陷阱3:不必要的ToList()调用
public void UnnecessaryToList()
{
IEnumerable<int> source = Enumerable.Range(0, 1000);
// 不必要:如果只是遍历
var list1 = source.ToList(); // 额外分配
foreach (var item in list1) { /* ... */ }
// 更好:直接遍历
foreach (var item in source) { /* ... */ }
// 但如果需要多次访问,ToList()是有益的
var list2 = source.ToList();
var count = list2.Count;
var first = list2[0];
var last = list2[list2.Count - 1];
}
}
List的最佳实践模式
public class ListBestPractices
{
// 模式1:容量规划
public List<string> ProcessItems(IEnumerable<string> items)
{
// 如果知道大致数量,预设容量
if (items is ICollection<string> collection)
{
var result = new List<string>(collection.Count);
result.AddRange(items);
return result;
}
// 否则使用合理初始值
return new List<string>(32) { items };
}
// 模式2:使用List<T>作为临时缓冲区
public void ProcessInBatches()
{
var buffer = new List<int>(1000);
foreach (var item in GetSourceData())
{
buffer.Add(item);
if (buffer.Count == 1000)
{
ProcessBatch(buffer);
buffer.Clear(); // 复用缓冲区
}
}
// 处理剩余数据
if (buffer.Count > 0)
{
ProcessBatch(buffer);
}
}
// 模式3:避免装箱
public void AvoidBoxing()
{
// 错误:IList接口导致值类型装箱
IList<int> list1 = new List<int>();
list1.Add(1); // 装箱发生在这里
// 正确:使用具体类型
List<int> list2 = new List<int>();
list2.Add(1); // 无装箱
// 需要接口时,考虑使用泛型方法
void AddToGenericList<T>(List<T> list, T item)
{
list.Add(item); // 无装箱
}
}
}
Dictionary:哈希表的正确打开方式
Dictionary
Dictionary的内部实现与性能关键点
Dictionary使用哈希表实现,理想情况下查找、插入、删除都是O(1)。但哈希冲突会退化为O(n)。理解其内部结构有助于避免性能陷阱。
// Dictionary内部机制演示
public class DictionaryInternals
{
// 自定义键类型必须正确实现GetHashCode和Equals
public class BadKey
{
public int Id { get; set; }
public string Name { get; set; }
// 错误:只基于Id,但Equals比较所有字段
public override int GetHashCode() => Id.GetHashCode();
public override bool Equals(object obj)
{
var other = obj as BadKey;
return other != null && Id == other.Id && Name == other.Name;
}
// 问题:两个对象可能有相同Id但不同Name,导致哈希冲突
// 但GetHashCode返回相同值,违反哈希契约
}
public class GoodKey
{
public int Id { get; set; }
public string Name { get; set; }
// 正确:基于所有用于相等比较的字段计算哈希
public override int GetHashCode()
{
// 使用HashCode.Combine(.NET Core 2.1+)
return HashCode.Combine(Id, Name);
// 或传统方式
// unchecked
// {
// int hash = 17;
// hash = hash * 31 + Id.GetHashCode();
// hash = hash * 31 + (Name?.GetHashCode() ?? 0);
// return hash;
// }
}
public override bool Equals(object obj)
{
var other = obj as GoodKey;
return other != null && Id == other.Id && Name == other.Name;
}
}
// 性能测试:哈希冲突的影响
public void HashCollisionImpact()
{
// 创建大量冲突的键(所有键哈希相同)
var badDict = new Dictionary<BadKey, int>();
var start = DateTime.Now;
for (int i = 0; i < 10000; i++)
{
// 所有BadKey实例的Id相同,导致哈希冲突
var key = new BadKey { Id = 1, Name = i.ToString() };
badDict[key] = i;
}
var elapsed = DateTime.Now - start;
Console.WriteLine($"BadKey Dictionary: {elapsed.TotalMilliseconds}ms");
// 正确哈希的键
var goodDict = new Dictionary<GoodKey, int>();
start = DateTime.Now;
for (int i = 0; i < 10000; i++)
{
var key = new GoodKey { Id = i, Name = i.ToString() };
goodDict[key] = i;
}
elapsed = DateTime.Now - start;
Console.WriteLine($"GoodKey Dictionary: {elapsed.TotalMilliseconds}ms");
}
}
Dictionary的内存陷阱与优化
public class DictionaryMemoryOptimization
{
// 陷阱1:键类型过大
public class LargeKey
{
public byte[] Data = new byte[1024]; // 1KB的键!
public int Id { get; set; }
public override int GetHashCode() => Id.GetHashCode();
public override bool Equals(object obj) => obj is LargeKey other && Id == other.Id;
}
public void LargeKeyProblem()
{
var dict = new Dictionary<LargeKey, string>();
// 每个条目:键1KB + 值引用 + 哈希桶开销 ≈ 1.2KB
// 1000个条目 ≈ 1.2MB,其中大部分是键的开销
// 解决方案:使用键的ID作为字典键,存储完整对象在值中
var optimized = new Dictionary<int, LargeKey>();
}
// 陷阱2:不必要的装箱
public void BoxingInDictionary()
{
// 错误:接口调用导致装箱
IDictionary<int, string> dict1 = new Dictionary<int, string>();
dict1.Add(1, "one"); // 值类型键会装箱
// 正确:使用具体类型
Dictionary<int, string> dict2 = new Dictionary<int, string>();
dict2.Add(1, "one"); // 无装箱
// 特别注意:TryGetValue的out参数
int key = 1;
string value;
if (dict2.TryGetValue(key, out value)) // 无装箱
{
// ...
}
}
// 陷阱3:容量设置不当
public void CapacityOptimization()
{
// 如果知道最终大小,预设容量
var items = GetItems(); // 假设返回1000个条目
// 错误:多次扩容
var dict1 = new Dictionary<int, string>();
foreach (var item in items) dict1.Add(item.Key, item.Value);
// 正确:预设容量
var dict2 = new Dictionary<int, string>(items.Count);
foreach (var item in items) dict2.Add(item.Key, item.Value);
// 更好:使用LINQ的ToDictionary(内部优化)
var dict3 = items.ToDictionary(x => x.Key, x => x.Value);
}
// 陷阱4:修改集合时的枚举问题
public void ModificationDuringEnumeration()
{
var dict = new Dictionary<int, string>
{
[1] = "one",
[2] = "two",
[3] = "three"
};
// 错误:运行时异常
try
{
foreach (var kvp in dict)
{
if (kvp.Key == 2)
{
dict.Remove(kvp.Key); // InvalidOperationException
}
}
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"错误:{ex.Message}");
}
// 正确:先收集要修改的键
var keysToRemove = new List<int>();
foreach (var kvp in dict)
{
if (kvp.Key == 2)
{
keysToRemove.Add(kvp.Key);
}
}
foreach (var key in keysToRemove)
{
dict.Remove(key);
}
// 或者使用LINQ
dict = dict.Where(kvp => kvp.Key != 2).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
}
Dictionary的高级使用模式
public class DictionaryAdvancedPatterns
{
// 模式1:缓存模式(线程安全)
private readonly Dictionary<int, string> cache = new Dictionary<int, string>();
private readonly object cacheLock = new object();
public string GetCachedValue(int key)
{
// 双重检查锁定
if (cache.TryGetValue(key, out var value))
{
return value;
}
lock (cacheLock)
{
if (cache.TryGetValue(key, out value))
{
return value;
}
value = ExpensiveOperation(key);
cache[key] = value;
return value;
}
}
// 模式2:分组数据(使用Lookup)
public void GroupingPattern()
{
var items = new[]
{
new { Category = "A", Value = 1 },
new { Category = "B", Value = 2 },
new { Category = "A", Value = 3 }
};
// 使用Dictionary手动分组
var grouped = new Dictionary<string, List<int>>();
foreach (var item in items)
{
if (!grouped.ContainsKey(item.Category))
{
grouped[item.Category] = new List<int>();
}
grouped[item.Category].Add(item.Value);
}
// 更优雅:使用Lookup(不可变,适合多次查询)
var lookup = items.ToLookup(x => x.Category, x => x.Value);
foreach (var group in lookup["A"])
{
Console.WriteLine(group); // 输出1, 3
}
}
// 模式3:并发字典的替代方案
public void ConcurrentDictionaryAlternatives()
{
// 场景:读多写少,可以使用ReaderWriterLockSlim
var dict = new Dictionary<int, string>();
var rwLock = new System.Threading.ReaderWriterLockSlim();
// 读操作
rwLock.EnterReadLock();
try
{
if (dict.TryGetValue(1, out var value))
{
// ...
}
}
finally
{
rwLock.ExitReadLock();
}
// 写操作
rwLock.EnterWriteLock();
try
{
dict[1] = "new value";
}
finally
{
rwLock.ExitWriteLock();
}
}
private string ExpensiveOperation(int key)
{
System.Threading.Thread.Sleep(100);
return $"value for {key}";
}
}
HashSet:集合运算的利器
HashSet
HashSet的实现与性能特征
public class HashSetPerformance
{
// HashSet vs List.Contains 性能对比
public void PerformanceComparison()
{
var size = 100000;
var data = Enumerable.Range(0, size).ToArray();
var searchValues = Enumerable.Range(size / 2, 1000).ToArray();
// 使用List(O(n)查找)
var list = new List<int>(data);
var sw = Stopwatch.StartNew();
foreach (var val in searchValues)
{
list.Contains(val); // 每次O(n)
}
sw.Stop();
Console.WriteLine($"List.Contains: {sw.ElapsedMilliseconds}ms");
// 使用HashSet(O(1)查找)
var hashSet = new HashSet<int>(data);
sw.Restart();
foreach (var val in searchValues)
{
hashSet.Contains(val); // 每次O(1)
}
sw.Stop();
Console.WriteLine($"HashSet.Contains: {sw.ElapsedMilliseconds}ms");
}
// 集合运算示例
public void SetOperations()
{
var setA = new HashSet<int> { 1, 2, 3, 4, 5 };
var setB = new HashSet<int> { 4, 5, 6, 7, 8 };
// 并集
var union = new HashSet<int>(setA);
union.UnionWith(setB); // {1,2,3,4,5,6,7,8}
// 交集
var intersection = new HashSet<int>(setA);
intersection.IntersectWith(setB); // {4,5}
// 差集
var difference = new HashSet<int>(setA);
difference.ExceptWith(setB); // {1,2,3}
// 对称差集
var symmetric = new HashSet<int>(setA);
symmetric.SymmetricExceptWith(setB); // {1,2,3,6,7,8}
// 检查子集/超集
bool isSubset = setA.IsSubsetOf(setB); // false
bool isSuperset = setA.IsSupersetOf(setB); // false
bool overlaps = setA.Overlaps(setB); // true
}
}
HashSet的自定义比较器
public class HashSetCustomComparers
{
// 场景:基于对象属性去重
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public override bool Equals(object obj)
{
return obj is Person other && Name == other.Name && Age == other.Age;
}
public override int GetHashCode()
{
return HashCode.Combine(Name, Age);
}
}
public void CustomEqualityComparison()
{
// 使用自定义比较器基于Name去重(忽略Age)
var comparer = new NameOnlyComparer();
var set = new HashSet<Person>(comparer);
set.Add(new Person { Name = "Alice", Age = 25 });
set.Add(new Person { Name = "Alice", Age = 30 }); // 会被认为是重复的
set.Add(new Person { Name = "Bob", Age = 25 });
Console.WriteLine(set.Count); // 输出2
}
private class NameOnlyComparer : IEqualityComparer<Person>
{
public bool Equals(Person x, Person y)
{
return x?.Name == y?.Name;
}
public int GetHashCode(Person obj)
{
return obj?.Name?.GetHashCode() ?? 0;
}
}
// 使用IEqualityComparer<T>的通用模式
public void GenericComparerPattern<T>(IEnumerable<T> source, IEqualityComparer<T> comparer)
{
var uniqueItems = new HashSet<T>(source, comparer);
// ...
}
}
SortedSet与SortedList:有序集合的选择
当需要有序存储时,SortedSet和SortedList提供了不同的权衡。
SortedSet的特性与使用
public class SortedSetExamples
{
// SortedSet保持元素有序,适合范围查询
public void RangeOperations()
{
var set = new SortedSet<int> { 5, 2, 8, 1, 9, 3 };
// 自动排序:{1, 2, 3, 5, 8, 9}
// 范围查询
var range = set.GetViewBetween(3, 8); // {3, 5, 8}
// 最小/最大值
int min = set.Min; // 1
int max = set.Max; // 9
// 添加重复元素会被忽略
set.Add(5); // 返回false,集合不变
// 移除范围
set.RemoveWhere(x => x > 5); // 移除8,9
}
// 自定义排序
public void CustomOrdering()
{
// 按字符串长度排序
var set = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
set.Add("apple");
set.Add("Banana");
set.Add("cherry");
// 输出:apple, Banana, cherry(按长度,忽略大小写)
}
}
SortedList vs SortedDictionary
public class SortedCollectionsComparison
{
// SortedList:基于数组,内存效率高,但插入/删除慢
// SortedDictionary:基于红黑树,插入/删除快,但内存开销大
public void PerformanceCharacteristics()
{
// SortedList适合:读多写少,需要索引访问
var list = new SortedList<int, string>();
for (int i = 0; i < 1000; i++)
{
list.Add(i, $"value{i}");
}
// 可以通过索引访问
var kvp = list[500]; // O(log n)查找,但索引访问是O(1)
// SortedDictionary适合:频繁插入删除
var dict = new SortedDictionary<int, string>();
for (int i = 0; i < 1000; i++)
{
dict.Add(i, $"value{i}");
}
// 无法通过索引访问,只能遍历
}
}
并发集合:线程安全的正确实现
并发集合(System.Collections.Concurrent)是.NET 4.0引入的专门用于多线程环境的集合。理解它们的内部机制对避免性能陷阱至关重要。
ConcurrentDictionary:最常用的并发集合
public class ConcurrentDictionaryDeepDive
{
// 基本使用
public void BasicUsage()
{
var dict = new ConcurrentDictionary<int, string>();
// 添加或更新
dict.AddOrUpdate(1, "one", (key, oldValue) => oldValue + " updated");
// 获取或添加
var value = dict.GetOrAdd(2, "two");
// 尝试获取
if (dict.TryGetValue(1, out var result))
{
Console.WriteLine(result);
}
// 更新(需要检查是否存在)
dict.AddOrUpdate(1, "new", (key, old) => "updated");
}
// 高级用法:GetOrAdd的延迟初始化
public void LazyInitializationPattern()
{
var cache = new ConcurrentDictionary<int, Lazy<string>>();
// 多个线程同时调用时,只有第一个会执行工厂函数
var value = cache.GetOrAdd(1, new Lazy<string>(() =>
{
Console.WriteLine("Factory executed");
System.Threading.Thread.Sleep(1000);
return "expensive value";
})).Value; // 只有在.Value访问时才真正执行
}
// 性能陷阱:过度使用AddOrUpdate
public void AddOrUpdatePerformance()
{
var dict = new ConcurrentDictionary<int, int>();
// 错误:高竞争下的性能问题
Parallel.For(0, 1000, i =>
{
// 每次调用都可能触发重试循环
dict.AddOrUpdate(i % 10, 1, (key, old) => old + 1);
});
// 更好:使用TryUpdate配合循环
var counters = new ConcurrentDictionary<int, int>();
Parallel.For(0, 1000, i =>
{
int key = i % 10;
while (true)
{
if (counters.TryGetValue(key, out var current))
{
if (counters.TryUpdate(key, current + 1, current))
break;
}
else
{
if (counters.TryAdd(key, 1))
break;
}
}
});
}
}
其他并发集合的适用场景
public class OtherConcurrentCollections
{
// ConcurrentQueue:FIFO,无锁队列
public void QueueExample()
{
var queue = new ConcurrentQueue<int>();
// 生产者
Parallel.For(0, 100, i => queue.Enqueue(i));
// 消费者
while (queue.TryDequeue(out var item))
{
Process(item);
}
}
// ConcurrentStack:LIFO,无锁栈
public void StackExample()
{
var stack = new ConcurrentStack<int>();
Parallel.For(0, 100, i => stack.Push(i));
while (stack.TryPop(out var item))
{
Process(item);
}
}
// ConcurrentBag:无序集合,适合线程本地存储
public void BagExample()
{
var bag = new ConcurrentBag<int>();
Parallel.For(0, 100, i =>
{
bag.Add(i);
// 每个线程有自己的存储,减少竞争
});
while (bag.TryTake(out var item))
{
Process(item);
}
}
// BlockingCollection:生产者-消费者模式
public void BlockingCollectionExample()
{
var collection = new BlockingCollection<int>(boundedCapacity: 10);
// 生产者任务
var producer = Task.Run(() =>
{
for (int i = 0; i < 100; i++)
{
collection.Add(i); // 如果满则阻塞
Thread.Sleep(10);
}
collection.CompleteAdding(); // 信号:生产完成
});
// 消费者任务
var consumer = Task.Run(() =>
{
foreach (var item in collection.GetConsumingEnumerable())
{
Process(item); // 如果空则阻塞,直到CompleteAdding
}
});
Task.WaitAll(producer, consumer);
}
private void Process(int item)
{
// 模拟处理
Thread.Sleep(1);
}
}
性能对比:实际基准测试
让我们通过实际的基准测试代码来比较不同集合的性能。
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
public class CollectionBenchmark
{
public static void RunAllBenchmarks()
{
Console.WriteLine("=== 集合性能基准测试 ===\n");
BenchmarkAddOperations();
BenchmarkLookupOperations();
BenchmarkMemoryUsage();
BenchmarkThreadSafety();
}
private static void BenchmarkAddOperations()
{
Console.WriteLine("1. 添加操作性能 (100,000次)");
const int count = 100000;
// 数组(预分配)
var sw = Stopwatch.StartNew();
var array = new int[count];
for (int i = 0; i < count; i++) array[i] = i;
sw.Stop();
Console.WriteLine($" 数组(预分配): {sw.ElapsedMilliseconds}ms");
// List<T>(预分配容量)
sw.Restart();
var list = new List<int>(count);
for (int i = 0; i < count; i++) list.Add(i);
sw.Stop();
Console.WriteLine($" List<T>(预分配): {sw.ElapsedMilliseconds}ms");
// List<T>(无预分配)
sw.Restart();
var list2 = new List<int>();
for (int i = 0; i < count; i++) list2.Add(i);
sw.Stop();
Console.WriteLine($" List<T>(无预分配): {sw.ElapsedMilliseconds}ms");
// Dictionary
sw.Restart();
var dict = new Dictionary<int, int>(count);
for (int i = 0; i < count; i++) dict[i] = i;
sw.Stop();
Console.WriteLine($" Dictionary: {sw.ElapsedMilliseconds}ms");
// HashSet
sw.Restart();
var hashSet = new HashSet<int>();
for (int i = 0; i < count; i++) hashSet.Add(i);
sw.Stop();
Console.WriteLine($" HashSet: {sw.ElapsedMilliseconds}ms");
Console.WriteLine();
}
private static void BenchmarkLookupOperations()
{
Console.WriteLine("2. 查找操作性能 (1,000,000次)");
const int count = 100000;
const int lookups = 1000000;
// 准备数据
var array = Enumerable.Range(0, count).ToArray();
var list = new List<int>(array);
var dict = array.ToDictionary(x => x);
var hashSet = new HashSet<int>(array);
var random = new Random(42);
var searchKeys = Enumerable.Range(0, lookups).Select(_ => random.Next(count)).ToArray();
// 数组(线性搜索)
var sw = Stopwatch.StartNew();
foreach (var key in searchKeys)
{
Array.IndexOf(array, key);
}
sw.Stop();
Console.WriteLine($" 数组线性搜索: {sw.ElapsedMilliseconds}ms");
// List(线性搜索)
sw.Restart();
foreach (var key in searchKeys)
{
list.IndexOf(key);
}
sw.Stop();
Console.WriteLine($" List线性搜索: {sw.ElapsedMilliseconds}ms");
// List(二分搜索 - 需要排序)
var sortedList = new List<int>(array);
sortedList.Sort();
sw.Restart();
foreach (var key in searchKeys)
{
sortedList.BinarySearch(key);
}
sw.Stop();
Console.WriteLine($" List二分搜索: {sw.ElapsedMilliseconds}ms");
// Dictionary
sw.Restart();
foreach (var key in searchKeys)
{
dict.ContainsKey(key);
}
sw.Stop();
Console.WriteLine($" Dictionary查找: {sw.ElapsedMilliseconds}ms");
// HashSet
sw.Restart();
foreach (var key in searchKeys)
{
hashSet.Contains(key);
}
sw.Stop();
Console.WriteLine($" HashSet查找: {sw.ElapsedMilliseconds}ms");
Console.WriteLine();
}
private static void BenchmarkMemoryUsage()
{
Console.WriteLine("3. 内存使用 (10,000个元素)");
const int count = 10000;
// 测量内存前强制GC
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
var before = GC.GetTotalMemory(true);
var array = new int[count];
for (int i = 0; i < count; i++) array[i] = i;
var afterArray = GC.GetTotalMemory(true);
Console.WriteLine($" 数组: {(afterArray - before):N0} bytes");
var list = new List<int>(count);
for (int i = 0; i < count; i++) list.Add(i);
var afterList = GC.GetTotalMemory(true);
Console.WriteLine($" List: {(afterList - afterArray):N0} bytes");
var dict = new Dictionary<int, int>(count);
for (int i = 0; i < count; i++) dict[i] = i;
var afterDict = GC.GetTotalMemory(true);
Console.WriteLine($" Dictionary: {(afterDict - afterList):N0} bytes");
var hashSet = new HashSet<int>();
for (int i = 0; i < count; i++) hashSet.Add(i);
var afterHashSet = GC.GetTotalMemory(true);
Console.WriteLine($" HashSet: {(afterHashSet - afterDict):N0} bytes");
Console.WriteLine();
}
private static void BenchmarkThreadSafety()
{
Console.WriteLine("4. 并发添加性能 (10线程 × 10,000次)");
const int perThread = 10000;
const int threadCount = 10;
// 非线程安全List(加锁)
var list = new List<int>();
var listLock = new object();
var sw = Stopwatch.StartNew();
Parallel.For(0, threadCount, _ =>
{
for (int i = 0; i < perThread; i++)
{
lock (listLock)
{
list.Add(i);
}
}
});
sw.Stop();
Console.WriteLine($" List + lock: {sw.ElapsedMilliseconds}ms");
// ConcurrentBag
var bag = new ConcurrentBag<int>();
sw.Restart();
Parallel.For(0, threadCount, _ =>
{
for (int i = 0; i < perThread; i++)
{
bag.Add(i);
}
});
sw.Stop();
Console.WriteLine($" ConcurrentBag: {sw.ElapsedMilliseconds}ms");
// ConcurrentDictionary
var dict = new ConcurrentDictionary<int, int>();
sw.Restart();
Parallel.For(0, threadCount, threadId =>
{
for (int i = 0; i < perThread; i++)
{
dict[threadId * perThread + i] = i;
}
});
sw.Stop();
Console.WriteLine($" ConcurrentDictionary: {sw.ElapsedMilliseconds}ms");
Console.WriteLine();
}
}
// 运行基准测试
// CollectionBenchmark.RunAllBenchmarks();
实际场景下的选择指南
基于上述分析,以下是不同场景下的集合选择建议:
场景1:固定大小的只读数据
推荐:数组
// 配置、映射表等
private static readonly string[] ConfigValues = { "val1", "val2", "val3" };
场景2:动态增长的集合
推荐:List
var list = new List<int>(expectedCount);
// 填充数据...
场景3:快速查找
推荐:Dictionary
var lookup = new Dictionary<string, User>();
// 注意键的选择和哈希实现
场景4:去重和集合运算
推荐:HashSet
var unique = new HashSet<int>(source);
场景5:多线程环境
推荐:并发集合
// 读多写少:ConcurrentDictionary
// 生产者-消费者:BlockingCollection
// 无序收集:ConcurrentBag
场景6:需要有序
推荐:SortedList或SortedDictionary
// 需要索引访问:SortedList
// 频繁修改:SortedDictionary
总结:避免陷阱的关键原则
- 理解底层实现:每种集合都有其内部机制,理解这些机制是避免陷阱的关键
- 预设容量:对于动态集合,尽可能预设容量以避免多次重新分配
- 正确实现哈希:自定义键类型必须正确实现GetHashCode和Equals
- 线程安全不是免费的:并发集合有性能开销,只在需要时使用
- 测量而非猜测:使用基准测试验证性能假设
- 内存意识:关注集合的内存开销,特别是大规模数据时
通过遵循这些原则和选择适当的集合类型,可以显著提高.NET应用程序的性能和可靠性。记住,没有”最好”的集合,只有”最适合”的集合。# 深入解析NET集合类型选择难题 从数组到并发包如何避免数据结构使用中的常见陷阱与性能瓶颈
在.NET开发中,集合类型的选择是每个开发者都会面临的日常决策。从简单的数组到复杂的并发集合,.NET Framework和.NET Core提供了丰富的集合类型,每种都有其特定的使用场景、性能特征和潜在陷阱。选择不当不仅会导致性能问题,还可能引发线程安全问题、内存浪费甚至程序崩溃。本文将深入剖析.NET中各种集合类型的特点,通过详细的性能对比和实际代码示例,帮助开发者理解何时使用数组、何时选择List,何时需要并发集合,以及如何避免常见的数据结构陷阱。
数组:基础但常被误解的性能利器
数组是.NET中最基础的集合类型,也是理解其他集合性能特征的基石。许多开发者认为数组已经过时,应该总是使用List
数组的核心优势与性能特征
数组在内存中是连续存储的,这带来了两个关键优势:CPU缓存友好性和确定的内存布局。当遍历数组时,CPU可以高效地预取数据到缓存中,而不需要像链表那样进行指针跳转。此外,数组的内存开销最小——只有元素本身和数组头的开销。
// 数组创建与基本操作示例
int[] numbers = new int[1000];
// 初始化数组元素
for (int i = 0; i < numbers.Length; i++)
{
numbers[i] = i * 2;
}
// 数组的直接访问 - 无边界检查开销(在某些优化场景下)
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
{
sum += numbers[i]; // JIT可能优化为指针算术
}
// 多维数组 vs 交错数组
int[,] matrix = new int[100, 100]; // 连续内存块
int[][] jagged = new int[100][]; // 100个独立数组
for (int i = 0; i < 100; i++)
{
jagged[i] = new int[100];
}
数组的性能陷阱与解决方案
数组最大的陷阱是固定大小特性。许多开发者在需要动态增长时错误地使用数组,导致频繁的重新分配和复制。然而,即使在这种情况下,理解数组的底层行为也能帮助优化List
// 错误示例:手动实现动态数组(性能差)
class BadDynamicArray
{
private int[] data = new int[4];
private int count = 0;
public void Add(int item)
{
if (count == data.Length)
{
// 每次都只增加1个容量 - 导致O(n²)复杂度
int[] newData = new int[data.Length + 1];
Array.Copy(data, newData, data.Length);
data = newData;
}
data[count++] = item;
}
}
// 正确示例:理解List<T>的容量策略
class OptimizedListUsage
{
public List<int> CreateLargeList(int expectedSize)
{
// 预先设置容量,避免多次重新分配
var list = new List<int>(expectedSize);
// List<T>内部容量增长策略:当前容量不足时,容量翻倍
// 这确保了添加n个元素的均摊时间复杂度为O(1)
for (int i = 0; i < expectedSize; i++)
{
list.Add(i);
}
return list;
}
public void ArrayCopyVsListAdd()
{
// 性能对比:Array.Copy vs List.Add
int[] source = new int[10000];
// 方法1:使用Array.Copy(最快,但需要管理索引)
int[] dest1 = new int[10000];
Array.Copy(source, dest1, source.Length);
// 方法2:使用List<T>(方便,但有额外开销)
var list = new List<int>(10000);
foreach (var item in source)
{
list.Add(item);
}
// 方法3:使用LINQ(最慢,但代码最简洁)
var list2 = source.ToList();
}
}
何时应该坚持使用数组
数组在以下场景中仍然是最佳选择:
- 固定大小的集合:当你确切知道元素数量时
- 高性能计算:需要极致性能的数值计算、图像处理等
- 互操作场景:与非托管代码交互时
- 多维数据:真正的多维数组(而非交错数组)在某些算法中更自然
// 数组的最佳实践示例
public class ArrayBestPractices
{
// 场景1:固定大小的查找表
private static readonly string[] DayNames =
{
"Monday", "Tuesday", "Wednesday", "Thursday",
"Friday", "Saturday", "Sunday"
};
// 场景2:缓冲区池(避免频繁分配)
private readonly Stack<byte[]> bufferPool = new Stack<byte[]>();
public byte[] RentBuffer(int size)
{
lock (bufferPool)
{
if (bufferPool.Count > 0)
{
var buffer = bufferPool.Peek();
if (buffer.Length >= size)
return bufferPool.Pop();
}
}
return new byte[size];
}
public void ReturnBuffer(byte[] buffer)
{
if (buffer == null || buffer.Length == 0) return;
lock (bufferPool)
{
// 限制池大小防止内存泄漏
if (bufferPool.Count < 100)
bufferPool.Push(buffer);
}
}
}
List:动态数组的权衡艺术
List
List的内部机制与性能特征
List
// List<T>内部机制演示
public class ListInternalMechanism
{
public void DemonstrateCapacityBehavior()
{
var list = new List<int>();
// 初始容量为0,第一次添加时容量变为4
list.Add(1); // Capacity = 4
list.Add(2); // Capacity = 4
list.Add(3); // Capacity = 4
list.Add(4); // Capacity = 4
list.Add(5); // Capacity = 8 (扩容)
list.Add(6); // Capacity = 8
list.Add(7); // Capacity = 8
list.Add(8); // Capacity = 8
list.Add(9); // Capacity = 16 (再次扩容)
// 手动设置容量可以避免多次扩容
var optimizedList = new List<int>(1000000);
for (int i = 0; i < 1000000; i++)
{
optimizedList.Add(i);
}
}
// 性能对比:不同初始化策略
public void InitializationPerformanceComparison()
{
const int count = 1000000;
// 方法1:无预设容量(最差)
var list1 = new List<int>();
for (int i = 0; i < count; i++) list1.Add(i);
// 方法2:预设准确容量(最佳)
var list2 = new List<int>(count);
for (int i = 0; i < count; i++) list2.Add(i);
// 方法3:预设过大容量(浪费内存)
var list3 = new List<int>(count * 2);
for (int i = 0; i < count; i++) list3.Add(i);
// 方法4:使用AddRange(内部优化)
var list4 = new List<int>();
list4.AddRange(Enumerable.Range(0, count));
}
}
List的常见陷阱与解决方案
// 陷阱1:在循环中频繁插入/删除元素
public class ListPerformanceTraps
{
// 错误:在循环中插入导致O(n²)复杂度
public void BadInsertionPattern()
{
var list = new List<int> { 1, 2, 3, 4, 5 };
// 每次插入都需要移动后续所有元素
for (int i = 0; i < 1000; i++)
{
list.Insert(0, i); // O(n)操作,1000次就是O(n²)
}
}
// 正确:先收集数据再批量插入
public void GoodInsertionPattern()
{
var list = new List<int> { 1, 2, 3, 4, 5 };
var temp = new List<int>(1000);
for (int i = 0; i < 1000; i++)
{
temp.Add(i);
}
// 批量插入,内部使用Array.Copy优化
list.InsertRange(0, temp);
}
// 陷阱2:使用Remove导致多次数组复制
public void BadRemovalPattern()
{
var list = new List<int>(Enumerable.Range(0, 1000));
// 每次Remove都会移动数组元素
while (list.Count > 0)
{
list.Remove(list[0]); // O(n)操作,n次就是O(n²)
}
}
// 正确:使用RemoveAt或Clear
public void GoodRemovalPattern()
{
var list = new List<int>(Enumerable.Range(0, 1000));
// 如果需要保留顺序
list.RemoveAt(0); // 单次操作
// 如果不需要保留顺序,使用Swap-Remove模式
if (list.Count > 0)
{
list[0] = list[list.Count - 1];
list.RemoveAt(list.Count - 1);
}
// 或者直接清空
list.Clear(); // O(1)操作
}
// 陷阱3:不必要的ToList()调用
public void UnnecessaryToList()
{
IEnumerable<int> source = Enumerable.Range(0, 1000);
// 不必要:如果只是遍历
var list1 = source.ToList(); // 额外分配
foreach (var item in list1) { /* ... */ }
// 更好:直接遍历
foreach (var item in source) { /* ... */ }
// 但如果需要多次访问,ToList()是有益的
var list2 = source.ToList();
var count = list2.Count;
var first = list2[0];
var last = list2[list2.Count - 1];
}
}
List的最佳实践模式
public class ListBestPractices
{
// 模式1:容量规划
public List<string> ProcessItems(IEnumerable<string> items)
{
// 如果知道大致数量,预设容量
if (items is ICollection<string> collection)
{
var result = new List<string>(collection.Count);
result.AddRange(items);
return result;
}
// 否则使用合理初始值
return new List<string>(32) { items };
}
// 模式2:使用List<T>作为临时缓冲区
public void ProcessInBatches()
{
var buffer = new List<int>(1000);
foreach (var item in GetSourceData())
{
buffer.Add(item);
if (buffer.Count == 1000)
{
ProcessBatch(buffer);
buffer.Clear(); // 复用缓冲区
}
}
// 处理剩余数据
if (buffer.Count > 0)
{
ProcessBatch(buffer);
}
}
// 模式3:避免装箱
public void AvoidBoxing()
{
// 错误:IList接口导致值类型装箱
IList<int> list1 = new List<int>();
list1.Add(1); // 装箱发生在这里
// 正确:使用具体类型
List<int> list2 = new List<int>();
list2.Add(1); // 无装箱
// 需要接口时,考虑使用泛型方法
void AddToGenericList<T>(List<T> list, T item)
{
list.Add(item); // 无装箱
}
}
private IEnumerable<int> GetSourceData()
{
return Enumerable.Range(0, 1000);
}
private void ProcessBatch(List<int> buffer)
{
// 处理逻辑
}
}
Dictionary:哈希表的正确打开方式
Dictionary
Dictionary的内部实现与性能关键点
Dictionary使用哈希表实现,理想情况下查找、插入、删除都是O(1)。但哈希冲突会退化为O(n)。理解其内部结构有助于避免性能陷阱。
// Dictionary内部机制演示
public class DictionaryInternals
{
// 自定义键类型必须正确实现GetHashCode和Equals
public class BadKey
{
public int Id { get; set; }
public string Name { get; set; }
// 错误:只基于Id,但Equals比较所有字段
public override int GetHashCode() => Id.GetHashCode();
public override bool Equals(object obj)
{
var other = obj as BadKey;
return other != null && Id == other.Id && Name == other.Name;
}
// 问题:两个对象可能有相同Id但不同Name,导致哈希冲突
// 但GetHashCode返回相同值,违反哈希契约
}
public class GoodKey
{
public int Id { get; set; }
public string Name { get; set; }
// 正确:基于所有用于相等比较的字段计算哈希
public override int GetHashCode()
{
// 使用HashCode.Combine(.NET Core 2.1+)
return HashCode.Combine(Id, Name);
// 或传统方式
// unchecked
// {
// int hash = 17;
// hash = hash * 31 + Id.GetHashCode();
// hash = hash * 31 + (Name?.GetHashCode() ?? 0);
// return hash;
// }
}
public override bool Equals(object obj)
{
var other = obj as GoodKey;
return other != null && Id == other.Id && Name == other.Name;
}
}
// 性能测试:哈希冲突的影响
public void HashCollisionImpact()
{
// 创建大量冲突的键(所有键哈希相同)
var badDict = new Dictionary<BadKey, int>();
var start = DateTime.Now;
for (int i = 0; i < 10000; i++)
{
// 所有BadKey实例的Id相同,导致哈希冲突
var key = new BadKey { Id = 1, Name = i.ToString() };
badDict[key] = i;
}
var elapsed = DateTime.Now - start;
Console.WriteLine($"BadKey Dictionary: {elapsed.TotalMilliseconds}ms");
// 正确哈希的键
var goodDict = new Dictionary<GoodKey, int>();
start = DateTime.Now;
for (int i = 0; i < 10000; i++)
{
var key = new GoodKey { Id = i, Name = i.ToString() };
goodDict[key] = i;
}
elapsed = DateTime.Now - start;
Console.WriteLine($"GoodKey Dictionary: {elapsed.TotalMilliseconds}ms");
}
}
Dictionary的内存陷阱与优化
public class DictionaryMemoryOptimization
{
// 陷阱1:键类型过大
public class LargeKey
{
public byte[] Data = new byte[1024]; // 1KB的键!
public int Id { get; set; }
public override int GetHashCode() => Id.GetHashCode();
public override bool Equals(object obj) => obj is LargeKey other && Id == other.Id;
}
public void LargeKeyProblem()
{
var dict = new Dictionary<LargeKey, string>();
// 每个条目:键1KB + 值引用 + 哈希桶开销 ≈ 1.2KB
// 1000个条目 ≈ 1.2MB,其中大部分是键的开销
// 解决方案:使用键的ID作为字典键,存储完整对象在值中
var optimized = new Dictionary<int, LargeKey>();
}
// 陷阱2:不必要的装箱
public void BoxingInDictionary()
{
// 错误:接口调用导致装箱
IDictionary<int, string> dict1 = new Dictionary<int, string>();
dict1.Add(1, "one"); // 值类型键会装箱
// 正确:使用具体类型
Dictionary<int, string> dict2 = new Dictionary<int, string>();
dict2.Add(1, "one"); // 无装箱
// 特别注意:TryGetValue的out参数
int key = 1;
string value;
if (dict2.TryGetValue(key, out value)) // 无装箱
{
// ...
}
}
// 陷阱3:容量设置不当
public void CapacityOptimization()
{
// 如果知道最终大小,预设容量
var items = GetItems(); // 假设返回1000个条目
// 错误:多次扩容
var dict1 = new Dictionary<int, string>();
foreach (var item in items) dict1.Add(item.Key, item.Value);
// 正确:预设容量
var dict2 = new Dictionary<int, string>(items.Count);
foreach (var item in items) dict2.Add(item.Key, item.Value);
// 更好:使用LINQ的ToDictionary(内部优化)
var dict3 = items.ToDictionary(x => x.Key, x => x.Value);
}
// 陷阱4:修改集合时的枚举问题
public void ModificationDuringEnumeration()
{
var dict = new Dictionary<int, string>
{
[1] = "one",
[2] = "two",
[3] = "three"
};
// 错误:运行时异常
try
{
foreach (var kvp in dict)
{
if (kvp.Key == 2)
{
dict.Remove(kvp.Key); // InvalidOperationException
}
}
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"错误:{ex.Message}");
}
// 正确:先收集要修改的键
var keysToRemove = new List<int>();
foreach (var kvp in dict)
{
if (kvp.Key == 2)
{
keysToRemove.Add(kvp.Key);
}
}
foreach (var key in keysToRemove)
{
dict.Remove(key);
}
// 或者使用LINQ
dict = dict.Where(kvp => kvp.Key != 2).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
}
private IEnumerable<KeyValuePair<int, string>> GetItems()
{
return Enumerable.Range(0, 1000).Select(i => new KeyValuePair<int, string>(i, $"value{i}"));
}
}
Dictionary的高级使用模式
public class DictionaryAdvancedPatterns
{
// 模式1:缓存模式(线程安全)
private readonly Dictionary<int, string> cache = new Dictionary<int, string>();
private readonly object cacheLock = new object();
public string GetCachedValue(int key)
{
// 双重检查锁定
if (cache.TryGetValue(key, out var value))
{
return value;
}
lock (cacheLock)
{
if (cache.TryGetValue(key, out value))
{
return value;
}
value = ExpensiveOperation(key);
cache[key] = value;
return value;
}
}
// 模式2:分组数据(使用Lookup)
public void GroupingPattern()
{
var items = new[]
{
new { Category = "A", Value = 1 },
new { Category = "B", Value = 2 },
new { Category = "A", Value = 3 }
};
// 使用Dictionary手动分组
var grouped = new Dictionary<string, List<int>>();
foreach (var item in items)
{
if (!grouped.ContainsKey(item.Category))
{
grouped[item.Category] = new List<int>();
}
grouped[item.Category].Add(item.Value);
}
// 更优雅:使用Lookup(不可变,适合多次查询)
var lookup = items.ToLookup(x => x.Category, x => x.Value);
foreach (var group in lookup["A"])
{
Console.WriteLine(group); // 输出1, 3
}
}
// 模式3:并发字典的替代方案
public void ConcurrentDictionaryAlternatives()
{
// 场景:读多写少,可以使用ReaderWriterLockSlim
var dict = new Dictionary<int, string>();
var rwLock = new System.Threading.ReaderWriterLockSlim();
// 读操作
rwLock.EnterReadLock();
try
{
if (dict.TryGetValue(1, out var value))
{
// ...
}
}
finally
{
rwLock.ExitReadLock();
}
// 写操作
rwLock.EnterWriteLock();
try
{
dict[1] = "new value";
}
finally
{
rwLock.ExitWriteLock();
}
}
private string ExpensiveOperation(int key)
{
System.Threading.Thread.Sleep(100);
return $"value for {key}";
}
}
HashSet:集合运算的利器
HashSet
HashSet的实现与性能特征
public class HashSetPerformance
{
// HashSet vs List.Contains 性能对比
public void PerformanceComparison()
{
var size = 100000;
var data = Enumerable.Range(0, size).ToArray();
var searchValues = Enumerable.Range(size / 2, 1000).ToArray();
// 使用List(O(n)查找)
var list = new List<int>(data);
var sw = Stopwatch.StartNew();
foreach (var val in searchValues)
{
list.Contains(val); // 每次O(n)
}
sw.Stop();
Console.WriteLine($"List.Contains: {sw.ElapsedMilliseconds}ms");
// 使用HashSet(O(1)查找)
var hashSet = new HashSet<int>(data);
sw.Restart();
foreach (var val in searchValues)
{
hashSet.Contains(val); // 每次O(1)
}
sw.Stop();
Console.WriteLine($"HashSet.Contains: {sw.ElapsedMilliseconds}ms");
}
// 集合运算示例
public void SetOperations()
{
var setA = new HashSet<int> { 1, 2, 3, 4, 5 };
var setB = new HashSet<int> { 4, 5, 6, 7, 8 };
// 并集
var union = new HashSet<int>(setA);
union.UnionWith(setB); // {1,2,3,4,5,6,7,8}
// 交集
var intersection = new HashSet<int>(setA);
intersection.IntersectWith(setB); // {4,5}
// 差集
var difference = new HashSet<int>(setA);
difference.ExceptWith(setB); // {1,2,3}
// 对称差集
var symmetric = new HashSet<int>(setA);
symmetric.SymmetricExceptWith(setB); // {1,2,3,6,7,8}
// 检查子集/超集
bool isSubset = setA.IsSubsetOf(setB); // false
bool isSuperset = setA.IsSupersetOf(setB); // false
bool overlaps = setA.Overlaps(setB); // true
}
}
HashSet的自定义比较器
public class HashSetCustomComparers
{
// 场景:基于对象属性去重
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public override bool Equals(object obj)
{
return obj is Person other && Name == other.Name && Age == other.Age;
}
public override int GetHashCode()
{
return HashCode.Combine(Name, Age);
}
}
public void CustomEqualityComparison()
{
// 使用自定义比较器基于Name去重(忽略Age)
var comparer = new NameOnlyComparer();
var set = new HashSet<Person>(comparer);
set.Add(new Person { Name = "Alice", Age = 25 });
set.Add(new Person { Name = "Alice", Age = 30 }); // 会被认为是重复的
set.Add(new Person { Name = "Bob", Age = 25 });
Console.WriteLine(set.Count); // 输出2
}
private class NameOnlyComparer : IEqualityComparer<Person>
{
public bool Equals(Person x, Person y)
{
return x?.Name == y?.Name;
}
public int GetHashCode(Person obj)
{
return obj?.Name?.GetHashCode() ?? 0;
}
}
// 使用IEqualityComparer<T>的通用模式
public void GenericComparerPattern<T>(IEnumerable<T> source, IEqualityComparer<T> comparer)
{
var uniqueItems = new HashSet<T>(source, comparer);
// ...
}
}
SortedSet与SortedList:有序集合的选择
当需要有序存储时,SortedSet和SortedList提供了不同的权衡。
SortedSet的特性与使用
public class SortedSetExamples
{
// SortedSet保持元素有序,适合范围查询
public void RangeOperations()
{
var set = new SortedSet<int> { 5, 2, 8, 1, 9, 3 };
// 自动排序:{1, 2, 3, 5, 8, 9}
// 范围查询
var range = set.GetViewBetween(3, 8); // {3, 5, 8}
// 最小/最大值
int min = set.Min; // 1
int max = set.Max; // 9
// 添加重复元素会被忽略
set.Add(5); // 返回false,集合不变
// 移除范围
set.RemoveWhere(x => x > 5); // 移除8,9
}
// 自定义排序
public void CustomOrdering()
{
// 按字符串长度排序
var set = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);
set.Add("apple");
set.Add("Banana");
set.Add("cherry");
// 输出:apple, Banana, cherry(按长度,忽略大小写)
}
}
SortedList vs SortedDictionary
public class SortedCollectionsComparison
{
// SortedList:基于数组,内存效率高,但插入/删除慢
// SortedDictionary:基于红黑树,插入/删除快,但内存开销大
public void PerformanceCharacteristics()
{
// SortedList适合:读多写少,需要索引访问
var list = new SortedList<int, string>();
for (int i = 0; i < 1000; i++)
{
list.Add(i, $"value{i}");
}
// 可以通过索引访问
var kvp = list[500]; // O(log n)查找,但索引访问是O(1)
// SortedDictionary适合:频繁插入删除
var dict = new SortedDictionary<int, string>();
for (int i = 0; i < 1000; i++)
{
dict.Add(i, $"value{i}");
}
// 无法通过索引访问,只能遍历
}
}
并发集合:线程安全的正确实现
并发集合(System.Collections.Concurrent)是.NET 4.0引入的专门用于多线程环境的集合。理解它们的内部机制对避免性能陷阱至关重要。
ConcurrentDictionary:最常用的并发集合
public class ConcurrentDictionaryDeepDive
{
// 基本使用
public void BasicUsage()
{
var dict = new ConcurrentDictionary<int, string>();
// 添加或更新
dict.AddOrUpdate(1, "one", (key, oldValue) => oldValue + " updated");
// 获取或添加
var value = dict.GetOrAdd(2, "two");
// 尝试获取
if (dict.TryGetValue(1, out var result))
{
Console.WriteLine(result);
}
// 更新(需要检查是否存在)
dict.AddOrUpdate(1, "new", (key, old) => "updated");
}
// 高级用法:GetOrAdd的延迟初始化
public void LazyInitializationPattern()
{
var cache = new ConcurrentDictionary<int, Lazy<string>>();
// 多个线程同时调用时,只有第一个会执行工厂函数
var value = cache.GetOrAdd(1, new Lazy<string>(() =>
{
Console.WriteLine("Factory executed");
System.Threading.Thread.Sleep(1000);
return "expensive value";
})).Value; // 只有在.Value访问时才真正执行
}
// 性能陷阱:过度使用AddOrUpdate
public void AddOrUpdatePerformance()
{
var dict = new ConcurrentDictionary<int, int>();
// 错误:高竞争下的性能问题
Parallel.For(0, 1000, i =>
{
// 每次调用都可能触发重试循环
dict.AddOrUpdate(i % 10, 1, (key, old) => old + 1);
});
// 更好:使用TryUpdate配合循环
var counters = new ConcurrentDictionary<int, int>();
Parallel.For(0, 1000, i =>
{
int key = i % 10;
while (true)
{
if (counters.TryGetValue(key, out var current))
{
if (counters.TryUpdate(key, current + 1, current))
break;
}
else
{
if (counters.TryAdd(key, 1))
break;
}
}
});
}
}
其他并发集合的适用场景
public class OtherConcurrentCollections
{
// ConcurrentQueue:FIFO,无锁队列
public void QueueExample()
{
var queue = new ConcurrentQueue<int>();
// 生产者
Parallel.For(0, 100, i => queue.Enqueue(i));
// 消费者
while (queue.TryDequeue(out var item))
{
Process(item);
}
}
// ConcurrentStack:LIFO,无锁栈
public void StackExample()
{
var stack = new ConcurrentStack<int>();
Parallel.For(0, 100, i => stack.Push(i));
while (stack.TryPop(out var item))
{
Process(item);
}
}
// ConcurrentBag:无序集合,适合线程本地存储
public void BagExample()
{
var bag = new ConcurrentBag<int>();
Parallel.For(0, 100, i =>
{
bag.Add(i);
// 每个线程有自己的存储,减少竞争
});
while (bag.TryTake(out var item))
{
Process(item);
}
}
// BlockingCollection:生产者-消费者模式
public void BlockingCollectionExample()
{
var collection = new BlockingCollection<int>(boundedCapacity: 10);
// 生产者任务
var producer = Task.Run(() =>
{
for (int i = 0; i < 100; i++)
{
collection.Add(i); // 如果满则阻塞
Thread.Sleep(10);
}
collection.CompleteAdding(); // 信号:生产完成
});
// 消费者任务
var consumer = Task.Run(() =>
{
foreach (var item in collection.GetConsumingEnumerable())
{
Process(item); // 如果空则阻塞,直到CompleteAdding
}
});
Task.WaitAll(producer, consumer);
}
private void Process(int item)
{
// 模拟处理
Thread.Sleep(1);
}
}
性能对比:实际基准测试
让我们通过实际的基准测试代码来比较不同集合的性能。
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Linq;
public class CollectionBenchmark
{
public static void RunAllBenchmarks()
{
Console.WriteLine("=== 集合性能基准测试 ===\n");
BenchmarkAddOperations();
BenchmarkLookupOperations();
BenchmarkMemoryUsage();
BenchmarkThreadSafety();
}
private static void BenchmarkAddOperations()
{
Console.WriteLine("1. 添加操作性能 (100,000次)");
const int count = 100000;
// 数组(预分配)
var sw = Stopwatch.StartNew();
var array = new int[count];
for (int i = 0; i < count; i++) array[i] = i;
sw.Stop();
Console.WriteLine($" 数组(预分配): {sw.ElapsedMilliseconds}ms");
// List<T>(预分配容量)
sw.Restart();
var list = new List<int>(count);
for (int i = 0; i < count; i++) list.Add(i);
sw.Stop();
Console.WriteLine($" List<T>(预分配): {sw.ElapsedMilliseconds}ms");
// List<T>(无预分配)
sw.Restart();
var list2 = new List<int>();
for (int i = 0; i < count; i++) list2.Add(i);
sw.Stop();
Console.WriteLine($" List<T>(无预分配): {sw.ElapsedMilliseconds}ms");
// Dictionary
sw.Restart();
var dict = new Dictionary<int, int>(count);
for (int i = 0; i < count; i++) dict[i] = i;
sw.Stop();
Console.WriteLine($" Dictionary: {sw.ElapsedMilliseconds}ms");
// HashSet
sw.Restart();
var hashSet = new HashSet<int>();
for (int i = 0; i < count; i++) hashSet.Add(i);
sw.Stop();
Console.WriteLine($" HashSet: {sw.ElapsedMilliseconds}ms");
Console.WriteLine();
}
private static void BenchmarkLookupOperations()
{
Console.WriteLine("2. 查找操作性能 (1,000,000次)");
const int count = 100000;
const int lookups = 1000000;
// 准备数据
var array = Enumerable.Range(0, count).ToArray();
var list = new List<int>(array);
var dict = array.ToDictionary(x => x);
var hashSet = new HashSet<int>(array);
var random = new Random(42);
var searchKeys = Enumerable.Range(0, lookups).Select(_ => random.Next(count)).ToArray();
// 数组(线性搜索)
var sw = Stopwatch.StartNew();
foreach (var key in searchKeys)
{
Array.IndexOf(array, key);
}
sw.Stop();
Console.WriteLine($" 数组线性搜索: {sw.ElapsedMilliseconds}ms");
// List(线性搜索)
sw.Restart();
foreach (var key in searchKeys)
{
list.IndexOf(key);
}
sw.Stop();
Console.WriteLine($" List线性搜索: {sw.ElapsedMilliseconds}ms");
// List(二分搜索 - 需要排序)
var sortedList = new List<int>(array);
sortedList.Sort();
sw.Restart();
foreach (var key in searchKeys)
{
sortedList.BinarySearch(key);
}
sw.Stop();
Console.WriteLine($" List二分搜索: {sw.ElapsedMilliseconds}ms");
// Dictionary
sw.Restart();
foreach (var key in searchKeys)
{
dict.ContainsKey(key);
}
sw.Stop();
Console.WriteLine($" Dictionary查找: {sw.ElapsedMilliseconds}ms");
// HashSet
sw.Restart();
foreach (var key in searchKeys)
{
hashSet.Contains(key);
}
sw.Stop();
Console.WriteLine($" HashSet查找: {sw.ElapsedMilliseconds}ms");
Console.WriteLine();
}
private static void BenchmarkMemoryUsage()
{
Console.WriteLine("3. 内存使用 (10,000个元素)");
const int count = 10000;
// 测量内存前强制GC
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
var before = GC.GetTotalMemory(true);
var array = new int[count];
for (int i = 0; i < count; i++) array[i] = i;
var afterArray = GC.GetTotalMemory(true);
Console.WriteLine($" 数组: {(afterArray - before):N0} bytes");
var list = new List<int>(count);
for (int i = 0; i < count; i++) list.Add(i);
var afterList = GC.GetTotalMemory(true);
Console.WriteLine($" List: {(afterList - afterArray):N0} bytes");
var dict = new Dictionary<int, int>(count);
for (int i = 0; i < count; i++) dict[i] = i;
var afterDict = GC.GetTotalMemory(true);
Console.WriteLine($" Dictionary: {(afterDict - afterList):N0} bytes");
var hashSet = new HashSet<int>();
for (int i = 0; i < count; i++) hashSet.Add(i);
var afterHashSet = GC.GetTotalMemory(true);
Console.WriteLine($" HashSet: {(afterHashSet - afterDict):N0} bytes");
Console.WriteLine();
}
private static void BenchmarkThreadSafety()
{
Console.WriteLine("4. 并发添加性能 (10线程 × 10,000次)");
const int perThread = 10000;
const int threadCount = 10;
// 非线程安全List(加锁)
var list = new List<int>();
var listLock = new object();
var sw = Stopwatch.StartNew();
Parallel.For(0, threadCount, _ =>
{
for (int i = 0; i < perThread; i++)
{
lock (listLock)
{
list.Add(i);
}
}
});
sw.Stop();
Console.WriteLine($" List + lock: {sw.ElapsedMilliseconds}ms");
// ConcurrentBag
var bag = new ConcurrentBag<int>();
sw.Restart();
Parallel.For(0, threadCount, _ =>
{
for (int i = 0; i < perThread; i++)
{
bag.Add(i);
}
});
sw.Stop();
Console.WriteLine($" ConcurrentBag: {sw.ElapsedMilliseconds}ms");
// ConcurrentDictionary
var dict = new ConcurrentDictionary<int, int>();
sw.Restart();
Parallel.For(0, threadCount, threadId =>
{
for (int i = 0; i < perThread; i++)
{
dict[threadId * perThread + i] = i;
}
});
sw.Stop();
Console.WriteLine($" ConcurrentDictionary: {sw.ElapsedMilliseconds}ms");
Console.WriteLine();
}
}
// 运行基准测试
// CollectionBenchmark.RunAllBenchmarks();
实际场景下的选择指南
基于上述分析,以下是不同场景下的集合选择建议:
场景1:固定大小的只读数据
推荐:数组
// 配置、映射表等
private static readonly string[] ConfigValues = { "val1", "val2", "val3" };
场景2:动态增长的集合
推荐:List
var list = new List<int>(expectedCount);
// 填充数据...
场景3:快速查找
推荐:Dictionary
var lookup = new Dictionary<string, User>();
// 注意键的选择和哈希实现
场景4:去重和集合运算
推荐:HashSet
var unique = new HashSet<int>(source);
场景5:多线程环境
推荐:并发集合
// 读多写少:ConcurrentDictionary
// 生产者-消费者:BlockingCollection
// 无序收集:ConcurrentBag
场景6:需要有序
推荐:SortedList或SortedDictionary
// 需要索引访问:SortedList
// 频繁修改:SortedDictionary
总结:避免陷阱的关键原则
- 理解底层实现:每种集合都有其内部机制,理解这些机制是避免陷阱的关键
- 预设容量:对于动态集合,尽可能预设容量以避免多次重新分配
- 正确实现哈希:自定义键类型必须正确实现GetHashCode和Equals
- 线程安全不是免费的:并发集合有性能开销,只在需要时使用
- 测量而非猜测:使用基准测试验证性能假设
- 内存意识:关注集合的内存开销,特别是大规模数据时
通过遵循这些原则和选择适当的集合类型,可以显著提高.NET应用程序的性能和可靠性。记住,没有”最好”的集合,只有”最适合”的集合。
