C#常用类库-详解Ecng.Collections
在C#开发中,System.Collections.Generic 提供了基础的集合类型(List<T>、Dictionary<TKey, TValue>),但在高性能场景、并发操作、特殊数据结构(如双向链表、循环缓冲区)及内存优化方面,原生集合往往存在不足。
Ecng.Collections 作为一款专注于 高性能、线程安全、特殊数据结构 的扩展类库,由Ecng团队开发,它完美补充了 .NET 原生集合的短板,提供了如 ObservableList<T>、ConcurrentHashSet<T>、CyclicBuffer<T> 等高频实用组件,是游戏服务器、高频交易系统、并发中间件及高性能后端开发的"隐形神器"。
本文聚焦"简练、详细、有深度",从核心价值、环境搭建、核心数据结构、进阶技巧到实战落地,全方位解析 Ecng.Collections,帮你将集合操作效率提升一个档次,解决原生集合无法覆盖的高频场景痛点。
一、核心定位:Ecng.Collections 解决什么问题?
Ecng.Collections 的核心不是"替代"原生集合,而是 "扩展" 与 "优化"。它针对原生集合的痛点,提供了三大核心解决方案,精准匹配高性能、高并发场景需求:
-
缺失的关键结构 :原生库缺少
HashSet的线程安全版本、强类型的双向链表、循环缓冲区等,Ecng.Collections 直接补齐,无需手动封装。 -
高性能优化:针对频繁增删、查找的场景,采用内存优化布局(减少装箱、优化指针操作),核心操作速度优于原生集合,尤其适合高频读写场景。
-
线程安全与可观测性 :原生
ConcurrentDictionary功能有限,Ecng.Collections 提供更精细的并发集合;同时内置INotifyCollectionChanged支持,完美适配 MVVM 数据绑定,无需额外封装。
核心优势对比(原生 vs Ecng)
| 特性 | System.Collections.Generic | Ecng.Collections |
|---|---|---|
| 线程安全集合 | 有限(仅 ConcurrentDictionary/ConcurrentQueue) | 丰富(ConcurrentHashSet、ConcurrentObservableStack 等) |
| 特殊数据结构 | 较少(仅基础链表、队列) | 齐全(双向链表、循环缓冲区、树节点、优先队列) |
| 数据绑定支持 | 需手动实现(ObservableCollection 性能一般) | 原生支持(ObservableList、ObservableDictionary) |
| 性能 | 通用场景良好,高频操作有瓶颈 | 极高(针对高频增删查优化,内存开销低) |
| API 友好度 | 基础够用,扩展能力弱 | 更简洁(提供通用扩展方法,简化代码) |
二、环境搭建:快速引入与初始化
Ecng.Collections 是纯 NuGet 包,无任何第三方依赖,安装简单,开箱即用,适配所有 .NET Standard 2.0+ 项目(.NET Core 2.0+、.NET 5+ 均支持)。
1. 安装 NuGet 包
打开 NuGet 包管理器,搜索"Ecng.Collections"安装,或执行以下 .NET CLI 命令:
bash
// 核心包(必装,包含所有核心集合结构与扩展方法)
dotnet add package Ecng.Collections
// 可选扩展包(包含与其他 Ecng 生态的集成,如日志、序列化)
// dotnet add package Ecng.Collections.Extensions
2. 核心命名空间
使用前只需引入两个核心命名空间,即可调用所有集合与扩展方法:
csharp
using Ecng.Collections; // 核心集合类(ConcurrentHashSet、ObservableList 等)
using Ecng.Common; // 通用扩展方法(集合转换、安全操作等)
三、核心数据结构详解(必学篇)
Ecng.Collections 提供了数十种集合,但企业级开发中最常用、最核心的是以下 5 类,掌握它们能覆盖 90% 的高频场景,重点关注"适用场景+核心API+性能优势"。
1. 线程安全哈希集 (ConcurrentHashSet) ------ 并发去重首选
原生痛点 :.NET 原生没有线程安全的 HashSet<T>,若要实现并发去重,需手动加锁(lock),不仅代码繁琐,还会影响性能。
核心价值:高性能的并发去重集合,所有操作(Add、Remove、Contains)均为原子性,无需外部锁,支持批量操作,适合多线程去重场景(如用户ID去重、请求去重)。
csharp
// 1. 创建并发HashSet(支持自定义比较器,默认使用EqualityComparer<T>.Default)
var set = new ConcurrentHashSet<int>();
// 2. 原子添加(返回是否成功添加,重复添加返回false)
bool added = set.Add(1); // true
bool alreadyExists = set.Add(1); // false
// 3. 原子移除(返回是否成功移除)
bool removed = set.Remove(1); // true
// 4. 快速查找(O(1)时间复杂度,线程安全)
bool exists = set.Contains(1); // false
// 5. 批量操作(原子性,避免多次锁竞争)
set.AddRange(new[] { 2, 3, 4 }); // 批量添加
set.RemoveRange(new[] { 2, 3 }); // 批量移除
// 6. 线程安全枚举(避免枚举时修改集合导致的InvalidOperationException)
foreach (var item in set.SafeEnumerable())
{
Console.WriteLine(item); // 输出 4
}
// 7. 手动锁控制(适合复杂批量操作)
using (set.Lock()) // 获取读写锁,using结束自动释放
{
foreach (var item in set)
{
if (item % 2 == 0) set.Remove(item);
}
}
2. 可观测列表 (ObservableList) ------ MVVM 绑定神器
原生痛点 :ObservableCollection<T> 在频繁修改(如批量添加、删除)时,会多次触发 CollectionChanged 事件,导致 UI 频繁刷新,性能不佳;且缺少批量操作 API。
核心价值 :高性能可观测集合,实现 INotifyCollectionChanged 和 INotifyPropertyChanged,支持批量更新(仅触发一次事件),精准控制事件触发,完美适配 WPF、Blazor 等 MVVM 场景。
csharp
// 1. 创建可观测列表
var list = new ObservableList<string>();
// 2. 订阅集合变化事件(UI绑定自动响应)
list.CollectionChanged += (s, e) =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Console.WriteLine($"添加元素:{e.NewItems[0]}");
break;
case NotifyCollectionChangedAction.Remove:
Console.WriteLine($"移除元素:{e.OldItems[0]}");
break;
case NotifyCollectionChangedAction.Reset:
Console.WriteLine("集合已重置");
break;
}
};
// 3. 基础操作(与List<T> API 完全兼容,学习成本低)
list.Add("Apple");
list.Insert(0, "Banana"); // 插入到头部
list.RemoveAt(0); // 移除头部元素
// 4. 核心优势:批量操作(仅触发一次CollectionChanged事件)
// 避免多次修改导致UI频繁刷新,大幅提升性能
list.AddRange(new[] { "Orange", "Grape", "Mango" });
list.RemoveRange(new[] { "Apple" });
// 5. 精准控制事件(暂停/恢复通知,适合大量修改场景)
list.SuspendNotifications(); // 暂停事件通知
list.Clear(); // 不触发事件
list.AddRange(new[] { "Pear", "Peach" }); // 不触发事件
list.ResumeNotifications(); // 恢复通知,触发一次Reset事件
3. 循环缓冲区 (CyclicBuffer) ------ 固定大小缓存首选
原生痛点:实现"固定大小、先进先出(FIFO)、满了自动覆盖最旧元素"的缓存逻辑,需手动封装数组+索引,代码繁琐且易出错。
核心价值:经典 FIFO 循环结构,内存占用固定,无需手动管理扩容/收缩,适合日志记录、消息缓存、滑动窗口计算等场景(如保留最近1000条日志)。
csharp
// 1. 创建容量为3的循环缓冲区(固定容量,不可修改)
var buffer = new CyclicBuffer<int>(3);
// 2. 添加元素(满了自动覆盖最旧元素)
buffer.Push(1); // 缓冲区:[1]
buffer.Push(2); // 缓冲区:[1, 2]
buffer.Push(3); // 缓冲区:[1, 2, 3](已满)
buffer.Push(4); // 覆盖最旧的1,缓冲区:[2, 3, 4]
// 3. 弹出元素(获取并移除最旧元素,FIFO)
int oldest = buffer.Pop(); // 返回2,缓冲区变为 [3, 4]
// 4. 查看元素(不移除)
int peekOldest = buffer.Peek(); // 查看最旧元素:3
int peekLatest = buffer.PeekLast();// 查看最新元素:4
// 5. 安全操作与状态判断
bool hasValue = buffer.TryPop(out int value); // 安全弹出,返回是否成功
int count = buffer.Count; // 当前元素数量:2
bool isFull = buffer.IsFull; // 是否已满:false(容量3,当前2个)
bool isEmpty = buffer.IsEmpty; // 是否为空:false
// 6. 批量填充与清空
buffer.Fill(new[] { 5, 6, 7 }); // 填充后:[5, 6, 7](已满)
buffer.Clear(); // 清空缓冲区
4. 双向链表 (LinkedList2) ------ 中间增删高效
原生痛点 :原生 LinkedList<T> 节点结构封闭,操作繁琐(如中间插入需先查找节点),且缺少反向遍历、批量操作等高频 API。
核心价值 :更易用、更高效的双向链表实现,提供简洁的 AddAfter、AddBefore、Find 等 API,支持正向/反向遍历,适合频繁在列表中间增删的场景(如任务队列、消息链表)。
csharp
// 1. 创建双向链表
var ll = new LinkedList2<int>();
// 2. 添加元素(返回节点引用,便于后续中间插入)
var node1 = ll.AddFirst(1); // 添加到头部,返回节点
var node3 = ll.AddLast(3); // 添加到尾部,返回节点
// 3. 中间插入(高效,无需遍历整个链表)
ll.AddAfter(node1, 2); // 在1后面插入2,链表:1 <-> 2 <-> 3
// 4. 遍历(正向/反向,API简洁)
foreach (var item in ll) Console.Write($"{item} "); // 1 2 3
foreach (var item in ll.Reverse()) Console.Write($"{item} "); // 3 2 1
// 5. 查找与移除(高效)
var node2 = ll.Find(2); // 查找值为2的节点(O(n),但节点操作O(1))
ll.Remove(node2); // 移除指定节点,链表变为:1 <-> 3
// 6. 批量操作
ll.AddRange(new[] { 4, 5 }); // 批量添加到尾部
ll.RemoveRange(new[] { 1, 3 }); // 批量移除
5. 并发可观测字典 (ConcurrentObservableDictionary<TKey, TValue>) ------ 并发+绑定双需求
原生痛点 :原生 ConcurrentDictionary 不支持 INotifyCollectionChanged,无法直接用于 MVVM 绑定;而 ObservableDictionary 是非线程安全的,多线程环境下易报错。
核心价值 :线程安全与可观测性的完美结合,既支持原子性的字典操作,又能触发 CollectionChanged 事件,适用于多线程环境下的 UI 数据绑定(如实时更新的配置字典)。
csharp
// 1. 创建并发可观测字典
var dict = new ConcurrentObservableDictionary<int, string>();
// 2. 订阅集合变化事件(UI自动响应)
dict.CollectionChanged += (s, e) =>
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
var kvp = (KeyValuePair<int, string>)e.NewItems[0];
Console.WriteLine($"添加键值对:{kvp.Key}={kvp.Value}");
}
};
// 3. 线程安全操作(所有操作均为原子性,同时触发事件)
dict.TryAdd(1, "One"); // 安全添加,触发Add事件
dict[2] = "Two"; // 索引器操作,线程安全
dict.TryUpdate(2, "Two_New", "Two"); // 原子更新(匹配旧值才更新)
dict.TryRemove(1, out _); // 安全移除
// 4. 多线程并发测试(模拟100个线程同时添加)
Parallel.For(0, 100, i =>
{
dict.TryAdd(i, $"Value_{i}");
});
// 5. 绑定UI(WPF示例)
// <DataGrid ItemsSource="{Binding Dict}" />
// ViewModel 中直接暴露 Dict 属性即可
四、进阶技巧:实战必备(深度重点)
掌握基础数据结构后,以下进阶技巧能进一步提升开发效率,规避性能问题,适配更复杂的企业级场景。
1. 集合转换与互操作(原生 ↔ Ecng)
Ecng.Collections 提供了丰富的扩展方法,可快速在原生集合与 Ecng 集合之间转换,无需手动遍历赋值,简化代码。
csharp
// 1. 原生List<T> → Ecng ObservableList<T>
var nativeList = new List<int> { 1, 2, 3 };
var obsList = nativeList.ToObservableList(); // 扩展方法
// 2. 原生Dictionary → Ecng ConcurrentObservableDictionary
var nativeDict = new Dictionary<int, string> { {1, "A"}, {2, "B"} };
var concObsDict = nativeDict.ToConcurrentObservableDictionary();
// 3. IEnumerable<T> → Ecng CyclicBuffer<T>
var array = new[] { 1, 2, 3 };
var buffer = array.ToCyclicBuffer(3); // 容量为3,填充数组元素
// 4. Ecng集合 → 原生集合
var ecngList = new ObservableList<int> { 1, 2, 3 };
var nativeList2 = ecngList.ToList(); // 转为原生List
var nativeDict2 = concObsDict.ToDictionary(); // 转为原生Dictionary
2. 高性能枚举与安全操作(并发场景避坑)
并发场景下,直接枚举集合可能会因"枚举时修改集合"导致 InvalidOperationException,Ecng 提供了两种安全枚举方式,兼顾性能与安全性。
csharp
var set = new ConcurrentHashSet<int> { 1, 2, 3, 4, 5 };
// 方式1:使用SafeEnumerable()(推荐,获取集合快照,无锁)
// 适合只读枚举,不修改集合的场景,性能最优
foreach (var item in set.SafeEnumerable())
{
Console.WriteLine(item);
}
// 方式2:使用Lock()手动控制(适合枚举时修改集合的场景)
// Lock()返回IDisposable,using结束自动释放锁,避免死锁
using (set.Lock())
{
foreach (var item in set)
{
if (item % 2 == 0)
{
set.Remove(item); // 枚举时修改集合,安全无异常
}
}
}
// 注意:避免直接枚举并发集合(错误示例)
// foreach (var item in set) { set.Remove(item); } // 可能抛出异常
3. 自定义比较器(适配业务场景)
Ecng 集合默认使用 EqualityComparer<T>.Default 进行元素比较,但实际业务中,常需按对象的某个属性(如ID)进行比较,此时可自定义比较器,灵活适配业务需求。
csharp
// 1. 定义业务实体
public class Student
{
public int Id { get; set; } // 唯一标识
public string Name { get; set; } // 姓名(可重复)
}
// 2. 自定义比较器:按Id比较(忽略Name差异)
public class StudentIdEqualityComparer : IEqualityComparer<Student>
{
public bool Equals(Student x, Student y)
{
if (x == null && y == null) return true;
if (x == null || y == null) return false;
return x.Id == y.Id; // 核心:按Id判断是否相等
}
public int GetHashCode(Student obj)
{
return obj.Id.GetHashCode(); // 哈希值基于Id计算
}
}
// 3. 使用自定义比较器创建Ecng集合
var comparer = new StudentIdEqualityComparer();
var studentSet = new ConcurrentHashSet<Student>(comparer);
// 4. 测试:Id相同,Name不同,视为重复元素
studentSet.Add(new Student { Id = 1, Name = "Alice" });
studentSet.Add(new Student { Id = 1, Name = "Bob" }); // 重复,添加失败
Console.WriteLine(studentSet.Count); // 输出:1(仅保留一个Id=1的元素)
4. 与 MVVM 结合(WPF/Blazor 实战)
ObservableList<T> 和 ConcurrentObservableDictionary<TKey, TValue> 是 MVVM 模式的最佳伙伴,无需额外继承 ObservableObject 基类,即可实现集合的自动通知,简化 ViewModel 代码。
csharp
// ViewModel 示例(WPF)
public class MainViewModel
{
// 可观测集合,自动实现INotifyCollectionChanged
public ObservableList<Student> Students { get; } = new ObservableList<Student>();
// 并发可观测字典,适配多线程更新UI
public ConcurrentObservableDictionary<int, string> ClassNames { get; }
= new ConcurrentObservableDictionary<int, string>();
public MainViewModel()
{
// 初始化数据
Students.AddRange(new[]
{
new Student { Id = 1, Name = "Alice" },
new Student { Id = 2, Name = "Bob" }
});
ClassNames.TryAdd(1, "计算机1班");
ClassNames.TryAdd(2, "计算机2班");
}
// 模拟多线程更新集合(UI自动响应)
public void AddStudent(Student student)
{
// 多线程环境下安全添加,同时触发UI更新
Students.Add(student);
}
}
// XAML 绑定示例(WPF)
// <ListBox ItemsSource="{Binding Students}" DisplayMemberPath="Name" />
// <ComboBox ItemsSource="{Binding ClassNames}" DisplayMemberPath="Value" />
五、实战场景:完整案例(高性能日志系统)
结合 Ecng.Collections 的核心数据结构,实现一个 线程安全、高性能、固定容量、UI 实时更新 的日志收集系统,贴合游戏服务器、后端服务的实际需求。
场景需求
-
多线程并发写入日志(如业务线程、异步线程同时写日志)。
-
日志总量固定,保留最新 1000 条,满了自动覆盖最旧日志。
-
快速查询某类日志(如错误日志)是否存在,用于告警判断。
-
日志变更时,自动通知 UI 实时显示,无需手动刷新。
实现代码
csharp
using Ecng.Collections;
using Ecng.Common;
using System.Collections.Specialized;
// 1. 日志模型
public enum LogType { Info, Warning, Error }
public class LogEntry
{
public LogType Type { get; set; }
public string Message { get; set; }
public DateTime Time { get; set; } = DateTime.Now;
}
// 2. 高性能日志服务(核心实现)
public class LogService
{
// 循环缓冲区:固定容量1000,存储最新日志,自动覆盖旧日志
private readonly CyclicBuffer<LogEntry> _logBuffer;
// 并发HashSet:快速判断某类日志是否存在(用于告警)
private readonly ConcurrentHashSet<LogType> _activeLogTypes;
// 可观测列表:用于UI绑定,实时显示日志
public ObservableList<LogEntry> Logs { get; }
// 线程安全锁(用于同步缓冲区与可观测列表)
private readonly object _lockObj = new object();
public LogService(int maxLogCount = 1000)
{
_logBuffer = new CyclicBuffer<LogEntry>(maxLogCount);
_activeLogTypes = new ConcurrentHashSet<LogType>();
Logs = new ObservableList<LogEntry>();
}
// 线程安全的写日志方法(核心API)
public void WriteLog(LogType type, string message)
{
var entry = new LogEntry
{
Type = type,
Message = message
};
lock (_lockObj)
{
// 1. 写入循环缓冲区(自动覆盖旧日志)
_logBuffer.Push(entry);
// 2. 更新活跃日志类型(用于告警判断)
_activeLogTypes.Add(type);
// 3. 更新UI绑定的可观测列表(保持与缓冲区一致)
Logs.Add(entry);
// 限制UI列表大小,避免内存溢出
if (Logs.Count > _logBuffer.Capacity)
{
Logs.RemoveAt(0);
}
}
}
// 快速查询某类日志是否存在(O(1)时间复杂度)
public bool HasLogType(LogType type)
{
return _activeLogTypes.Contains(type);
}
// 获取所有日志(线程安全快照)
public LogEntry[] GetAllLogs()
{
using (_logBuffer.Lock())
{
return _logBuffer.ToArray();
}
}
// 清空所有日志
public void ClearLogs()
{
lock (_lockObj)
{
_logBuffer.Clear();
_activeLogTypes.Clear();
Logs.Clear();
}
}
}
// 3. 调用示例(多线程测试)
public class Program
{
public static void Main()
{
var logService = new LogService(1000);
// 模拟多线程并发写日志(10个线程,每个线程写100条)
Parallel.For(0, 10, threadId =>
{
for (int i = 0; i < 100; i++)
{
var logType = (LogType)(i % 3); // 循环生成3种日志类型
logService.WriteLog(logType, $"线程{threadId}:日志{i},类型{logType}");
}
});
// 测试查询功能
bool hasError = logService.HasLogType(LogType.Error);
Console.WriteLine($"是否存在错误日志:{hasError}");
// 测试获取所有日志
var allLogs = logService.GetAllLogs();
Console.WriteLine($"当前日志总数:{allLogs.Length}"); // 输出1000(固定容量)
// UI 绑定(WPF/Blazor)
// var vm = new MainViewModel { LogService = logService };
// 界面绑定 LogService.Logs 即可实时显示日志
}
}
六、避坑指南与最佳实践(深度重点)
Ecng.Collections 用法简洁,但在高性能、高并发场景下,若使用不当,会导致性能损耗或异常,以下是企业级开发的避坑要点和最佳实践。
1. 集合选型避坑(关键!)
不同集合适配不同场景,选错集合会导致性能瓶颈,精准选型是关键:
-
多线程去重、快速查找 → 选
ConcurrentHashSet<T>(O(1) 查找,原子操作)。 -
MVVM 集合绑定、UI 实时更新 → 选
ObservableList<T>(批量操作优化,事件可控)。 -
固定大小缓存、日志/消息存储 → 选
CyclicBuffer<T>(内存固定,自动覆盖)。 -
频繁在列表中间增删 → 选
LinkedList2<T>(O(1) 节点操作,比 List 高效)。 -
多线程+UI绑定的字典场景 → 选
ConcurrentObservableDictionary(并发安全+可观测)。 -
通用只读场景 → 优先用原生集合(
List、HashSet),避免过度封装。
2. 性能注意事项
-
避免不必要的集合转换:频繁在原生集合与 Ecng 集合之间转换,会增加内存开销和性能损耗,尽量在初始化时确定集合类型。
-
并发集合的锁控制:
ConcurrentHashSet、ConcurrentObservableDictionary已实现原子操作,无需额外加锁;但复杂批量操作(如枚举+修改),需使用Lock()手动控制,避免竞争。 -
ObservableList事件优化:频繁批量修改时,先调用SuspendNotifications()暂停事件,修改完成后调用ResumeNotifications(),减少 UI 刷新次数。 -
循环缓冲区容量设置:根据业务需求合理设置容量,容量过大浪费内存,过小会频繁覆盖,建议结合日志/缓存的实际保留需求设置(如1000条、10000条)。
3. 常见异常规避
-
枚举异常:并发场景下,避免直接枚举 Ecng 并发集合,优先使用
SafeEnumerable()或Lock()安全枚举。 -
比较器异常:自定义比较器时,需确保
Equals和GetHashCode逻辑一致(如按 Id 比较,哈希值也需基于 Id),否则会导致集合查找、去重异常。 -
内存溢出:
ObservableList用于 UI 绑定时,需限制列表大小,避免无限制添加元素(如日志系统中,与循环缓冲区容量保持一致)。
4. 通用最佳实践
-
封装复用:将常用集合操作(如集合转换、安全枚举、批量更新)封装为工具类,避免重复代码,统一维护。
-
版本兼容:Ecng.Collections 版本更新较快,需确保项目中使用的版本与 .NET 版本兼容(推荐使用最新稳定版,适配 .NET 6+)。
-
日志与监控:在高频操作的集合中,添加日志记录(如循环缓冲区覆盖、并发集合操作失败),便于排查问题;高并发场景下,监控集合操作耗时,优化性能瓶颈。
-
优先使用扩展方法:Ecng 提供的扩展方法(如
ToObservableList、SafeEnumerable)经过性能优化,比手动实现更高效、更安全。
七、总结
Ecng.Collections 的核心价值是 "填补原生集合短板,优化高性能、高并发场景体验"。它不是对原生集合的替代,而是精准补充,让 C# 开发者在面对特殊数据结构、并发操作、UI 绑定等场景时,无需手动封装,直接使用成熟、高效的组件。
掌握 Ecng.Collections 的关键:精准选型(根据场景选择合适的集合)、熟练使用核心 API、规避并发与性能坑,结合实战场景灵活运用。对于游戏服务器、高频交易系统、后端并发服务等高性能需求的项目,Ecng.Collections 能大幅提升开发效率,降低维护成本,优化系统性能。
扩展建议:深入学习 Ecng 生态的其他类库(如 Ecng.Logging、Ecng.Serialization),实现集合与日志、序列化的无缝集成;同时关注 Ecng 官方文档,了解最新的集合扩展与性能优化特性。