返回题库高级 .NET 刷题C# 选择题 · 第 25 / 50 篇

C# 试题 25:Dictionary、HashSet 与相等性

0481 关于 Dictionary<TKey, TValue> 的查找复杂度,哪项正确?

难度: 进阶

  • A. 平均接近 O(1),最坏情况受哈希冲突与扩容影响,可能退化为 O(n)
  • B. 无论比较器实现和键分布如何,查找都严格 O(1)
  • C. 平均 O(log n)
  • D. 最坏情况也是 O(1),扩容后自动重哈希保证
查看答案与解析

正确答案A

解析: Dictionary 用哈希桶定位,平均情况下每个桶元素很少,接近 O(1);一旦大量键哈希碰撞(如劣质比较器或恶意输入),单桶链表变长,最坏退化为 O(n)。

关键点: “严格 O(1)”是常见误解,最坏情况由哈希质量决定。

0482 以下代码输出什么?

难度: 实战

class Key
{
    public int A;
    public int B;
    public override bool Equals(object? o) => o is Key k && A == k.A && B == k.B;
    public override int GetHashCode() => HashCode.Combine(A, B);
}

var key = new Key { A = 1, B = 2 };
var dict = new Dictionary<Key, string> { [key] = "v" };
key.B = 3;

Console.WriteLine(dict.ContainsKey(new Key { A = 1, B = 3 }));
  • A. True,键值相等即可命中
  • B. False,键的哈希码随 B 改变而变,条目存放在旧哈希对应的桶中,新哈希查不到
  • C. True,字典会在查找时重新计算并迁移条目
  • D. False,因为传入的是不同对象引用(即使不修改 B 也找不到)
查看答案与解析

正确答案B

解析: 入表后键的 B 从 2 改为 3,GetHashCode 也随之变化;条目仍留在按旧哈希定位的桶里,用新哈希查找必然落空。这与“值相等但引用不同”无关——比较器本身是值语义。

关键点: 键入表后不应改变参与相等与哈希的状态。

0483 以下代码输出什么?

难度: 实战

var dict = new Dictionary<string, int> { ["a"] = 1 };

Console.WriteLine(dict.TryGetValue("b", out int v));
Console.WriteLine(v);
  • A. True / 0
  • B. False / 抛异常
  • C. True / 1
  • D. False / 0,查找失败时 out 参数被赋 default
查看答案与解析

正确答案D

解析: TryGetValue 未命中时返回 false,并把 out 参数赋为 default(int)0,不会抛异常。注意 v 的初值无意义,必须读返回值后再使用。

关键点: TryGetValue 的 out 参数在失败时是 default,不是上一次的值。

0484 以下代码运行结果是什么?

难度: 实战

var dict = new Dictionary<string, int> { ["a"] = 1 };

Console.WriteLine(dict["b"]);
  • A. 输出 0
  • B. 输出 null
  • C. 运行抛 KeyNotFoundException
  • D. 输出 1
查看答案与解析

正确答案C

解析: 索引器 dict["b"] 对缺失键抛 KeyNotFoundException;只有 TryGetValueGetValueOrDefault 才提供不抛异常的读取路径。

关键点: 索引器是“假设存在”的契约,读取前先判断或改用 TryGetValue。

0485 以下代码输出什么?

难度: 进阶

class K
{
    public int Id;
    public override bool Equals(object? o) => true;
    public override int GetHashCode() => Id;
}

var set = new HashSet<K>();
set.Add(new K { Id = 1 });

Console.WriteLine(set.Contains(new K { Id = 2 }));
  • A. True,Equals 恒为 true
  • B. False,哈希码决定桶位置,Id=2 与 Id=1 落入不同桶,永远不会走到 Equals
  • C. True,HashSet 会线性扫描全部元素
  • D. False,因为 K 没有实现 IEquatable<K>
查看答案与解析

正确答案B

解析: HashSet 先用 GetHashCode 定位桶,只在同桶内用 Equals 精比。Id=1Id=2 哈希不同、落入不同桶,Contains 直接返回 false,即便 Equals 恒真也不会被调用。

关键点: Equals 相等必须保证 GetHashCode 相等,否则元素“永远找不到”。

0486 以下代码输出什么?

难度: 进阶

var dict = new Dictionary<double, string> { [double.NaN] = "x" };

Console.WriteLine(dict[double.NaN]);
  • A. 输出 x
  • B. 输出空字符串
  • C. 抛 KeyNotFoundException,NaN 不等于自身
  • D. 编译失败
查看答案与解析

正确答案A

解析: double.Equals(NaN, NaN) 返回 true(IEEE 754 的 NaN != NaN 只适用于比较运算符),EqualityComparer<double>.Default 按 Equals 语义工作且 NaN 的哈希稳定,因此 NaN 作为键可正常命中。

关键点: “NaN 不等于自身”对 == 成立,对 Equals/字典查找不成立。

0487 以下代码输出什么?

难度: 实战

struct Point
{
    public int X;
    public int Y;
}

var dict = new Dictionary<Point, string>();
dict[new Point { X = 1, Y = 2 }] = "v";

Console.WriteLine(dict.ContainsKey(new Point { X = 1, Y = 2 }));
  • A. False,两个 struct 实例引用不同
  • B. False,struct 默认使用引用相等
  • C. True,未重写时 struct 按字段逐项比较且哈希一致
  • D. 运行抛异常
查看答案与解析

正确答案C

解析: 未重写 Equals/GetHashCode 的 struct 由 ValueType 提供字段级值相等,两个成员相同的实例相等且哈希相同,因此可以命中。注意这条路径有反射开销,热点代码应显式实现 IEquatable<T>

关键点: struct 默认是值相等,与 class 的引用相等相反。

0488 以下代码输出什么?

难度: 实战

var ci = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase) { ["Key"] = 1 };
var cs = new Dictionary<string, int> { ["Key"] = 1 };

Console.WriteLine(ci["key"]);
Console.WriteLine(cs.ContainsKey("key"));
  • A. 1 / True
  • B. 0 / False
  • C. 1 / 1
  • D. 1 / False,默认字符串键比较是区分大小写的序号比较
查看答案与解析

正确答案D

解析: StringComparer.OrdinalIgnoreCase"Key""key" 视为同一键;而 Dictionary 的默认比较器是 EqualityComparer<string>.Default,按序号且区分大小写,ContainsKey("key") 为 false。

关键点: 字符串默认相等是 ordinal(区分大小写),需要忽略大小写必须显式传入比较器。

0489 关于 string.GetHashCode() 的稳定性,哪项正确?

难度: 进阶

  • A. 跨进程、跨运行时完全稳定,可用于持久化存储键
  • B. 每次调用都返回随机值
  • C. 同一进程内同一字符串多次调用结果一致;但 .NET Core 起字符串哈希按进程随机化,跨进程可能不同,不能用于持久化
  • D. 结果受当前区域文化影响
查看答案与解析

正确答案C

解析: .NET Core 对 string.GetHashCode() 使用进程级随机种子,防御哈希碰撞攻击。同一进程内结果稳定,但不同进程可能不同,因此绝不能把该值写入数据库或用于跨进程协议。

关键点: 哈希码是进程内寻址用的,不是稳定标识符。

0490 以下代码输出什么?

难度: 实战

var dict = new Dictionary<string, int>();
dict[null!] = 1;

Console.WriteLine(dict.Count);
  • A. 0,null 不是合法键
  • B. 1,引用类型键允许 null
  • C. 抛 ArgumentNullException
  • D. 编译失败
查看答案与解析

正确答案B

解析: 对引用类型键,Dictionary 显式允许 nulldict[null!] = 1 正常新增一个条目。只有值类型键才不可能为 null。

关键点: null 键合法,别把“不能为 null”当成通用规则。

0491 以下代码输出什么?

难度: 实战

var dict = new Dictionary<string, int>();

Console.WriteLine(dict.GetValueOrDefault("x"));
Console.WriteLine(dict.GetValueOrDefault("x", 42));
  • A. 0 / 42
  • B. 0 / 0
  • C. 抛 KeyNotFoundException
  • D. null / 42
查看答案与解析

正确答案A

解析: GetValueOrDefault 是 .NET Core 2.0+ 提供的便捷方法:无重载版本返回 default(TValue),带默认值重载返回指定默认值,都不抛异常。注意返回值类型是 int,不存在 null

关键点: 读取缺失键有三条路径:索引器抛异常、TryGetValue 返回 bool、GetValueOrDefault 返回默认值。

0492 以下代码运行结果是什么?

难度: 实战

var dict = new Dictionary<string, int> { ["a"] = 1, ["b"] = 2 };

foreach (var k in dict.Keys)
{
    dict.Add("c", 3);
}
  • A. 正常结束,Count 变为 3
  • B. 只添加一次后正常结束
  • C. 无限循环
  • D. 第一次 Add 时抛 InvalidOperationException
查看答案与解析

正确答案D

解析: Dictionary 的枚举器通过版本号检测结构修改,遍历期间调用 Add/Remove 会使版本号变化,下一次 MoveNextInvalidOperationException。先收集键再批量修改是安全做法。

关键点: 枚举期间任何结构性修改都会让迭代失效。

0493 以下代码输出什么?

难度: 进阶

var dict = new Dictionary<string, int>();

ref int v = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, "a", out bool exists);
v = 42;

Console.WriteLine($"{exists} {dict["a"]}");
  • A. True 42
  • B. False 0
  • C. False 42
  • D. True 0
查看答案与解析

正确答案C

解析:"a" 不存在时 GetValueRefOrAddDefault 会以默认值插入条目,out existsfalse,并返回指向该值的 ref;通过 ref 写入 42 直接落进字典,等价于一次查找完成“取或加 + 赋值”。

关键点: 该 API 避免 ContainsKey + 索引器的两次查找,但只应作用于引用类型值或立即使用的场景。

0494 以下代码输出什么?

难度: 进阶

var dict = new Dictionary<string, int>();

ref int v = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, "k", out bool first);
v = 7;
ref int w = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, "k", out bool second);

Console.WriteLine($"{first} {second} {w}");
  • A. False False 7
  • B. False True 7
  • C. True True 7
  • D. False True 0
查看答案与解析

正确答案B

解析: 第一次调用键不存在,first=false 并插入;第二次调用键已存在,second=true,返回的 ref 指向同一个 7,所以 w7

关键点: out 参数表示“本次调用是否新增”,读取已有值时拿到的是现存值。

0495 以下代码结果是什么?

难度: 实战

struct S { public int X; }

var dict = new Dictionary<string, S>();
dict["a"] = new S { X = 1 };
dict["a"].X = 2;
  • A. 编译失败(CS1612):索引器返回值是副本,不能就地修改结构体字段
  • B. 运行后 X 变为 2
  • C. 运行抛 InvalidOperationException
  • D. 静默无效,X 仍为 1
查看答案与解析

正确答案A

解析: dict["a"] 返回的是结构体的副本,对副本字段赋值没有意义,编译器直接以 CS1612 拒绝。要让字典里的结构体字段可改,应取 ref(CollectionsMarshal)或改成可变类、改用整体替换 dict["a"] = new S { X = 2 }

关键点: 字典索引器返回副本——“改了没生效”多半是这里。

0496 以下代码输出什么?

难度: 实战

var set = new HashSet<Person>();
set.Add(new Person("A"));

Console.WriteLine(set.Contains(new Person("A")));

record Person(string Name);
  • A. False,record 是引用类型,使用引用相等
  • B. False,每次 new 都是不同对象
  • C. 编译失败,record 不能作为 HashSet 元素
  • D. True,record 自动实现基于成员的 Equals/GetHashCode
查看答案与解析

正确答案D

解析: 编译器为 record 合成值语义的 EqualsGetHashCode(基于 Name),两个 Person("A") 相等且哈希相同,因此 Contains 返回 true。容易误以为“record 也是 class 就该引用相等”。

关键点: record 的相等语义是成员级,与普通 class 不同。

0497 以下代码输出什么?

难度: 实战

var dict = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
dict["A"] = 1;
dict["a"] = 2;

Console.WriteLine(dict.Count);
Console.WriteLine(dict["A"]);
  • A. 2 / 1
  • B. 1 / 1
  • C. 1 / 2,忽略大小写下两次写入命中同一键,后者覆盖
  • D. 2 / 2
查看答案与解析

正确答案C

解析: 比较器忽略大小写后,"A""a" 是同一个键:第一次 Add 语义、第二次覆盖值。因此 Count 为 1,读 "A" 得到 2。

关键点: 比较器决定“同键”的判定,写覆盖不产生新条目。

0498 以下代码输出什么?

难度: 进阶

var dict = new Dictionary<object, string>();
dict[1] = "a";

Console.WriteLine(dict[1]);
  • A. 输出 a,装箱后仍按值比较
  • B. 抛 KeyNotFoundException,两次装箱引用不同
  • C. 输出 null
  • D. 编译失败,object 键无法装箱
查看答案与解析

正确答案A

解析: 两个内容相同的装箱 intEquals/GetHashCode 上按值比较(boxed.Equals(boxed) 委托给 int.Equals),所以 dict[1] 能命中先前存入的键。

关键点: 装箱不改变相等语义,别把“引用不同”套到值类型的装箱对象上。

0499 以下代码输出什么?

难度: 进阶

class K
{
    public int Id;
    public override bool Equals(object? o) => o is K k && Id == k.Id;
    // 未重写 GetHashCode
}

var dict = new Dictionary<K, int>();
dict[new K { Id = 1 }] = 10;

Console.WriteLine(dict.ContainsKey(new K { Id = 1 }));
  • A. True,Equals 相等即可命中
  • B. False,未重写 GetHashCode 时使用基于引用的哈希,两个实例落入不同桶
  • C. True,编译器会自动生成 GetHashCode
  • D. 编译警告但结果仍为 True
查看答案与解析

正确答案B

解析: 只重写 Equals 不重写 GetHashCode 会触发 CS0659 警告;GetHashCode 仍是 object 的基于引用实现,两个内容相同但引用不同的键哈希不同,落入不同桶,ContainsKey 返回 false。

关键点: 相等性契约要求 Equals 与 GetHashCode 成对重写。

0500 关于 CollectionsMarshal.GetValueRefOrAddDefault 返回的 ref 生命周期,哪项正确?

难度: 进阶

  • A. ref 永久有效,可长期保存并跨方法传递
  • B. 只有扩容才失效,Add 已有键不影响
  • C. ref 在字典发生结构变化(Add/Remove/扩容/清空)后失效,继续使用是未定义行为
  • D. ref 指向值类型的装箱副本,修改不会反映到字典
查看答案与解析

正确答案C

解析: 返回的 ref 直接指向字典内部槽位,任何改变内部数组或条目布局的结构性操作都会使该 ref 失效。官方文档明确:修改字典后继续使用旧 ref 属于未定义行为,可能读到垃圾数据甚至破坏内存。

关键点: 拿到 ref 后应立即读写完,不要跨越结构修改继续使用。

官方资料

当前分类

C# 选择题

查看全部分类 →
  1. 23C# 试题 23:集合接口20 题
  2. 24C# 试题 24:线性集合20 题
  3. 25C# 试题 25:Dictionary、HashSet 与相等性20 题
  4. 26C# 试题 26:有序、并发与不可变集合20 题
  5. 27C# 试题 27:LINQ 延迟执行20 题
ESC

输入关键词开始搜索