C# 试题 26:有序、并发与不可变集合
0501 以下代码输出什么?
难度: 实战
var dict = new SortedDictionary<int, string>();
dict[3] = "c";
dict[1] = "a";
dict[2] = "b";
foreach (var kv in dict) Console.Write(kv.Key);
- A.
321,后进先出 - B.
213,按哈希序 - C.
123,SortedDictionary 按键升序枚举,与插入顺序无关 - D.
312,按插入顺序
查看答案与解析
正确答案C
解析: SortedDictionary 底层是红黑树,枚举始终按比较器(默认 Comparer<TKey>.Default 升序)输出键,插入顺序完全无关。想按插入顺序应使用普通 Dictionary(实现上通常有序但不保证)。
关键点: “Sorted” 是键序,不是插入序。
0502 以下代码输出什么?
难度: 实战
var dict = new SortedDictionary<int, string>(Comparer<int>.Create((x, y) => y.CompareTo(x)));
dict[1] = "a";
dict[3] = "c";
dict[2] = "b";
foreach (var k in dict.Keys) Console.Write(k);
- A.
123 - B.
321,自定义比较器反转后按键降序枚举 - C.
312 - D. 抛
ArgumentException,比较器不可逆
查看答案与解析
正确答案B
解析: 比较器 y.CompareTo(x) 把顺序颠倒,树按降序组织,枚举输出 3,2,1。自定义比较器必须满足全序(反对称、传递),否则树结构会错乱。
关键点: SortedDictionary 的“顺序”完全由比较器定义。
0503 面对以下两个场景,哪项选型正确?
难度: 进阶
- A. 频繁插入删除且需保持有序选 SortedDictionary(增删 O(log n));批量构建后主要做查找可选 SortedList(数组存储、缓存友好、内存紧凑)
- B. 两个场景都选 SortedList,插入永远是 O(1)
- C. 两个场景都选 SortedDictionary,查找永远 O(1)
- D. 两者行为完全一致,选哪个无所谓
查看答案与解析
正确答案A
解析: SortedDictionary 用树,插入/删除 O(log n) 适合动态维护;SortedList 用连续数组,插入 O(n)(移位)但占用内存小、缓存命中好,适合先排序后只读/二分查找。混淆二者是常见选型错误。
关键点: SortedList 插入是 O(n),不是“List 就快”。
0504 以下代码输出什么?
难度: 实战
var dict = new ConcurrentDictionary<string, int>();
Console.WriteLine(dict.TryAdd("a", 1));
Console.WriteLine(dict.TryAdd("a", 2));
Console.WriteLine(dict["a"]);
- A.
True/True/2 - B.
True/False/2 - C.
False/False/1 - D.
True/False/1
查看答案与解析
正确答案D
解析: TryAdd 在键不存在时添加并返回 true,键已存在时不做任何事返回 false。第二次调用被拒绝,字典里仍是 1。
关键点: TryAdd 是“仅新增”,与索引器的“有则覆盖”语义不同。
0505 以下代码输出什么?
难度: 实战
var dict = new ConcurrentDictionary<string, int>();
dict["a"] = 1;
dict["a"] = 2;
Console.WriteLine(dict["a"]);
- A. 输出
1,索引器只在键不存在时写入 - B. 抛异常,索引器不能覆盖已有键
- C. 输出
2,索引器等价于 AddOrUpdate,存在即覆盖 - D. 输出
0
查看答案与解析
正确答案C
解析: ConcurrentDictionary 的索引器 setter 与普通 Dictionary 相同:键不存在时新增,存在时覆盖。这与 TryAdd 的语义形成对比,也是并发场景最常用的“原子 upsert”。
关键点: 索引器 set = AddOrUpdate;TryAdd = Add only。
0506 以下代码中 calls 的值最有可能是多少?
难度: 进阶
var dict = new ConcurrentDictionary<string, int>();
int calls = 0;
Parallel.For(0, 100, _ =>
dict.GetOrAdd("k", _ => Interlocked.Increment(ref calls)));
Console.WriteLine(calls);
- A. 一定是 1
- B. 一定是 100
- C. 一定是 0
- D. 可能大于 1,GetOrAdd 不保证 valueFactory 只执行一次
查看答案与解析
正确答案D
解析: GetOrAdd 的 valueFactory 在锁外执行:多个线程可能同时发现键缺失并各自调用工厂,最终只有一个结果被采纳。因此 calls 往往大于 1,且工厂不能有“只执行一次”的副作用假设。
关键点: 需要一次性初始化请用 Lazy<T> 或先 TryAdd 再处理。
0507 以下代码输出什么?
难度: 进阶
var dict = new ConcurrentDictionary<int, int>();
for (int i = 0; i < 5; i++) dict[i] = i;
foreach (var kv in dict)
{
dict[kv.Key + 100] = kv.Value;
}
Console.WriteLine(dict.Count);
- A. 抛
InvalidOperationException,枚举期间不能修改 - B. 输出
10,并发字典枚举基于快照,遍历中增删不抛异常 - C. 输出
5 - D. 死循环
查看答案与解析
正确答案B
解析: ConcurrentDictionary 的枚举器遍历的是快照,遍历期间其他线程的增删(包括本线程在循环体内写入)不会触发版本检查,也不抛异常。循环给 5 个键各加一个新键,最终 Count 为 10。
关键点: 并发字典“快照枚举”与 Dictionary 的“版本检查”行为不同。
0508 以下代码输出什么?
难度: 实战
using System.Collections.Immutable;
ImmutableArray<int> a = default;
Console.WriteLine(a.IsDefault);
try { Console.WriteLine(a.Length); }
catch (NullReferenceException) { Console.WriteLine("NRE"); }
- A.
False/0 - B.
True/0 - C.
False/NRE - D.
True/NRE,未初始化的 ImmutableArray 访问 Length 抛 NullReferenceException
查看答案与解析
正确答案D
解析: default(ImmutableArray<T>) 的底层数组为 null,IsDefault 为 true;访问 Length、索引器等成员时直接抛 NullReferenceException。使用前必须先判 IsDefault/IsDefaultOrEmpty。
关键点: 默认构造的 ImmutableArray 不是空数组,是“未初始化”。
0509 以下代码输出什么?
难度: 进阶
using System.Collections.Immutable;
int[] arr = { 1, 2, 3 };
var ia = ImmutableArray.Create(arr);
arr[0] = 99;
Console.WriteLine(ia[0]);
- A.
99,ImmutableArray.Create(T[]) 直接包装数组而不复制 - B.
1,创建时已复制 - C. 抛
InvalidOperationException,不可变数组被修改 - D.
0
查看答案与解析
正确答案A
解析: ImmutableArray.Create(T[]) 复用传入数组作为底层存储,不做防御性复制,调用方随后修改数组会反映到“不可变”实例中。要隔离必须自行复制后传入。
关键点: 不可变的是“之后不允许修改”,不保证隔离外部输入。
0510 以下代码输出什么?
难度: 实战
using System.Collections.Immutable;
var builder = ImmutableArray.CreateBuilder<int>();
builder.Add(1);
builder.Add(2);
var a = builder.ToImmutable();
builder.Add(3);
Console.WriteLine(a.Length);
- A.
3 - B.
0 - C.
2,ToImmutable 返回当前内容的不可变快照,之后改动 builder 不影响 - D. 抛异常
查看答案与解析
正确答案C
解析: Builder.ToImmutable() 返回当前内容的不可变快照(内部会复制),此后继续向 builder 添加元素不影响已生成的数组,a.Length 仍是 2。反之,用 builder.MoveToImmutable() 则转移底层数组并清空 builder。
关键点: ToImmutable 是快照,MoveToImmutable 是转移,语义不同。
0511 以下代码输出什么?
难度: 实战
using System.Collections.Frozen;
var frozen = new Dictionary<int, string> { [1] = "a", [2] = "b" }.ToFrozenDictionary();
Console.WriteLine(frozen[1]);
Console.WriteLine(frozen.ContainsKey(3));
- A.
a/True - B.
a/False - C.
1/False - D. 抛异常,FrozenDictionary 不能读取
查看答案与解析
正确答案B
解析: ToFrozenDictionary 一次性构建针对只读高频查找优化的存储,索引器与 ContainsKey 正常工作;键 3 不存在,ContainsKey 返回 false。Frozen 只约束“不可修改”,不改变读取语义。
关键点: 冻结的是可写性,查找行为与普通字典一致。
0512 关于 FrozenDictionary、ImmutableDictionary 与普通 Dictionary 的选型,哪项正确?
难度: 进阶
- A. FrozenDictionary 支持 Add/Remove,与 ImmutableDictionary 完全等价
- B. FrozenDictionary 适合频繁增删的写多读少场景
- C. ImmutableDictionary 的每次修改都原地更新
- D. FrozenDictionary 适合“启动后固定不变、高频只读”场景,查找更快;ImmutableDictionary 适合需要持久化更新(Add 返回新实例)的场景
查看答案与解析
正确答案D
解析: FrozenDictionary 构建后不可变,专门为只读热路径做哈希布局优化(默认实现可能用完美哈希或更紧凑的桶);ImmutableDictionary 支持 O(log n) 的持久化更新,每次修改返回新实例、旧实例不受影响。二者用途不同。
关键点: “Frozen = 构建一次只读”与“Immutable = 可持久更新”别混为一谈。
0513 以下代码输出什么?
难度: 实战
using System.Collections.Concurrent;
var bc = new BlockingCollection<int>();
bc.Add(1);
bc.CompleteAdding();
Console.WriteLine(bc.Take());
Console.WriteLine(bc.IsAddingCompleted);
try { bc.Take(); }
catch (InvalidOperationException) { Console.WriteLine("empty"); }
- A.
1/True/empty,CompleteAdding 后元素取完,再 Take 抛 InvalidOperationException - B.
1/False/empty - C. 抛异常,CompleteAdding 后不能 Take
- D.
1/True/0
查看答案与解析
正确答案A
解析: CompleteAdding 只标记“不再添加”,已入队元素仍可正常 Take;等队列清空后再 Take 会抛 InvalidOperationException(表示消费方应停止),而不是返回 0 或阻塞。
关键点: 空 + 已完成的 Take 是异常信号,不是默认值。
0514 以下代码输出什么?
难度: 实战
using System.Collections.Concurrent;
var bc = new BlockingCollection<int>(boundedCapacity: 1);
bc.Add(1);
Console.WriteLine(bc.TryAdd(2, 100));
Console.WriteLine(bc.Count);
- A.
True/2 - B.
False/2 - C.
False/1,容量已满时 TryAdd 等待 100ms 后返回 false - D.
True/1
查看答案与解析
正确答案C
解析: 有界容量为 1 且已满时,TryAdd(item, 100) 阻塞等待最多 100ms,期间没有消费者取走元素,超时返回 false,集合仍只有 1 个元素。Add 则会在满时无限阻塞。
关键点: 有界集合的“Try”变体是带超时的阻塞,不是立即失败。
0515 在异步生产者/消费者场景中使用 BlockingCollection,哪项正确?
难度: 进阶
- A. BlockingCollection 提供原生 TakeAsync 方法,应直接使用
- B. BlockingCollection 没有 TakeAsync;其 Add/Take 会阻塞线程,现代异步代码优先考虑 Channel
- C. TakeAsync 返回 ValueTask,永远不会占用线程
- D. BlockingCollection 阻塞调用不占用任何资源,可随意并发
查看答案与解析
正确答案B
解析: BlockingCollection<T> 只有同步阻塞 API,并不存在 TakeAsync。阻塞的 Add/Take 会挂起线程池线程,异步高并发场景应改用 Channel<T>(WriteAsync/ReadAsync)来避免线程阻塞。
关键点: “有 Async 方法”的假设要先查 API 再信。
0516 以下代码输出什么?
难度: 进阶
using System.Collections.Concurrent;
var bc = new BlockingCollection<int> { 1, 2, 3 };
bc.CompleteAdding();
Console.WriteLine(bc.GetConsumingEnumerable().Count());
Console.WriteLine(bc.GetConsumingEnumerable().Count());
- A.
3/3 - B.
3/ 抛异常 - C.
0/0 - D.
3/0,GetConsumingEnumerable 会移出元素,第二次枚举为空
查看答案与解析
正确答案D
解析: GetConsumingEnumerable 与普通只读枚举不同:每产出一个元素就把它从集合中移除。第一次 Count() 消费掉全部 3 个元素,第二次枚举时集合已空,结果为 0。
关键点: 消费型枚举只能消费一次,别把结果缓存后再复用。
0517 关于 Channel<T> 与 BlockingCollection<T>,哪项正确?
难度: 进阶
- A. Channel 提供无阻塞/异步 API(WriteAsync、ReadAsync),适合高吞吐异步场景;BlockingCollection 以阻塞 API 为主
- B. Channel 只能有界,BlockingCollection 只能无界
- C. 两者继承同一抽象,可以互换
- D. Channel 是旧 API,官方建议用 BlockingCollection 替代
查看答案与解析
正确答案A
解析: Channel<T> 是无锁、面向 async/await 的生产者消费者管道,WriteAsync/ReadAsync 不占用线程等待;BlockingCollection 基于同步阻塞原语。官方文档明确建议新异步代码优先使用 Channel。
关键点: 同步阻塞选 BlockingCollection,异步流式选 Channel。
0518 以下代码运行结果是什么?
难度: 实战
class K { public int V; }
var dict = new SortedDictionary<K, int>();
dict.Add(new K { V = 1 }, 1);
dict.Add(new K { V = 2 }, 2);
- A. 编译失败,K 必须实现 IComparable
- B. 第一次 Add 抛
InvalidOperationException - C. 第二次 Add 抛
InvalidOperationException:K 未实现 IComparable/IComparable<K> 且未提供比较器,首次比较时失败 - D. 正常运行,K 按引用比较
查看答案与解析
正确答案C
解析: 插入第一个键时树为空、无需比较,能成功;插入第二个键需要与已有键比较,而 K 既不实现 IComparable 也没有传入比较器,Comparer<K>.Default 在首次 Compare 时抛 InvalidOperationException。
关键点: SortedDictionary 要求键可比——要么实现 IComparable,要么传入比较器。
0519 以下代码输出什么?
难度: 进阶
using System.Collections.Immutable;
ImmutableArray<int> a = ImmutableArray.Create(1, 2);
ImmutableArray<int> b = a;
Console.WriteLine(a == b);
Console.WriteLine(a.IsDefault);
- A.
False/False,struct 复制后互不相干 - B.
True/False,复制的是包装结构,底层数组共享,== 按底层数组引用比较 - C.
True/True - D. 编译失败,struct 不能赋值
查看答案与解析
正确答案B
解析: ImmutableArray<T> 是包着一维数组的 struct,赋值 b = a 只复制包装(引用同一数组)。== 运算符比较的是底层数组引用,因此为 true;IsDefault 仍为 false。
关键点: struct 复制共享引用,比较运算符比的是内部数组。
0520 以下代码输出什么?
难度: 实战
var dict = new ConcurrentDictionary<string, int>();
dict.AddOrUpdate("a", 1, (key, old) => old + 10);
dict.AddOrUpdate("a", 1, (key, old) => old + 10);
Console.WriteLine(dict["a"]);
- A.
1,更新工厂从不执行 - B.
10 - C.
20 - D.
11,第一次键不存在走 addValue=1,第二次存在走 updateFactory:1+10
查看答案与解析
正确答案D
解析: AddOrUpdate(key, addValue, updateFactory):键不存在时添加 addValue(1),键已存在时用工厂基于旧值计算(1+10=11)。注意参数顺序是“新增值在前、更新工厂在后”,写反是高频错误。
关键点: AddOrUpdate 的两个分支参数顺序别记反。