C# 泛型与集合
泛型类型系统、约束、协变逆变、List/Dictionary/HashSet/Queue/Stack/PriorityQueue、不可变集合、Frozen 集合、迭代器(yield)、LINQ to Objects、性能模型与生产实践
前置知识
- 可空引用类型 NRT:建议先完成前一篇的学习
学习目标
- 掌握「历史动机与背景」的核心机制、典型用法与常见陷阱
- 掌握「形式化定义」的核心机制、典型用法与常见陷阱
- 掌握「理论推导」的核心机制、典型用法与常见陷阱
- 掌握「代码示例」的核心机制、典型用法与常见陷阱
- 掌握「对比分析」的核心机制、典型用法与常见陷阱
历史动机与背景
泛型的诞生:类型安全的复用
在泛型出现之前,复用容器代码只能依赖 System.Object 与装箱拆箱(boxing/unboxing)。.NET 1.x 的 ArrayList、Hashtable、Queue、Stack 接收 object,存在三类严重问题:
- 类型安全丧失:
ArrayList可同时装入string、int、DateTime,运行时才会暴露类型错误。 - 性能损耗:值类型装入
ArrayList需装箱为object,读取时拆箱,伴随堆分配与类型检查。int装箱在 32 位系统上从 4 字节膨胀为 12 字节(对象头 + 方法表指针 + 数据)。 - 代码可读性差:开发者需手动类型转换,编译期无法保证正确性。
2005 年,Don Syme 等人在 .NET 2.0 中引入泛型(Generics),同时由 C# 2.0、CLR 2.0 提供语言与运行时支持。设计目标:
- 编译期类型检查:错误前置到编译阶段,避免运行时
InvalidCastException。 - 零装箱开销:值类型实例化为专用代码,无
object转换。 - 代码复用:一份算法实现可适用于多种类型,保留类型信息。
CLR 泛型 vs Java 泛型
C# 泛型与 Java 泛型(2004 年 Java 5 引入)走出了两条截然不同的路径:
- CLR 泛型(reified generics):类型参数在运行时存在,
typeof(List<int>)与typeof(List<string>)是不同的Type对象。值类型泛型由 CLR 为每种值类型生成专用代码(如List<int>与List<double>是两个不同的 JIT 编译产物);引用类型泛型共享同一份代码(List<string>与List<object>共用方法实现,因引用都是 8 字节指针)。 - Java 泛型(type erasure):类型参数仅存在于编译期,编译后被擦除为
Object。List<String>与List<Integer>在运行时是同一个List类型。这导致无法new T()、无法instanceof List<String>、基本类型必须装箱(List<Integer>而非List<int>)。
CLR 选择 reified 的代价是元数据膨胀与潜在的代码膨胀,但获得了运行时类型信息与值类型零装箱的关键优势。
集合库的演进
- .NET 1.0(2002):
System.Collections命名空间,非泛型ArrayList、Hashtable、Queue、Stack、SortedList,基于object。 - .NET 2.0(2005):
System.Collections.Generic,泛型List<T>、Dictionary<TKey,TValue>、Queue<T>、Stack<T>、LinkedList<T>、SortedDictionary<TKey,TValue>、SortedList<TKey,TValue>、HashSet<T>(3.5 后)。 - .NET 3.5(2007):
HashSet<T>、Lookup<TKey,TElement>、扩展方法与 LINQ。 - .NET 4.0(2010):
System.Collections.Concurrent,ConcurrentDictionary<TKey,TValue>、ConcurrentQueue<T>、ConcurrentBag<T>、BlockingCollection<T>。 - .NET 4.5(2012):
IReadOnlyList<T>、IReadOnlyDictionary<TKey,TValue>、IReadOnlyCollection<T>。 - .NET 4.6(2015):
Array.Empty<T>()、Span<T>萌芽。 - .NET Core 2.0(2017):
Span<T>、Memory<T>、ReadOnlySpan<T>。 - .NET Core 3.0(2019):
Stack<T>内部优化为基于数组的紧凑布局。 - .NET 5(2020):集合性能大幅优化,
List<T>.AddRange改进、Dictionary内部结构升级。 - .NET 6(2021):
PriorityQueue<TElement,TPriority>引入,基于最小堆。 - .NET 8(2023):
FrozenDictionary<TKey,TValue>、FrozenSet<T>,针对只读场景优化查询性能;CollectionView、SortedDictionary改进。 - .NET 9(2024):
OrderedDictionary<TKey,TValue>(泛型版)、ReadOnlySet<T>、集合表达式([]与..)增强。
C# 12(2023)引入集合表达式(Collection Expressions),统一了数组、List<T>、Span<T>、IEnumerable<T> 的初始化语法,消除了历史遗留的多套初始化风格。
形式化定义
泛型类型的形式化模型
泛型类型可形式化为类型构造器(type constructor)。设类型宇宙 ,泛型类型 是从类型参数到具体类型的函数:
其中 是类型参数个数。例如 List<T> 是 的类型构造器,Dictionary<TKey, TValue> 是 。
实例化:给定类型实参 ,实例化类型为 。CLR 在运行时为每个不同的实例化创建独立的 Type 对象:
子类型化与协变逆变
设 是类型构造器, 表示子类型关系。
- 协变(Covariant): 协变当且仅当
- 逆变(Contravariant): 逆变当且仅当
- 不变(Invariant):既不协变也不逆变
C# 使用 out 标记协变类型参数(仅出现在输出位置),in 标记逆变类型参数(仅出现在输入位置):
类型安全证明(协变):设 IProducer<out T> 有方法 T Produce()。若 StringProducer : IProducer<string>,则 string Produce() 满足。将 IProducer<string> 视为 IProducer<object> 调用 Produce() 返回 string,可安全转为 object。类型安全成立。
类型安全反例(若 List<T> 协变):若 List<string> 可视为 List<object>,则 list.Add(42) 会向 string 数组写入 int,运行时崩溃。故 List<T> 必须不变。
集合的代数结构
集合类型可视为代数结构 ,其中 是元素集合,运算定义为:
- 插入
- 删除
- 并集
- 交集
- 差集
不同集合类型对运算的实现复杂度差异显著:
| 运算 | List<T> | HashSet<T> | SortedSet<T> | Dictionary |
|---|---|---|---|---|
| 插入 | 均摊 | 均摊 | 均摊 | |
| 删除 | ||||
| 查找 | ||||
| 排序 | - | - | - | |
| 集合运算 | - |
哈希函数的形式化
Dictionary<TKey, TValue> 与 HashSet<T> 基于哈希表。哈希函数 ,其中 是桶数。理想哈希函数满足:
- 均匀分布:
- 雪崩效应:输入微小变化导致输出大幅变化
- 确定性:相同输入恒定输出
冲突解决:CLR 采用链地址法(separate chaining),每个桶维护一个链表。装载因子 ,查找复杂度为 。当 超过阈值(默认 0.72 或 1.0 视集合而定)时触发扩容:(质数),重新哈希所有元素,均摊 。
理论推导
List<T> 动态扩容的均摊分析
List<T> 内部维护数组 _items,初始容量为 0 或构造时指定。Add 操作:
- 若
Count < Capacity,直接写入_items[Count++],。 - 若
Count == Capacity,扩容:Array.Resize分配新数组(容量翻倍 + 1),复制旧元素,再写入。扩容代价 。
均摊复杂度:设扩容序列为 。前 次插入的总代价:
均摊每次插入:。
容量预分配优化:若已知最终元素数 ,构造时使用 new List<T>(n) 或调用 EnsureCapacity(n),避免 次扩容与复制,总代价降至 (系数减半)。
Dictionary 哈希冲突的概率分析
设哈希表桶数 ,元素数 ,哈希函数均匀分布。生日问题给出至少一对冲突的概率:
当 时,冲突概率约 50%。链地址法下,单桶链表长度的期望为 ,查找复杂度 。
.NET 实现:Dictionary 在装载因子超过 0.72(默认)时扩容至下一个质数。质数选择避免哈希值与桶数的公约数导致的聚集(clustering)。CLR 维护质数表 HashHelpers.primes,扩容时查找下一个质数。
哈希碰撞攻击:若攻击者能构造大量哈希相同的键,将 Dictionary 退化为 查找的链表,引发 DoS。.NET 4.5+ 引入随机化字符串哈希(UseRandomizedStringHashAlgorithm),每次应用启动使用不同种子。
SortedDictionary 的红黑树复杂度
SortedDictionary<TKey, TValue> 基于红黑树(Red-Black Tree),保证:
- 树高
- 插入、删除、查找均为
- 有序遍历
红黑树通过五条不变量维护平衡:
- 节点是红色或黑色
- 根节点是黑色
- 叶子(NIL)是黑色
- 红色节点的子节点必须是黑色(不能连续两个红)
- 从任一节点到其所有叶子节点的路径包含相同数量的黑色节点
这些约束保证最长路径不超过最短路径的两倍,确保 高度。
协变逆变的类型推断
C# 编译器在方法调用时进行类型推断。设方法签名:
TResult Select<TSource, TResult>(IEnumerable<TSource> source, Func<TSource, TResult> selector)
调用 Select(list, x => x.ToString()) 时,编译器推断:
list类型为List<int>,List<int>实现IEnumerable<int>,推断TSource = int- Lambda
x => x.ToString()中x : int,返回string,推断TResult = string - 实例化为
Select<int, string>
类型推断算法基于约束求解,复杂度 ( 为类型参数数)。
Span<T> 与 Memory<T> 的内存零拷贝
Span<T> 是对连续内存区域的类型安全视图,定义为:
ref T是托管指针,指向内存起始length是元素数
Span<T> 可覆盖托管数组、栈分配 stackalloc、非托管内存 NativeMemory,无运行时装箱。但其必须 ref struct(不能装箱、不能作为字段、不能跨 await),保证生命周期局限于栈帧。
Memory<T> 是 Span<T> 的堆可存储版本,可用于异步方法字段,通过 MemoryMarshal.TryGetArray 获取底层 ArraySegment<T>。
切片 span.Slice(start, length) 仅需 修改指针与长度,无需复制数据,是零拷贝算法的关键。
代码示例
示例 1:泛型类与多约束
using System;
using System.Collections.Generic;
/// <summary>
/// 泛型缓存:支持带 TTL 的键值存储。
/// 多约束:TKey 必须 implements IComparable<TKey> 且有无参构造,
/// TValue 必须是 class(引用类型)。
/// </summary>
public class TtlCache<TKey, TValue>
where TKey : notnull, IComparable<TKey>, IEquatable<TKey>
where TValue : class
{
// 内部条目:存储值与过期时间
private readonly Dictionary<TKey, Entry> _store = new();
private readonly TimeSpan _ttl;
private readonly object _lock = new();
public TtlCache(TimeSpan ttl) => _ttl = ttl;
/// <summary>设置键值对,过期时间为当前时间 + TTL。</summary>
public void Set(TKey key, TValue value)
{
lock (_lock)
{
_store[key] = new Entry(value, DateTime.UtcNow + _ttl);
}
}
/// <summary>
/// 尝试获取值。若已过期则移除并返回 false。
/// 使用 ref 局部变量避免两次哈希查找。
/// </summary>
public bool TryGet(TKey key, out TValue? value)
{
lock (_lock)
{
ref var entry = ref CollectionsMarshal.GetValueRefOrNullRef(_store, key);
if (!Unsafe.IsNullRef(ref entry))
{
if (entry.ExpiresAt > DateTime.UtcNow)
{
value = entry.Value;
return true;
}
_store.Remove(key);
}
}
value = default;
return false;
}
/// <summary>清理所有过期条目。</summary>
public int Cleanup()
{
lock (_lock)
{
var now = DateTime.UtcNow;
var expired = new List<TKey>();
foreach (var kv in _store)
{
if (kv.Value.ExpiresAt <= now)
expired.Add(kv.Key);
}
expired.ForEach(k => _store.Remove(k));
return expired.Count;
}
}
private readonly record struct Entry(TValue Value, DateTime ExpiresAt);
}
示例 2:协变逆变与自定义集合
using System;
using System.Collections;
using System.Collections.Generic;
/// <summary>协变生产者接口:仅输出 T,不消费 T。</summary>
public interface IProducer<out T>
{
T Produce();
IEnumerable<T> ProduceMany(int count);
}
/// <summary>逆变消费者接口:仅消费 T,不输出 T。</summary>
public interface IConsumer<in T>
{
void Consume(T item);
void ConsumeAll(IEnumerable<T> items);
}
/// <summary>具体生产者:字符串工厂。</summary>
public sealed class StringFactory : IProducer<string>
{
private int _counter = 0;
public string Produce() => $"item-{++_counter}";
public IEnumerable<string> ProduceMany(int count)
{
for (int i = 0; i < count; i++)
yield return Produce();
}
}
/// <summary>具体消费者:日志记录器,能消费任意 object。</summary>
public sealed class Logger : IConsumer<object>
{
public void Consume(object item) =>
Console.WriteLine($"[Log] {item}");
public void ConsumeAll(IEnumerable<object> items)
{
foreach (var item in items)
Consume(item);
}
}
public class CovarianceDemo
{
public static void Run()
{
IProducer<string> stringProducer = new StringFactory();
// 协变:IProducer<string> 可视为 IProducer<object>
IProducer<object> objectProducer = stringProducer;
object val = objectProducer.Produce(); // 返回 string,安全转为 object
IConsumer<object> objectConsumer = new Logger();
// 逆变:IConsumer<object> 可视为 IConsumer<string>
IConsumer<string> stringConsumer = objectConsumer;
stringConsumer.Consume("hello"); // 调用接收 object 的方法,string 是 object
}
}
示例 3:自定义可枚举集合(环形缓冲区)
using System;
using System.Collections;
using System.Collections.Generic;
/// <summary>
/// 环形缓冲区:固定容量的 FIFO 队列,满时覆盖最旧元素。
/// 实现 IEnumerable<T> 支持 LINQ 与 foreach。
/// </summary>
public sealed class CircularBuffer<T> : IEnumerable<T>, IReadOnlyCollection<T>
{
private readonly T[] _buffer;
private int _head; // 下一个写入位置
private int _count;
public CircularBuffer(int capacity)
{
if (capacity <= 0)
throw new ArgumentOutOfRangeException(nameof(capacity));
_buffer = new T[capacity];
_head = 0;
_count = 0;
}
public int Capacity => _buffer.Length;
public int Count => _count;
/// <summary>添加元素。若已满,覆盖最旧元素并返回被覆盖的值。</summary>
public bool TryAdd(T item, out T? overwritten)
{
overwritten = default;
bool wasFull = _count == _buffer.Length;
if (wasFull)
overwritten = _buffer[_head]; // 即将被覆盖
_buffer[_head] = item;
_head = (_head + 1) % _buffer.Length;
if (!wasFull) _count++;
return wasFull;
}
/// <summary>按写入顺序返回元素(最旧到最新)。</summary>
public IEnumerator<T> GetEnumerator()
{
int start = _count == _buffer.Length ? _head : 0;
for (int i = 0; i < _count; i++)
{
yield return _buffer[(start + i) % _buffer.Length];
}
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class CircularBufferDemo
{
public static void Run()
{
var buf = new CircularBuffer<int>(3);
for (int i = 1; i <= 5; i++)
{
buf.TryAdd(i, out var overwritten);
if (overwritten != default)
Console.WriteLine($"覆盖: {overwritten}");
}
// 输出: 覆盖: 1, 覆盖: 2
Console.WriteLine(string.Join(", ", buf)); // 3, 4, 5
Console.WriteLine($"Sum: {buf.Sum()}, Max: {buf.Max()}"); // 12, 5
}
}
示例 4:迭代器组合与惰性求值
using System;
using System.Collections.Generic;
/// <summary>惰性序列生成器:基于 yield 的函数式风格。</summary>
public static class Seq
{
/// <summary>无限自然数序列(从 1 开始)。</summary>
public static IEnumerable<int> Naturals()
{
int n = 1;
while (true) yield return n++;
}
/// <summary>斐波那契数列。</summary>
public static IEnumerable<long> Fibonacci()
{
long a = 0, b = 1;
while (true)
{
yield return b;
(a, b) = (b, a + b);
}
}
/// <summary>素数序列(埃拉托色尼筛法变种)。</summary>
public static IEnumerable<int> Primes()
{
yield return 2;
var primes = new List<int> { 2 };
int candidate = 3;
while (true)
{
bool isPrime = true;
int sqrt = (int)Math.Sqrt(candidate);
foreach (var p in primes)
{
if (p > sqrt) break;
if (candidate % p == 0) { isPrime = false; break; }
}
if (isPrime)
{
primes.Add(candidate);
yield return candidate;
}
candidate += 2;
}
}
/// <summary>通用过滤:惰性迭代器组合的核心。</summary>
public static IEnumerable<T> Where<T>(
this IEnumerable<T> source, Func<T, bool> predicate)
{
foreach (var item in source)
if (predicate(item))
yield return item;
}
/// <summary>通用映射。</summary>
public static IEnumerable<TResult> Select<T, TResult>(
this IEnumerable<T> source, Func<T, TResult> selector)
{
foreach (var item in source)
yield return selector(item);
}
/// <summary>取前 n 个。</summary>
public static IEnumerable<T> Take<T>(
this IEnumerable<T> source, int count)
{
int i = 0;
foreach (var item in source)
{
if (i++ >= count) yield break;
yield return item;
}
}
}
public class IteratorDemo
{
public static void Run()
{
// 前 10 个偶斐波那契数的平方
var result = Seq.Fibonacci()
.Where(n => n % 2 == 0)
.Select(n => n * n)
.Take(10)
.ToList();
Console.WriteLine(string.Join(", ", result));
// 1, 9, 64, 1764, 16641, 93025, 113569, 4356, 116964, 278784
// (具体值取决于是否从 1 开始)
}
}
示例 5:Dictionary 性能优化与并发场景
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
public class DictionaryPerformance
{
/// <summary>统计词频:使用 GetValueRefOrNullRef 优化重复查找。</summary>
public static Dictionary<string, int> CountWords(IEnumerable<string> words)
{
var counts = new Dictionary<string, int>();
foreach (var word in words)
{
// .NET 6+: 获取引用避免两次哈希查找
ref int count = ref CollectionsMarshal.GetValueRefOrAddDefault(counts, word, out _);
count++;
}
return counts;
}
/// <summary>并发词频统计:使用 ConcurrentDictionary + 多线程。</summary>
public static ConcurrentDictionary<string, int> CountWordsParallel(IEnumerable<string> words)
{
var counts = new ConcurrentDictionary<string, int>();
var batches = words.Chunk(10_000);
Parallel.ForEach(batches, batch =>
{
foreach (var word in batch)
counts.AddOrUpdate(word, 1, (_, old) => old + 1);
});
return counts;
}
/// <summary>对比不同字典在只读场景下的查询性能。</summary>
public static void BenchmarkReadOnlyLookup()
{
var keys = Enumerable.Range(0, 100_000).Select(i => i.ToString()).ToArray();
var dict = keys.ToDictionary(k => k, k => k);
var frozen = dict.ToFrozenDictionary();
var immutable = dict.ToImmutableDictionary();
// 预热
for (int i = 0; i < 1000; i++)
{
_ = dict[keys[i]];
_ = frozen[keys[i]];
_ = immutable[keys[i]];
}
var sw = Stopwatch.StartNew();
for (int i = 0; i < keys.Length; i++) _ = dict[keys[i]];
sw.Stop();
Console.WriteLine($"Dictionary: {sw.ElapsedMilliseconds} ms");
sw.Restart();
for (int i = 0; i < keys.Length; i++) _ = frozen[keys[i]];
sw.Stop();
Console.WriteLine($"FrozenDictionary: {sw.ElapsedMilliseconds} ms");
sw.Restart();
for (int i = 0; i < keys.Length; i++) _ = immutable[keys[i]];
sw.Stop();
Console.WriteLine($"ImmutableDictionary: {sw.ElapsedMilliseconds} ms");
}
}
示例 6:不可变集合与函数式更新
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
/// <summary>不可变配置树:演示 ImmutableDictionary 的函数式更新。</summary>
public sealed class AppConfig
{
public ImmutableDictionary<string, string> Settings { get; }
public ImmutableList<string> AllowedOrigins { get; }
public ImmutableDictionary<string, int> RateLimits { get; }
public AppConfig()
: this(ImmutableDictionary<string, string>.Empty,
ImmutableList<string>.Empty,
ImmutableDictionary<string, int>.Empty)
{ }
private AppConfig(
ImmutableDictionary<string, string> settings,
ImmutableList<string> origins,
ImmutableDictionary<string, int> limits)
{
Settings = settings;
AllowedOrigins = origins;
RateLimits = limits;
}
/// <summary>更新设置:返回新的 AppConfig,原对象不变。</summary>
public AppConfig WithSetting(string key, string value) =>
new(Settings.SetItem(key, value), AllowedOrigins, RateLimits);
/// <summary>添加允许的源。</summary>
public AppConfig WithOrigin(string origin) =>
new(Settings, AllowedOrigins.Add(origin), RateLimits);
/// <summary>更新速率限制。</summary>
public AppConfig WithRateLimit(string endpoint, int limit) =>
new(Settings, AllowedOrigins, RateLimits.SetItem(endpoint, limit));
/// <summary>快照:所有集合已经是不可变的,可直接返回。</summary>
public AppConfig Snapshot() => this;
}
public class ImmutableDemo
{
public static void Run()
{
var config = new AppConfig()
.WithSetting("app.name", "FANDEX")
.WithSetting("env", "production")
.WithOrigin("https://fandex.example.com")
.WithRateLimit("/api/search", 100);
var updated = config.WithSetting("env", "staging");
Console.WriteLine(config.Settings["env"]); // production(原对象未变)
Console.WriteLine(updated.Settings["env"]); // staging
}
}
示例 7:PriorityQueue 实现任务调度
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// 优先级任务调度器:使用 PriorityQueue 按优先级出队。
/// 演示 .NET 6+ PriorityQueue 的典型用法。
/// </summary>
public sealed class PriorityScheduler : IDisposable
{
private readonly PriorityQueue<Func<Task>, (int Priority, long Sequence)> _queue = new();
private readonly object _lock = new();
private readonly SemaphoreSlim _signal = new(0);
private readonly CancellationTokenSource _cts = new();
private long _sequence;
private readonly Task _worker;
public PriorityScheduler()
{
_worker = Task.Run(WorkerLoop);
}
/// <summary>提交任务:优先级数值越小越优先执行。</summary>
public void Submit(Func<Task> task, int priority)
{
lock (_lock)
{
// 使用 (Priority, Sequence) 作为复合优先级,保证相同优先级 FIFO
_queue.Enqueue(task, (priority, ++_sequence));
}
_signal.Release();
}
private async Task WorkerLoop()
{
await _signal.WaitAsync(_cts.Token);
while (!_cts.IsCancellationRequested)
{
Func<Task> task;
lock (_lock)
{
if (_queue.Count == 0)
{
Monitor.Exit(_lock);
await _signal.WaitAsync(_cts.Token);
Monitor.Enter(_lock);
if (_queue.Count == 0) continue;
}
task = _queue.Dequeue();
}
try
{
await task();
}
catch (Exception ex)
{
Console.WriteLine($"任务执行失败: {ex.Message}");
}
}
}
public void Dispose()
{
_cts.Cancel();
_signal.Release();
_worker.Wait(TimeSpan.FromSeconds(5));
_cts.Dispose();
_signal.Dispose();
}
}
public class SchedulerDemo
{
public static async Task Run()
{
using var scheduler = new PriorityScheduler();
scheduler.Submit(async () =>
{
Console.WriteLine("低优先级任务");
await Task.Delay(100);
}, priority: 10);
scheduler.Submit(async () =>
{
Console.WriteLine("高优先级任务");
await Task.Delay(100);
}, priority: 1);
scheduler.Submit(async () =>
{
Console.WriteLine("中优先级任务");
await Task.Delay(100);
}, priority: 5);
await Task.Delay(1000);
// 输出顺序:高 -> 中 -> 低
}
}
示例 8:自定义相等比较器与排序
using System;
using System.Collections.Generic;
using System.Linq;
/// <summary>不区分大小写的字符串比较器。</summary>
public sealed class CaseInsensitiveComparer : IEqualityComparer<string>, IComparer<string>
{
public static readonly CaseInsensitiveComparer Instance = new();
public bool Equals(string? x, string? y) =>
string.Equals(x, y, StringComparison.OrdinalIgnoreCase);
public int GetHashCode(string obj) =>
StringComparer.OrdinalIgnoreCase.GetHashCode(obj);
public int Compare(string? x, string? y) =>
string.Compare(x, y, StringComparison.OrdinalIgnoreCase);
}
/// <summary>多字段排序器:链式比较。</summary>
public sealed class ChainedComparer<T> : IComparer<T>
{
private readonly List<(Func<T, T, int> Compare, bool Descending)> _comparisons = new();
public static ChainedComparer<T> Create() => new();
public ChainedComparer<T> ThenBy<TKey>(Func<T, TKey> keySelector, bool descending = false)
where TKey : IComparable<TKey>
{
_comparisons.Add((a, b =>
{
int r = keySelector(a).CompareTo(keySelector(b));
return descending ? -r : r;
}, false));
return this;
}
public int Compare(T? x, T? y)
{
if (x is null && y is null) return 0;
if (x is null) return -1;
if (y is null) return 1;
foreach (var (cmp, _) in _comparisons)
{
int r = cmp(x, y);
if (r != 0) return r;
}
return 0;
}
}
public class Person
{
public string Name { get; set; } = "";
public int Age { get; set; }
public string City { get; set; } = "";
public override string ToString() => $"{Name} ({Age}, {City})";
}
public class ComparerDemo
{
public static void Run()
{
var people = new List<Person>
{
new() { Name = "Alice", Age = 30, City = "Beijing" },
new() { Name = "Bob", Age = 25, City = "Shanghai" },
new() { Name = "alice", Age = 28, City = "Beijing" },
new() { Name = "Charlie", Age = 30, City = "Shenzhen" },
};
// 使用不区分大小写的比较器去重(按 Name)
var byName = new Dictionary<string, Person>(CaseInsensitiveComparer.Instance);
foreach (var p in people)
byName[p.Name] = p;
Console.WriteLine($"按 Name 去重后剩余: {byName.Count}"); // 3(Alice 与 alice 合并)
// 链式排序:先按 City,再按 Age 降序
var sorted = people
.OrderBy(p => p.City)
.ThenByDescending(p => p.Age)
.ToList();
sorted.ForEach(Console.WriteLine);
}
}
示例 9:Span<T> 零拷贝切片
using System;
using System.Linq;
using System.Text;
public class SpanDemo
{
/// <summary>统计字节数组中等于目标值的元素个数,零分配。</summary>
public static int Count(byte[] source, byte target)
{
var span = source.AsSpan();
int count = 0;
foreach (var b in span)
if (b == target) count++;
return count;
}
/// <summary>反转字符串中的字符顺序,无需中间字符串。</summary>
public static string ReverseString(string input)
{
if (string.IsNullOrEmpty(input)) return input;
// 借助 Span<char> 避免多次字符串分配
Span<char> buffer = input.Length <= 256
? stackalloc char[input.Length]
: new char[input.Length];
input.AsSpan().CopyTo(buffer);
buffer.Reverse();
return new string(buffer);
}
/// <summary>解析整数行:基于 ReadOnlySpan 的零拷贝解析。</summary>
public static int[] ParseNumbers(ReadOnlySpan<char> input, char separator = ',')
{
var result = new List<int>();
while (!input.IsEmpty)
{
int idx = input.IndexOf(separator);
ReadOnlySpan<char> token = idx < 0 ? input : input[..idx];
if (!token.IsEmpty && int.TryParse(token, out int n))
result.Add(n);
if (idx < 0) break;
input = input[(idx + 1)..];
}
return result.ToArray();
}
/// <summary>base64 编码:使用 Span 避免中间 byte[] 分配。</summary>
public static string ToBase64(ReadOnlySpan<byte> data)
{
// 预分配精确大小,避免 Convert.ToBase64String 内部缓冲区扩容
Span<char> chars = stackalloc char[4 * ((data.Length + 2) / 3)];
if (Convert.TryToBase64Chars(data, chars, out int written))
return new string(chars[..written]);
throw new InvalidOperationException("base64 编码失败");
}
public static void Run()
{
Console.WriteLine(ReverseString("hello")); // olleh
var nums = ParseNumbers("1, 22, 333, 4444");
Console.WriteLine(string.Join(", ", nums)); // 1, 22, 333, 4444
var bytes = Encoding.UTF8.GetBytes("hello world");
Console.WriteLine(ToBase64(bytes)); // aGVsbG8gd29ybGQ=
}
}
示例 10:集合表达式(C# 12)与展开运算符
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
public class CollectionExpressionDemo
{
public static void Run()
{
// 统一初始化语法
int[] array = [1, 2, 3];
List<string> list = ["a", "b", "c"];
Span<int> span = [1, 2, 3];
HashSet<string> set = ["x", "y", "z"];
ImmutableArray<int> immutable = [1, 2, 3];
// 展开运算符 ..
int[] a = [1, 2, 3];
int[] b = [4, 5, 6];
int[] combined = [..a, ..b, 7, 8, 9];
Console.WriteLine(string.Join(", ", combined)); // 1,2,3,4,5,6,7,8,9
// 作为方法参数
Process([1, 2, 3, 4, 5]);
Process([..a, ..b]);
}
/// <summary>方法接受 ReadOnlySpan,调用方可传集合表达式。</summary>
static void Process(ReadOnlySpan<int> values)
{
Console.WriteLine($"处理 {values.Length} 个值: {string.Join(", ", values.ToArray())}");
}
/// <summary>返回集合表达式:编译器自动选择合适的目标类型。</summary>
static IEnumerable<int> GenerateNumbers()
{
return [1, 2, 3, 4, 5];
}
/// <summary>泛型方法中的集合表达式(需指定目标类型)。</summary>
static List<T> AppendDefault<T>(List<T> source)
{
// 此处不能直接用 [..source, default],编译器需明确目标类型
var result = new List<T>(source) { default! };
return result;
}
}
对比分析
C# 泛型与其他语言泛型对比
| 特性 | C#(CLR) | Java(JVM) | C++(模板) | Rust |
|---|---|---|---|---|
| 实现机制 | Reified(运行时类型存在) | Type Erasure(编译期擦除) | 编译期代码生成 | 单态化 |
| 值类型泛型 | 零装箱,专用代码 | 必须装箱(Integer) | 零开销 | 零开销 |
new T() | 通过 where T : new() 约束 | 不支持 | 通过默认构造模板 | 通过 Default trait |
| 运行时类型信息 | typeof(T) 可用 | 不可用 | 不可用(无反射) | 受限(TypeId) |
| 协变逆变 | out/in 显式声明 | 通配符 ? extends/? super | 不支持 | 生命周期与 trait bound |
| 约束 | where T : IComparable<T> | <T extends Comparable<T>> | 概念(C++20) | T: Ord |
| 代码膨胀 | 引用类型共享代码 | 无(共享 Object) | 严重(每种类型独立生成) | 严重 |
| 反射 | 完整支持 | 受限 | 不支持 | 受限 |
讨论:C# 泛型在类型安全、性能、表达力之间取得了良好平衡。Java 通过类型擦除简化实现,但牺牲了运行时类型信息与值类型效率;C++ 模板是最强大的元编程工具,但缺乏约束导致错误信息难读、代码膨胀严重;Rust 通过 trait bound 与单态化实现了零开销抽象,但学习曲线陡峭。
集合类型选择决策矩阵
| 需求场景 | 推荐集合 | 备选 | 原因 |
|---|---|---|---|
| 频繁随机索引访问 | List<T> / T[] | Array | 索引,缓存友好 |
| 频繁尾部增删 | List<T> | Queue<T> | 均摊 ,但中间插入慢 |
| 频繁首尾增删 | LinkedList<T> | Deque | 双端操作,但内存开销大 |
| 键值查找 | Dictionary<TKey,TValue> | SortedDictionary | 查找 |
| 有序键值查找 | SortedDictionary | SortedList | 查找,插入 |
| 内存敏感的有序 | SortedList<TKey,TValue> | - | 紧凑数组,但插入 |
| 元素去重 | HashSet<T> | Dictionary(值设为 byte) | 包含检查 |
| 有序去重 | SortedSet<T> | - | 有序操作 |
| 先进先出 | Queue<T> | ConcurrentQueue | 入队出队 |
| 后进先出 | Stack<T> | ConcurrentStack | 入栈出栈 |
| 优先级出队 | PriorityQueue<TElement,TPriority> | - | 堆操作 |
| 只读快查 | FrozenDictionary (.NET 8+) | ImmutableDictionary | 极速查询,构建代价高 |
| 只读快查(小集合) | FrozenSet | HashSet | 编译期优化,5-20 元素最快 |
| 多线程读写 | ConcurrentDictionary | Dictionary + 锁 | 细粒度锁,并发友好 |
| 生产者-消费者 | ConcurrentQueue / BlockingCollection | - | 线程安全入队出队 |
| 不可变持久化 | ImmutableList<T> | ImmutableArray | 函数式更新,结构共享 |
| 高性能零拷贝 | Span<T> / Memory<T> | T[] + offset/length | 无分配切片 |
不可变集合对比
| 集合 | 内部结构 | 构建成本 | 查询复杂度 | 更新复杂度 | 适用场景 |
|---|---|---|---|---|---|
ImmutableArray<T> | 数组 | 复制 | 索引 | 全复制 | 构建后只读,频繁索引 |
ImmutableList<T> | AVL 树 | 频繁更新,结构共享 | |||
ImmutableDictionary | 哈希字典树 | 平均 | 频繁更新键值 | ||
ImmutableHashSet | 哈希集合树 | 平均 | 频繁更新去重 | ||
FrozenDictionary | 优化哈希表 | 高 | 极快 | 不可更新 | 启动后只读配置 |
FrozenSet | 优化哈希表 | 高 | 极快 | 不可更新 | 启动后只读集合 |
ReadOnlyDictionary | 包装现有字典 | 包装 | 与底层一致 | 不可更新 | 视图层防修改 |
哈希集合与排序集合复杂度对比
| 操作 | HashSet<T> | SortedSet<T> | List<T> |
|---|---|---|---|
Add | 均摊 | 均摊 | |
Remove | |||
Contains | |||
Clear | |||
| 有序遍历 | 无序 | 升序 | 输入序 |
| 范围查询 | 不支持 | GetViewBetween | Skip/Take |
| 集合运算 |
常见陷阱与反模式
陷阱 1:在 foreach 中修改集合
// 反模式:迭代时修改集合,抛出 InvalidOperationException
var list = new List<int> { 1, 2, 3, 4, 5 };
foreach (var n in list)
{
if (n % 2 == 0)
list.Remove(n); // 抛出异常:集合已修改
}
// 正确:使用 RemoveAll 一次性删除
list.RemoveAll(n => n % 2 == 0);
// 或:先收集要删除的,再倒序删除
var toRemove = list.Where(n => n % 2 == 0).ToList();
foreach (var n in toRemove)
list.Remove(n);
// 或:迭代时使用 for 循环倒序
for (int i = list.Count - 1; i >= 0; i--)
{
if (list[i] % 2 == 0)
list.RemoveAt(i);
}
原因:List<T>.Enumerator 维护版本号,Add/Remove/Clear 递增版本号,迭代时检查不一致则抛异常。
陷阱 2:Dictionary 键的可变性
// 反模式:使用可变对象作为 Dictionary 键,修改后丢失
var dict = new Dictionary<Person, string>();
var p = new Person { Name = "Alice", Age = 30 };
dict[p] = "engineer";
p.Age = 31; // 修改了影响哈希码的字段
_ = dict[p]; // KeyNotFoundException,哈希码已变,找不到桶
// 正确:使用不可变对象作为键
public sealed record PersonKey(string Name, int Age);
var key = new PersonKey("Alice", 30);
dict[key] = "engineer";
// PersonKey 是 record,无法修改,键永远稳定
原则:作为字典键的对象必须不可变,或至少影响 GetHashCode 与 Equals 的字段不可变。
陷阱 3:值类型集合的装箱
// 反模式:非泛型 ArrayList 装箱值类型,性能极差
var list = new ArrayList();
for (int i = 0; i < 1_000_000; i++)
list.Add(i); // 每次装箱:4 字节 int -> 12 字节 boxed object
// 总内存:12 MB 而非 4 MB,且装箱耗时
// 正确:使用泛型 List<int>
var list = new List<int>();
for (int i = 0; i < 1_000_000; i++)
list.Add(i); // 直接写入 int[],无装箱
性能差距:在 .NET 8 实测,相同操作 List<int> 比 ArrayList 快约 5-10 倍,内存占用少 3 倍。
陷阱 4:误解协变只读安全
// 误以为 IEnumerable<object> 可以写入
IEnumerable<object> objects = new List<string> { "a", "b" };
// objects.Add("c"); // 编译错误:IEnumerable<object> 没有 Add
// 但若强制转换为 List<object>:
var list = (List<object>)objects; // InvalidCastException
// 正确:使用 IEnumerable<T> 仅读取
foreach (object o in objects)
Console.WriteLine(o);
// 需要写入:创建新集合
var writable = objects.ToList(); // List<object>
writable.Add("c");
陷阱 5:迭代器的延迟执行副作用
// 反模式:多次枚举 yield 迭代器触发重复计算
IEnumerable<int> Generate()
{
Console.WriteLine("generating...");
for (int i = 0; i < 5; i++) yield return i;
}
var nums = Generate();
var sum = nums.Sum(); // 输出 "generating...",计算
var count = nums.Count(); // 再次输出 "generating...",重新计算
var list = nums.ToList(); // 第三次输出 "generating..."
// 正确:缓存结果
var cached = nums.ToList(); // 仅迭代一次
var sum = cached.Sum();
var count = cached.Count;
原则:IEnumerable<T> 可能是延迟序列,多次遍历会重复执行。需要多次访问时先 ToList() 或 ToArray() 物化。
陷阱 6:线程安全的误解
// 反模式:多个线程同时向 List<T> 添加,数据竞争
var list = new List<int>();
Parallel.For(0, 10000, i => list.Add(i)); // 可能丢失元素、抛异常、数据损坏
// 正确:使用 ConcurrentBag 或加锁
var bag = new ConcurrentBag<int>();
Parallel.For(0, 10000, i => bag.Add(i));
// 或:使用锁
var list2 = new List<int>();
var lockObj = new object();
Parallel.For(0, 10000, i =>
{
lock (lockObj) list2.Add(i);
});
// 或:先分区后合并
var results = Enumerable.Range(0, 10000)
.AsParallel()
.Select(i => i)
.ToList(); // PLINQ 自动处理
陷阱 7:HashSet 性能因哈希函数退化
// 反模式:自定义类型未重写 GetHashCode/Equals,使用默认引用相等
public class Item
{
public int Id { get; set; }
// 未重写 Equals 与 GetHashCode
}
var set = new HashSet<Item>();
set.Add(new Item { Id = 1 });
set.Add(new Item { Id = 1 }); // 不同对象,被加入,去重失败
Console.WriteLine(set.Count); // 2,预期 1
// 正确:重写 Equals 与 GetHashCode,或使用 record
public sealed record Item(int Id);
var set2 = new HashSet<Item>();
set2.Add(new Item(1));
set2.Add(new Item(1)); // Equals 返回 true,被去重
Console.WriteLine(set2.Count); // 1
原则:放入 HashSet<T> 或作为 Dictionary 键的类型必须正确实现 Equals 与 GetHashCode,保证相等对象有相同哈希码。record 自动实现结构化相等。
陷阱 8:List 容量未预分配导致扩容开销
// 反模式:知道最终大小但未预分配,触发多次扩容
var list = new List<int>();
for (int i = 0; i < 1_000_000; i++)
list.Add(i); // 触发约 20 次扩容与复制
// 正确:构造时指定容量
var list2 = new List<int>(1_000_000);
for (int i = 0; i < 1_000_000; i++)
list2.Add(i); // 无扩容
// 或:使用 EnsureCapacity
var list3 = new List<int>();
list3.EnsureCapacity(1_000_000);
for (int i = 0; i < 1_000_000; i++)
list3.Add(i);
性能差距:1 百万元素插入,预分配版本快约 2 倍,内存碎片更少。
陷阱 9:误用 LINQ 多次枚举数据库查询
// 反模式:EF Core 查询作为 IEnumerable 多次遍历,每次访问数据库
using var db = new AppDbContext();
var query = db.Users.Where(u => u.Age > 18); // IQueryable<User>
// 第一次:执行 SQL
var first = query.FirstOrDefault();
// 第二次:再次执行 SQL
var count = query.Count();
// 第三次:再次执行 SQL
var all = query.ToList();
// 正确:一次性加载到内存
var users = db.Users.Where(u => u.Age > 18).ToList();
var first = users.FirstOrDefault();
var count = users.Count;
// 或:根据需要分别构造不同 SQL
var first = db.Users.Where(u => u.Age > 18).FirstOrDefault();
var count = db.Users.Where(u => u.Age > 18).Count();
陷阱 10:Span<T> 错误的栈分配
// 反模式:stackalloc 大量数据导致栈溢出
public static string Process(string input)
{
Span<char> buffer = stackalloc char[input.Length]; // 输入 100MB 时栈溢出
// ...
return new string(buffer);
}
// 正确:根据大小选择栈分配或堆分配
public static string Process(string input)
{
if (input.Length > 256)
{
Span<char> buffer = new char[input.Length]; // 堆分配
input.AsSpan().CopyTo(buffer);
// ... 处理
return new string(buffer);
}
else
{
Span<char> buffer = stackalloc char[input.Length]; // 栈分配
input.AsSpan().CopyTo(buffer);
// ... 处理
return new string(buffer);
}
}
// 反模式:将 Span 存储为字段
public class Bad
{
private Span<int> _data; // 编译错误:Span<T> 是 ref struct,不能作为字段
}
// 反模式:跨 await 使用 Span
public async Task BadAsync()
{
Span<byte> buffer = stackalloc byte[1024];
await SomeAsyncMethod(); // 编译错误:Span 不能跨 await
}
工程实践
实践 1:选择正确的集合类型
决策流程:
- 是否需要键值映射?
- 是 → 是否需要有序?是 →
SortedDictionary;否 →Dictionary - 否 → 继续
- 是 → 是否需要有序?是 →
- 是否需要去重?
- 是 → 是否需要有序?是 →
SortedSet;否 →HashSet - 否 → 继续
- 是 → 是否需要有序?是 →
- 是否需要随机索引访问?
- 是 →
List<T>或T[] - 否 → 继续
- 是 →
- 访问模式?
- FIFO →
Queue<T> - LIFO →
Stack<T> - 优先级 →
PriorityQueue<TElement, TPriority> - 双端频繁增删 →
LinkedList<T>
- FIFO →
- 是否线程安全?
- 是 →
ConcurrentDictionary/ConcurrentQueue/ConcurrentBag - 否 → 上述选择
- 是 →
- 是否只读且长期使用?
- 是 →
FrozenDictionary/FrozenSet(启动时构建) - 否 → 上述选择
- 是 →
实践 2:性能优化的 7 个技巧
// 1. 预分配容量
var list = new List<int>(knownSize);
var dict = new Dictionary<string, int>(expectedCount);
// 2. 使用 CollectionsMarshal 避免重复哈希查找
ref int value = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out _);
value++;
// 3. 只读查询使用 AsReadOnly 或 FrozenSet
var readOnly = list.AsReadOnly();
var frozen = dict.ToFrozenDictionary();
// 4. 大集合分批处理使用 Chunk
foreach (var batch in largeList.Chunk(1000))
ProcessBatch(batch);
// 5. 使用 Array.Empty<T>() 代替 new T[0]
static IEnumerable<T> EmptyIfNull<T>(IEnumerable<T>? source) =>
source ?? Array.Empty<T>();
// 6. 字符串键使用 StringComparer 而非默认
var dict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
// 7. 使用 Span 切片避免 LINQ 在性能敏感路径的开销
public static int SumRange(int[] data, int start, int length)
{
var slice = data.AsSpan(start, length);
int sum = 0;
foreach (var n in slice) sum += n;
return sum; // 比 data.Skip(start).Take(length).Sum() 快 10 倍以上
}
实践 3:实现自定义可枚举类型
using System;
using System.Collections;
using System.Collections.Generic;
/// <summary>
/// 自定义分页序列:支持延迟求值与多次遍历。
/// 实现 IEnumerable<T> 使其与 LINQ 兼容。
/// </summary>
public sealed class PagedSequence<T> : IEnumerable<IEnumerable<T>>
{
private readonly IEnumerable<T> _source;
private readonly int _pageSize;
public PagedSequence(IEnumerable<T> source, int pageSize)
{
if (pageSize <= 0)
throw new ArgumentOutOfRangeException(nameof(pageSize));
_source = source;
_pageSize = pageSize;
}
public IEnumerator<IEnumerable<T>> GetEnumerator()
{
var page = new List<T>(_pageSize);
foreach (var item in _source)
{
page.Add(item);
if (page.Count == _pageSize)
{
yield return page;
page = new List<T>(_pageSize);
}
}
if (page.Count > 0)
yield return page;
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
public class PaginationDemo
{
public static void Run()
{
var numbers = Enumerable.Range(1, 25);
var paged = new PagedSequence<int>(numbers, 10);
foreach (var page in paged)
{
Console.WriteLine($"页 ({page.Count()}): {string.Join(", ", page)}");
}
}
}
实践 4:领域特定的泛型仓储
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
/// <summary>泛型仓储接口:定义标准 CRUD 与查询。</summary>
public interface IRepository<T, in TKey> where T : class
{
Task<T?> GetByIdAsync(TKey id);
Task<IReadOnlyList<T>> FindAsync(Expression<Func<T, bool>> predicate);
Task<IReadOnlyList<T>> GetAllAsync();
Task<T> AddAsync(T entity);
Task UpdateAsync(T entity);
Task DeleteAsync(TKey id);
Task<int> CountAsync(Expression<Func<T, bool>>? predicate = null);
}
/// <summary>内存仓储:用于单元测试与原型开发。</summary>
public class InMemoryRepository<T, TKey> : IRepository<T, TKey>
where T : class
where TKey : notnull
{
private readonly Dictionary<TKey, T> _store = new();
private readonly Func<T, TKey> _keySelector;
public InMemoryRepository(Func<T, TKey> keySelector) => _keySelector = keySelector;
public Task<T?> GetByIdAsync(TKey id) =>
Task.FromResult(_store.TryGetValue(id, out var t) ? t : null);
public Task<IReadOnlyList<T>> FindAsync(Expression<Func<T, bool>> predicate)
{
var compiled = predicate.Compile();
var results = _store.Values.Where(compiled).ToList();
return Task.FromResult<IReadOnlyList<T>>(results);
}
public Task<IReadOnlyList<T>> GetAllAsync() =>
Task.FromResult<IReadOnlyList<T>>(_store.Values.ToList());
public Task<T> AddAsync(T entity)
{
var key = _keySelector(entity);
_store[key] = entity;
return Task.FromResult(entity);
}
public Task UpdateAsync(T entity)
{
_store[_keySelector(entity)] = entity;
return Task.CompletedTask;
}
public Task DeleteAsync(TKey id)
{
_store.Remove(id);
return Task.CompletedTask;
}
public Task<int> CountAsync(Expression<Func<T, bool>>? predicate = null)
{
var count = predicate is null
? _store.Count
: _store.Values.Count(predicate.Compile());
return Task.FromResult(count);
}
}
实践 5:高并发场景的 LRU 缓存
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
/// <summary>
/// 线程安全的 LRU 缓存:基于 Dictionary + LinkedList,读写均加锁。
/// 适用于读多写多的中小规模缓存(数千到数十万项)。
/// </summary>
public sealed class LruCache<TKey, TValue> where TKey : notnull
{
private readonly int _capacity;
private readonly Dictionary<TKey, LinkedListNode<CacheItem>> _dict;
private readonly LinkedList<CacheItem> _list;
private readonly object _lock = new();
public LruCache(int capacity)
{
if (capacity <= 0) throw new ArgumentOutOfRangeException(nameof(capacity));
_capacity = capacity;
_dict = new Dictionary<TKey, LinkedListNode<CacheItem>>(capacity);
_list = new LinkedList<CacheItem>();
}
public bool TryGet(TKey key, out TValue? value)
{
lock (_lock)
{
if (_dict.TryGetValue(key, out var node))
{
// 移动到链表头部(最近使用)
_list.Remove(node);
_list.AddFirst(node);
value = node.Value.Value;
return true;
}
}
value = default;
return false;
}
public void Set(TKey key, TValue value)
{
lock (_lock)
{
if (_dict.TryGetValue(key, out var node))
{
node.Value.Value = value;
_list.Remove(node);
_list.AddFirst(node);
}
else
{
if (_dict.Count >= _capacity)
{
// 淘汰最久未使用(链表尾部)
var last = _list.Last!;
_list.RemoveLast();
_dict.Remove(last.Key);
}
var item = new CacheItem(key, value);
var newNode = _list.AddFirst(item);
_dict[key] = newNode;
}
}
}
public int Count
{
get { lock (_lock) return _dict.Count; }
}
private sealed class CacheItem
{
public TKey Key { get; }
public TValue Value { get; set; }
public CacheItem(TKey key, TValue value) { Key = key; Value = value; }
}
}
/// <summary>
/// 无锁版 LRU 缓存:基于 ConcurrentDictionary,读不加锁。
/// 适合读远多于写的场景。
/// </summary>
public sealed class ConcurrentLruCache<TKey, TValue> where TKey : notnull
{
private readonly int _capacity;
private readonly ConcurrentDictionary<TKey, long> _accessTimes;
private readonly ConcurrentDictionary<TKey, TValue> _values;
private long _accessCounter;
public ConcurrentLruCache(int capacity)
{
_capacity = capacity;
_accessTimes = new ConcurrentDictionary<TKey, long>();
_values = new ConcurrentDictionary<TKey, TValue>();
}
public bool TryGet(TKey key, out TValue? value)
{
if (_values.TryGetValue(key, out var v))
{
_accessTimes[key] = Interlocked.Increment(ref _accessCounter);
value = v;
return true;
}
value = default;
return false;
}
public void Set(TKey key, TValue value)
{
_values[key] = value;
_accessTimes[key] = Interlocked.Increment(ref _accessCounter);
// 后台清理:低频触发,避免每次写入都扫描
if (_values.Count > _capacity * 2)
{
var toRemove = _accessTimes
.OrderBy(kv => kv.Value)
.Take(_values.Count - _capacity)
.Select(kv => kv.Key)
.ToList();
foreach (var k in toRemove)
{
_values.TryRemove(k, out _);
_accessTimes.TryRemove(k, out _);
}
}
}
}
案例研究
案例 1:日志分析系统的集合选型
场景:处理每日 10 亿条日志记录,需支持按时间范围查询、按关键词过滤、按用户聚合。
初始实现(性能瓶颈):
// 反模式:所有日志加载到 List<LogEntry>,每查询全扫描
public class LogStore
{
private readonly List<LogEntry> _logs = new();
public void Add(LogEntry log) => _logs.Add(log);
public List<LogEntry> Query(DateTime start, DateTime end, string? keyword)
{
return _logs
.Where(l => l.Timestamp >= start && l.Timestamp <= end)
.Where(l => keyword is null || l.Message.Contains(keyword))
.ToList(); // 10 亿条全扫描,单次查询分钟级
}
}
优化版本(分层索引):
public class OptimizedLogStore
{
// 时间索引:按小时分桶
private readonly Dictionary<DateTime, List<LogEntry>> _byHour = new();
// 用户索引:倒排
private readonly Dictionary<string, HashSet<int>> _byUser = new();
// 关键词倒排(分词后)
private readonly Dictionary<string, HashSet<int>> _byKeyword = new();
// 全量存储(按 ID)
private readonly Dictionary<int, LogEntry> _byId = new();
private int _nextId;
public void Add(LogEntry log)
{
int id = _nextId++;
_byId[id] = log;
var hour = log.Timestamp.Date.AddHours(log.Timestamp.Hour);
if (!_byHour.TryGetValue(hour, out var bucket))
{
bucket = new List<LogEntry>();
_byHour[hour] = bucket;
}
bucket.Add(log);
_byUser.GetOrAdd(log.UserId, _ => new HashSet<int>()).Add(id);
foreach (var word in Tokenize(log.Message))
_byKeyword.GetOrAdd(word, _ => new HashSet<int>()).Add(id);
}
public List<LogEntry> Query(DateTime start, DateTime end, string? keyword)
{
IEnumerable<int> candidates = null;
// 时间过滤:直接定位小时桶
var hours = EnumerateHours(start, end);
candidates = hours.SelectMany(h =>
_byHour.TryGetValue(h, out var b) ? b.Select(l => _nextId - b.Count + b.IndexOf(l)) : Enumerable.Empty<int>());
// 关键词过滤:交集
if (keyword is not null)
{
var words = Tokenize(keyword);
foreach (var word in words)
{
if (_byKeyword.TryGetValue(word, out var ids))
candidates = candidates is null
? ids.AsEnumerable()
: candidates.Intersect(ids);
else
return new List<LogEntry>();
}
}
return candidates
.Where(id => _byId.TryGetValue(id, out var l) && l.Timestamp >= start && l.Timestamp <= end)
.Select(id => _byId[id])
.ToList();
}
private static List<string> Tokenize(string text) =>
text.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList();
private static IEnumerable<DateTime> EnumerateHours(DateTime start, DateTime end)
{
for (var h = start.Date.AddHours(start.Hour); h <= end; h = h.AddHours(1))
yield return h;
}
}
public record LogEntry(DateTime Timestamp, string UserId, string Message);
优化效果:
- 时间范围查询从 降至 ( 为范围内日志数)
- 关键词查询从 降至 ( 为匹配数)
- 内存占用增加约 3 倍(索引开销),但查询性能提升 1000 倍
案例 2:配置中心的多级缓存
场景:微服务架构中,配置中心需要将配置分发到数千个服务实例,配置项约 10 万条,要求查询 < 1ms。
架构:
public class ConfigCenter
{
// L1: 每实例内存缓存(FrozenDictionary,启动时构建)
private FrozenDictionary<string, ConfigValue>? _l1Cache;
// L2: 本地持久化缓存(如 Redis),热启动加载
private readonly Dictionary<string, ConfigValue> _l2Cache = new();
// L3: 远程配置中心
private readonly IConfigService _remoteService;
public ConfigCenter(IConfigService remoteService) => _remoteService = remoteService;
/// <summary>启动时加载并构建 L1 缓存。</summary>
public async Task InitializeAsync()
{
var allConfigs = await _remoteService.LoadAllAsync();
_l1Cache = allConfigs.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
}
/// <summary>查询:优先 L1,未命中走 L2,再未命中走 L3。</summary>
public async Task<ConfigValue?> GetAsync(string key)
{
// L1: FrozenDictionary,纳秒级
if (_l1Cache?.TryGetValue(key, out var v) == true)
return v;
// L2: Dictionary,纳秒级
if (_l2Cache.TryGetValue(key, out v))
return v;
// L3: 远程调用,毫秒级
v = await _remoteService.GetAsync(key);
if (v is not null)
{
_l2Cache[key] = v;
// L1 不可变,需重建(异步触发)
_ = RebuildL1Async();
}
return v;
}
/// <summary>定期重建 L1 缓存(后台任务)。</summary>
private async Task RebuildL1Async()
{
var all = await _remoteService.LoadAllAsync();
Interlocked.Exchange(ref _l1Cache, all.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase));
}
}
public record ConfigValue(string Key, string Value, DateTime UpdatedAt);
public interface IConfigService
{
Task<ConfigValue?> GetAsync(string key);
Task<IReadOnlyDictionary<string, ConfigValue>> LoadAllAsync();
}
性能数据:
- L1 命中:< 100ns(FrozenDictionary 高度优化)
- L2 命中:< 200ns(Dictionary 查找)
- L3 命中:~ 5ms(网络往返)
- L1 命中率 > 99.9%,平均查询时间 < 200ns
案例 3:实时流处理的窗口聚合
场景:实时统计每分钟、每 5 分钟、每小时的活跃用户数、订单总额等指标。
实现:
using System.Collections.Concurrent;
public class SlidingWindowAggregator
{
// 滑动窗口:键为窗口起始时间,值为聚合状态
private readonly ConcurrentDictionary<long, WindowState> _windows = new();
private readonly TimeSpan _windowSize;
private readonly TimeSpan _retention;
public SlidingWindowAggregator(TimeSpan windowSize, TimeSpan retention)
{
_windowSize = windowSize;
_retention = retention;
}
public void Record(long timestamp, string userId, decimal amount)
{
long windowStart = AlignToWindow(timestamp, _windowSize.Ticks);
var state = _windows.GetOrAdd(windowStart, _ => new WindowState());
lock (state)
{
state.Users.Add(userId);
state.TotalAmount += amount;
state.Count++;
}
// 定期清理过期窗口
CleanupOldWindows(timestamp);
}
public WindowStats? GetWindow(long timestamp)
{
long windowStart = AlignToWindow(timestamp, _windowSize.Ticks);
if (_windows.TryGetValue(windowStart, out var state))
{
lock (state)
{
return new WindowStats(
state.Users.Count,
state.TotalAmount,
state.Count);
}
}
return null;
}
private void CleanupOldWindows(long now)
{
var cutoff = now - _retention.Ticks;
foreach (var kv in _windows)
{
if (kv.Key < cutoff)
_windows.TryRemove(kv.Key, out _);
}
}
private static long AlignToWindow(long timestamp, long windowTicks) =>
timestamp / windowTicks * windowTicks;
private sealed class WindowState
{
public HashSet<string> Users { get; } = new();
public decimal TotalAmount { get; set; }
public int Count { get; set; }
}
}
public sealed record WindowStats(int ActiveUsers, decimal TotalAmount, int OrderCount);
关键设计:
ConcurrentDictionary支持多生产者并发写入- 窗口内部加锁保证
HashSet与计数器原子更新 - 定期清理过期窗口避免内存膨胀
- 桶对齐时间戳(除法取整)确保相同窗口事件进入同一桶
扩展:若需多级窗口(1min、5min、1hour),可使用多个聚合器实例,或采用层级降采样(downsampling)模式。
基础题
-
类型推断:给定方法签名
static T Find<T>(IEnumerable<T> source, Func<T, bool> predicate),分析调用Find(new[] { "a", "bb", "ccc" }, s => s.Length > 1)时编译器如何推断T。- 参考答案要点:从
string[]推断T = string,Lambda 参数s : string,返回bool匹配谓词签名。
- 参考答案要点:从
-
集合选择:你需要实现一个频繁插入与删除(首尾均有)、偶尔按索引访问的场景。下列哪个集合最合适?
- A)
List<T>B)LinkedList<T>C)Dictionary<int, T>D)Queue<T> - 参考答案要点:B。
LinkedList<T>提供 首尾增删,索引访问 但偶发可接受。
- A)
-
协变逆变:判断下列哪些赋值合法并说明原因:
IEnumerable<object> = new List<string>()IList<object> = new List<string>()Action<object> = Action<string>(即Action<in T>)Func<string> = Func<object>(即Func<out T>)- 参考答案要点:合法、非法(
IList<T>不变)、非法(逆变方向错误,应为Action<string> = Action<object>)、合法(协变,Func<object>可视为Func<string>)。
进阶题
-
迭代器实现:编写一个泛型方法
Interleave<T>(IEnumerable<T> first, IEnumerable<T> second),交替返回两个序列的元素。当其中一个序列耗尽时,继续返回另一序列剩余元素。- 参考答案要点:使用
yield return,两个枚举器交替推进,处理任一先结束的情况。
- 参考答案要点:使用
-
性能优化:下列代码处理 100 万元素,运行耗时 800ms。分析瓶颈并优化至 50ms 以内。
var dict = new Dictionary<string, int>(); foreach (var s in largeList) // largeList 含 1M 字符串 if (!dict.ContainsKey(s)) dict[s] = 0; else dict[s]++;- 参考答案要点:
ContainsKey+ 索引器导致两次哈希查找,改为CollectionsMarshal.GetValueRefOrAddDefault;预分配容量;考虑并行分批处理。
- 参考答案要点:
-
自定义集合:实现一个
BoundedQueue<T>,固定容量、线程安全、支持阻塞Enqueue与Dequeue(满时阻塞入队、空时阻塞出队)。- 参考答案要点:使用
SemaphoreSlim控制可用槽位,lock保护内部Queue<T>,WaitAsync与Release配合实现阻塞语义。
- 参考答案要点:使用
挑战题
-
类型系统设计:设计一个支持”任意类型到任意类型”映射的
TypeMapper,要求:- 编译期类型安全(不能把
int映射到string后取出DateTime) - 运行时支持反射查询所有注册的映射
- 提示:使用泛型接口 + 内部
Dictionary<Type, Dictionary<Type, object>>存储委托 - 参考答案要点:定义
IMapper<TFrom, TTo>,TypeMapper内部用Dictionary<(Type, Type), Delegate>,注册时Register<TFrom, TTo>(Func<TFrom, TTo>),查询时Map<TFrom, TTo>(TFrom),校验类型匹配。
- 编译期类型安全(不能把
-
持久化数据结构:实现一个简单的持久化向量(Persistent Vector),基于位分片树(bit-mapped trie),支持:
- 随机访问
- 末尾添加(结构共享)
- 不可变更新(返回新版本)
- 提示:参考 Clojure 的 PersistentVector,分支因子 32
- 参考答案要点:32 叉树,深度 ,叶子节点存值,内部节点存子节点引用。更新时复制路径节点,未变子节点共享。
-
零分配迭代器:实现一个
RefEnumerable<T>,实现IEnumerable<T>但内部使用Span<T>与ref局部变量,避免装箱。讨论其与Span<T>.GetEnumerator()的差异。- 参考答案要点:
Span<T>的枚举器是ref struct,无法作为IEnumerable<T>使用。可包装为List<T>.Enumerator风格,但需保证不跨await。或使用ReadOnlySpan<T>+ 自定义结构枚举器,对外暴露IEnumerable<T>时会装箱。
- 参考答案要点:
-
并发哈希表设计:分析
ConcurrentDictionary的分段锁设计,对比Dictionary+ 全局锁的性能。给定 8 核 CPU、1000 万次写入,预测两种方案的吞吐量比例。- 参考答案要点:
ConcurrentDictionary在 .NET 5+ 改为细粒度锁(每个桶独立锁或无锁读取 + CAS 写入),并发度随核数线性扩展。Dictionary+ 锁在 8 核下吞吐量约为ConcurrentDictionary的 1/4 至 1/8(锁竞争导致串行化)。实测可设计 Benchmark 验证。
- 参考答案要点:
官方文档
- .NET 泛型:https://learn.microsoft.com/dotnet/csharp/fundamentals/types/generics
- System.Collections.Generic 命名空间:https://learn.microsoft.com/dotnet/api/system.collections.generic
- System.Collections.Immutable:https://learn.microsoft.com/dotnet/api/system.collections.immutable
- System.Collections.Concurrent:https://learn.microsoft.com/dotnet/api/system.collections.concurrent
- Span<T> 与 Memory<T>:https://learn.microsoft.com/dotnet/api/system.span-1
- C# 12 集合表达式:https://learn.microsoft.com/dotnet/csharp/language-reference/operators/collection-expressions
- FrozenSet 与 FrozenDictionary:https://learn.microsoft.com/dotnet/api/system.collections.frozen
经典教材
- Jeffrey Richter. CLR via C# (4th Edition). Microsoft Press, 2012. Chapter 12: Generics, 289–323.
- Jon Skeet. C# in Depth (4th Edition). Manning Publications, 2019. Chapter 3: Parameterized typing with generics, 58–92.
- Andrew Troelsen, Phil Japikse. Pro C# 10 with .NET 6 (11th Edition). Apress, 2022. Chapter 10: Collections and Generics.
- Joseph Albahari, Ben Albahari. C# 12 in a Nutshell. O’Reilly Media, 2024. Chapter 7: Collections, 281–340.
- Mark Michaelis, Eric Lippert. Essential C# 12.0 (8th Edition). Addison-Wesley, 2024. Chapter 18: Generics, 645–690.
前沿论文与开源项目
- .NET Runtime 源码:https://github.com/dotnet/runtime/tree/main/src/libraries/System.Private.CoreLib/src/System/Collections/Generic
- .NET BCL 集合性能基准:https://github.com/dotnet/performance/tree/main/src/benchmarks/micro/libraries/System.Collections
- Clojure PersistentVector 设计:https://hypirion.com/musings/understanding-persistent-vector-pt-1
- Okasaki 纯函数数据结构:https://www.cs.cmu.edu/~rwh/theses/okasaki.pdf
- .NET 8 Frozen Collections 实现解析:https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-8/#frozen-collections
相关模式与实践
- 仓储模式:Martin Fowler, Patterns of Enterprise Application Architecture, 2002. Chapter 3: Repository, 322–333.
- 规约模式:Eric Evans, Domain-Driven Design, 2003. Chapter 5: Specifications, 224–243.
- LMAX Disruptor:Martin Thompson et al., LMAX Disruptor: High Performance Inter-Thread Messaging Library, 2011. https://lmax-exchange.github.io/disruptor/
- 对象池模式:.NET
ObjectPool<T>实现,https://learn.microsoft.com/aspnet/core/performance/objectpool
实战项目
- BenchmarkDotNet:https://github.com/dotnet/BenchmarkDotNet (集合性能基准测试工具)
- FSharp.Core Collections:https://github.com/dotnet/fsharp (F# 不可变集合参考实现)
- System.Interactive:https://github.com/dotnet/reactive (Ix 库,扩展 LINQ 迭代器)
- LanguageExt:https://github.com/louthy/language-ext (C# 函数式数据结构库)
泛型类与方法
基本写法:泛型类定义
public class <类名><T> { ... }
// 定义类型参数化的泛型类
public class Repository<T>
{
private readonly List<T> _items = [];
}
基本写法:泛型类带约束
public class <类名><T> where T : <约束> { ... }
// 定义带约束的泛型类
public class Repository<T> where T : class
{
public T? FindById(int id) => _items.FirstOrDefault();
}
基本写法:泛型方法
public static <返回类型> <方法名><T>(<参数>)
// 方法定义类型参数
public static void Swap<T>(ref T a, ref T b)
{
(a, b) = (b, a);
}
基本写法:泛型方法带约束
public static <返回类型> <方法名><T>(<参数>) where T : <约束>
// 带约束的泛型方法
public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) >= 0 ? a : b;
}
基本写法:多类型参数泛型方法
public static <返回类型> <方法名><T1, T2>(<参数>)
// 多类型参数的泛型方法
public static TResult Convert<TInput, TResult>(
TInput input, Func<TInput, TResult> converter)
{
return converter(input);
}
泛型约束
基本写法:class 引用类型约束
where T : class
// 限制泛型参数为引用类型
public class Repository<T> where T : class { }
基本写法:struct 值类型约束
where T : struct
// 限制泛型参数为值类型
public class NumberProcessor<T> where T : struct { }
基本写法:new 无参构造约束
where T : new()
// 限制泛型参数有无参公共构造函数
public class Factory<T> where T : new()
{
public T Create() => new T();
}
基本写法:基类约束
where T : <基类>
// 限制泛型参数继承自指定基类
public class AnimalProcessor<T> where T : Animal { }
基本写法:接口约束
where T : <接口>
// 限制泛型参数实现指定接口
public class Comparer<T> where T : IComparable<T> { }
基本写法:unmanaged 约束
where T : unmanaged
// 限制泛型参数为非托管类型
public class NativeBuffer<T> where T : unmanaged { }
单行写法:多约束
where <参数> : <约束1>, <约束2>
// 单行声明多个约束
public class Service<T> where T : class, ICloneable { }
换行写法:多类型参数多约束
where <参数1> : <约束> where <参数2> : <约束>
// 换行声明多类型参数的约束
public class Service<TInput, TOutput>
where TInput : class, ICloneable
where TOutput : new()
{
public TOutput Process(TInput input) => new TOutput();
}
协变与逆变
基本写法:协变接口
interface <接口名><out T> { T <方法>(); }
// 协变接口允许子类型到父类型转换
public interface IProducer<out T>
{
T Produce();
}
基本写法:协变接口转换
<父接口> <变量> = <实现类>;
// 协变允许 string producer 转换为 object producer
IProducer<object> producer = new StringProducer();
基本写法:IEnumerable 协变
IEnumerable<<父类型>> <变量> = <子类型集合>;
// 内置 IEnumerable<out T> 协变
IEnumerable<string> strings = new List<string> { "a", "b" };
IEnumerable<object> objects = strings;
基本写法:逆变接口
interface <接口名><in T> { void <方法>(T <参数>); }
// 逆变接口允许父类型到子类型转换
public interface IConsumer<in T>
{
void Consume(T item);
}
基本写法:逆变接口转换
<子接口> <变量> = <实现类>;
// 逆变允许 object consumer 转换为 string consumer
IConsumer<string> consumer = new ObjectConsumer();
List<T>
基本写法:List 初始化
var <变量> = new List<<类型>> { <元素>, ... };
// 初始化列表
var list = new List<int> { 1, 2, 3, 4, 5 };
基本写法:List 添加元素
<列表>.Add(<元素>);
// 向列表末尾添加元素
list.Add(6);
基本写法:List 批量添加
<列表>.AddRange(<集合>);
// 批量添加元素到列表
list.AddRange([7, 8, 9]);
基本写法:List 指定位置插入
<列表>.Insert(<索引>, <元素>);
// 在指定索引位置插入元素
list.Insert(0, 0);
基本写法:List 索引访问
<类型> <变量> = <列表>[<索引>];
// 通过索引访问列表元素
int first = list[0];
基本写法:List 末尾访问
<类型> <变量> = <列表>[^1];
// 从末尾访问列表元素
int last = list[^1];
基本写法:List Contains 检查
bool <结果> = <列表>.Contains(<元素>);
// 检查列表是否包含指定元素
bool has = list.Contains(3);
基本写法:List IndexOf 定位
int <结果> = <列表>.IndexOf(<元素>);
// 获取元素首次出现的索引
int index = list.IndexOf(3);
基本写法:List Find 查找
<类型> <结果> = <列表>.Find(<谓词>);
// 查找第一个匹配条件的元素
int found = list.Find(x => x > 5);
基本写法:List FindAll 查找全部
List<<类型>> <结果> = <列表>.FindAll(<谓词>);
// 查找所有匹配条件的元素
List<int> allFound = list.FindAll(x => x > 3);
基本写法:List Remove 删除
<列表>.Remove(<元素>);
// 删除第一个匹配元素
list.Remove(3);
基本写法:List RemoveAt 索引删除
<列表>.RemoveAt(<索引>);
// 删除指定索引的元素
list.RemoveAt(0);
基本写法:List RemoveAll 条件删除
<列表>.RemoveAll(<谓词>);
// 删除所有匹配条件的元素
list.RemoveAll(x => x > 5);
基本写法:List 默认排序
<列表>.Sort();
// 使用默认比较器排序
list.Sort();
基本写法:List 自定义排序
<列表>.Sort(<比较器>);
// 使用自定义比较器降序排序
list.Sort((a, b) => b.CompareTo(a));
基本写法:List ForEach 遍历
<列表>.ForEach(<动作>);
// 对每个元素执行指定动作
list.ForEach(item => Console.WriteLine(item));
Dictionary<TKey, TValue>
基本写法:Dictionary 初始化
var <变量> = new Dictionary<<TKey>, <TValue>> { [<键>] = <值>, ... };
// 初始化字典
var dict = new Dictionary<string, int>
{
["apple"] = 5,
["banana"] = 3
};
基本写法:Dictionary Add 添加
<字典>.Add(<键>, <值>);
// 添加键值对(键不存在时)
dict.Add("grape", 4);
基本写法:Dictionary 索引赋值
<字典>[<键>] = <值>;
// 添加或更新键值对
dict["mango"] = 6;
基本写法:Dictionary TryAdd 尝试添加
bool <结果> = <字典>.TryAdd(<键>, <值>);
// 尝试添加键值对
dict.TryAdd("pear", 2);
基本写法:Dictionary 索引访问
<值类型> <变量> = <字典>[<键>];
// 通过键获取值(键不存在抛异常)
int count = dict["apple"];
基本写法:Dictionary TryGetValue 安全访问
bool <结果> = <字典>.TryGetValue(<键>, out <输出变量>);
// 安全获取值,返回是否成功
if (dict.TryGetValue("banana", out int value))
{
Console.WriteLine($"banana: {value}");
}
基本写法:Dictionary 默认值访问
<值类型> <变量> = <字典>.GetValueOrDefault(<键>, <默认值>);
// 获取值或默认值
int safe = dict.GetValueOrDefault("kiwi", 0);
基本写法:Dictionary 遍历
foreach (var (<键>, <值>) in <字典>)
// 遍历字典的键值对
foreach (var (key, val) in dict)
{
Console.WriteLine($"{key}: {val}");
}
基本写法:Dictionary ContainsKey 检查
bool <结果> = <字典>.ContainsKey(<键>);
// 检查字典是否包含指定键
bool hasKey = dict.ContainsKey("apple");
基本写法:Dictionary Remove 删除
<字典>.Remove(<键>);
// 删除指定键的键值对
dict.Remove("apple");
HashSet<T> 与其他集合
基本写法:HashSet 交集
<集合1>.IntersectWith(<集合2>);
// 计算两个集合的交集
var set1 = new HashSet<int> { 1, 2, 3, 4, 5 };
var set2 = new HashSet<int> { 4, 5, 6, 7, 8 };
set1.IntersectWith(set2);
基本写法:HashSet 并集
<集合1>.UnionWith(<集合2>);
// 计算两个集合的并集
set1.UnionWith(set2);
基本写法:HashSet 差集
<集合1>.ExceptWith(<集合2>);
// 计算两个集合的差集
set1.ExceptWith(set2);
基本写法:Queue 入队
<队列>.Enqueue(<元素>);
// 向队列末尾添加元素
var queue = new Queue<string>();
queue.Enqueue("第一个");
基本写法:Queue 出队
<类型> <变量> = <队列>.Dequeue();
// 从队列头部移除并返回元素
string dequeued = queue.Dequeue();
基本写法:Stack 压栈
<栈>.Push(<元素>);
// 向栈顶压入元素
var stack = new Stack<int>();
stack.Push(1);
基本写法:Stack 出栈
<类型> <变量> = <栈>.Pop();
// 从栈顶弹出元素
int popped = stack.Pop();
基本写法:PriorityQueue 入队
<队列>.Enqueue(<元素>, <优先级>);
// 按优先级入队
var pq = new PriorityQueue<string, int>();
pq.Enqueue("高优先级", 1);
基本写法:PriorityQueue 出队
<类型> <变量> = <队列>.Dequeue();
// 取出优先级最高的元素
string highest = pq.Dequeue();
基本写法:LinkedList 尾部添加
<链表>.AddLast(<元素>);
// 在链表末尾添加节点
var linked = new LinkedList<int>();
linked.AddLast(1);
基本写法:LinkedList 头部添加
<链表>.AddFirst(<元素>);
// 在链表头部添加节点
linked.AddFirst(0);
不可变集合
基本写法:ImmutableList 创建
var <变量> = ImmutableList.Create(<元素>, ...);
// 创建不可变列表
var list = ImmutableList.Create(1, 2, 3);
基本写法:ImmutableList 添加
var <新列表> = <列表>.Add(<元素>);
// 添加元素返回新列表,原列表不变
var newList = list.Add(4);
基本写法:Frozen 字典创建
var <变量> = <字典>.ToFrozenDictionary();
// 创建后不可修改,查询性能极快
var frozen = new Dictionary<string, int>
{
["a"] = 1, ["b"] = 2
}.ToFrozenDictionary();
LINQ to Objects
基本写法:Where 筛选
var <结果> = <集合>.Where(<谓词>);
// 筛选满足条件的元素
var expensive = products.Where(p => p.Price > 1000);
基本写法:Select 投影
var <结果> = <集合>.Select(<选择器>);
// 将元素投影为新形式
var names = products.Select(p => p.Name);
基本写法:Select 匿名类型投影
var <结果> = <集合>.Select(<选择器>);
// 投影为匿名类型
var projections = products.Select(p => new { p.Name, p.Price });
基本写法:OrderBy 排序
var <结果> = <集合>.OrderBy(<键选择器>);
// 按指定键升序排序
var sorted = products.OrderBy(p => p.Price);
基本写法:OrderByDescending 降序排序
var <结果> = <集合>.OrderByDescending(<键选择器>);
// 按指定键降序排序
var sortedDesc = products.OrderByDescending(p => p.Price);
基本写法:ThenBy 多级排序
var <结果> = <集合>.OrderBy(<键1>).ThenBy(<键2>);
// 多级排序
var multiSort = products.OrderBy(p => p.Category).ThenBy(p => p.Price);
基本写法:GroupBy 分组
var <结果> = <集合>.GroupBy(<键选择器>);
// 按指定键分组
var grouped = products.GroupBy(p => p.Category);
基本写法:Sum 求和
<类型> <结果> = <集合>.Sum(<选择器>);
// 计算指定属性的总和
decimal total = products.Sum(p => p.Price);
基本写法:Average 平均值
<类型> <结果> = <集合>.Average(<选择器>);
// 计算指定属性的平均值
decimal avg = products.Average(p => p.Price);
基本写法:Count 条件计数
int <结果> = <集合>.Count(<谓词>);
// 统计满足条件的元素数量
int count = products.Count(p => p.Price > 500);
基本写法:Skip Take 分页
var <结果> = <集合>.Skip(<数量>).Take(<数量>);
// 跳过指定数量后取指定数量
var page = products.OrderBy(p => p.Price).Skip(10).Take(5);
基本写法:Distinct 去重
var <结果> = <集合>.Select(<选择器>).Distinct();
// 对指定属性去重
var categories = products.Select(p => p.Category).Distinct();
基本写法:Intersect 交集
var <结果> = <集合1>.Intersect(<集合2>);
// 计算两个集合的交集
var intersect = allNames.Intersect(someNames);
基本写法:Union 并集
var <结果> = <集合1>.Union(<集合2>);
// 计算两个集合的并集
var union = allNames.Union(someNames);
基本写法:Except 差集
var <结果> = <集合1>.Except(<集合2>);
// 计算两个集合的差集
var except = allNames.Except(someNames);
单行写法:查询语法
from <变量> in <集合> where <条件> select <结果>
// 单行 SQL 风格查询表达式
var query = from p in products where p.Price > 500 select p;
换行写法:查询语法
from <变量> in <集合> where <条件> orderby <排序> select <结果>
// 换行 SQL 风格查询表达式
var query = from p in products
where p.Price > 500
orderby p.Price descending
select new { p.Name, p.Price };
基本写法:Join 连接查询
from <变量1> in <集合1> join <变量2> in <集合2> on <键1> equals <键2> select <结果>
// 连接两个集合查询
var result = from p in products
join o in orders on p.Name equals o.ProductName
select new { p.Name, o.Quantity };
迭代器 (yield)
基本写法:yield return 惰性生成
yield return <值>;
// 惰性生成斐波那契数列
public static IEnumerable<int> Fibonacci(int count)
{
int a = 0, b = 1;
for (int i = 0; i < count; i++)
{
yield return a;
(a, b) = (b, a + b);
}
}
基本写法:yield break 提前退出
yield break;
// 遇到负数时停止生成
public static IEnumerable<int> GetPositive(int[] numbers)
{
foreach (var n in numbers)
{
if (n < 0) yield break;
if (n > 0) yield return n;
}
}
基本写法:惰性读取文件
yield return <行>;
// 逐行惰性读取文件
public static IEnumerable<string> ReadLinesLazy(string path)
{
using var reader = new StreamReader(path);
while (reader.ReadLine() is string line)
{
yield return line;
}
}
集合表达式 (C# 12)
基本写法:数组集合表达式
<类型>[] <变量> = [<元素>, ...];
// 使用集合表达式初始化数组
int[] array = [1, 2, 3];
基本写法:List 集合表达式
List<<类型>> <变量> = [<元素>, ...];
// 使用集合表达式初始化列表
List<string> list = ["a", "b", "c"];
基本写法:Span 集合表达式
Span<<类型>> <变量> = [<元素>, ...];
// 使用集合表达式初始化 Span
Span<int> span = [1, 2, 3];
基本写法:HashSet 集合表达式
HashSet<<类型>> <变量> = [<元素>, ...];
// 使用集合表达式初始化 HashSet
HashSet<string> set = ["x", "y", "z"];
基本写法:展开运算符合并
<类型>[] <变量> = [..<集合1>, ..<集合2>, <元素>];
// 使用展开运算符合并多个集合
int[] a = [1, 2, 3];
int[] b = [4, 5, 6];
int[] combined = [..a, ..b, 7, 8];