C# 试题 48:装箱、闭包与性能
0941 以下代码运行结果是什么?
难度: 实战
object o = 5;
long l = (long)o;
- A. 编译失败
- B. 运行抛
InvalidCastException:拆箱目标类型必须与装箱类型完全一致 - C. 输出 5
- D. 运行抛
OverflowException
查看答案与解析
正确答案B
解析: o 里装的是 int 的箱,(long)o 要求拆箱到 long,与装箱类型 int 不一致,运行时抛 InvalidCastException。拆箱不是数值转换,必须先拆回 int 再转 long。溢出检查在这里根本轮不到,类型不符先失败。
关键点: 拆箱类型必须与装箱类型完全一致。
0942 以下代码输出什么?
难度: 进阶
ConsoleColor c = ConsoleColor.Red;
object o = c;
Console.WriteLine(o.Equals(12));
Console.WriteLine(o.Equals(ConsoleColor.Red));
- A.
True与True - B.
False与False - C.
True与False - D.
False与True:参数分别装箱为int与ConsoleColor,枚举Equals只与同类型相等
查看答案与解析
正确答案D
解析: 枚举装箱后调用 Equals(object):12 装箱为 int,与装箱的 ConsoleColor 类型不同,返回 False;ConsoleColor.Red 装箱为同类型,值相等返回 True。枚举与整数看似同值,但装箱后类型不同,Equals 不会做数值等价。
关键点: 装箱后枚举与整数的 Equals 永不相等。
0943 以下代码输出什么?
难度: 实战
object o = 5;
Console.WriteLine(o is long);
Console.WriteLine(o is int);
- A.
False与True:is按真实装箱类型判断,不做数值转换 - B.
True与True - C.
True与False - D. 编译失败
查看答案与解析
正确答案A
解析: 装箱后对象携带的是原始类型 System.Int32。is long 检查装箱类型是否为 long,不是,返回 False;is int 匹配返回 True。is 不做“数值上能否容纳”的转换,这与显式转换语义完全不同。
关键点: is 匹配装箱时的原始类型,不是数值范围。
0944 关于装箱触发点,哪项正确?
难度: 进阶
- A.
int调用GetHashCode()一定会装箱 - B.
is类型测试会导致装箱 - C. 值类型转换为
object或所实现的接口时装箱;int的GetHashCode/ToString已重写所以不装箱,而接口调用(如IComparable.CompareTo)会装箱 - D.
typeof(int)会产生装箱
查看答案与解析
正确答案C
解析: 装箱发生在“值类型 → 引用(object/接口)”的转换点。Int32 重写了 GetHashCode/ToString,直接调用走约束调用不装箱;is/typeof 是类型元数据操作也不装箱。但 ((IComparable)x).CompareTo(y) 中 x 必须先装箱成接口引用,参数 y 再装箱一次。
关键点: 装箱发生在类型转换点,重写后的虚方法不装箱。
0945 以下代码运行结果是什么?
难度: 实战
object o = null;
int i = (int)o;
- A. 运行抛
NullReferenceException:对 null 拆箱 - B. 编译失败
- C.
i为 0 - D. 运行抛
InvalidCastException
查看答案与解析
正确答案A
解析: 拆箱要求引用非空且类型匹配。o 为 null,直接对 null 拆箱抛 NullReferenceException(先判空,后判类型)。常见混淆点是以为会得到默认值 0——不会,拆箱 null 永远抛异常。
关键点: 对 null 拆箱抛 NullReferenceException。
0946 以下代码运行结果是什么?
难度: 实战
object o = 5L;
int i = (int)o;
- A.
i为 5 - B.
i为 0 - C. 运行抛
OverflowException - D. 运行抛
InvalidCastException:long装箱后不能直接按int拆箱
查看答案与解析
正确答案D
解析: o 装的是 long,(int)o 要求拆箱为 int,类型不一致,直接抛 InvalidCastException。即使数值 5 完全落在 int 范围内也不行——拆箱没有数值收窄的概念,必须先 (int)(long)o。
关键点: 拆箱严格按类型匹配,不按数值范围放行。
0947 以下代码输出什么?
难度: 进阶
object o = 5;
long l = (long)(int)o;
Console.WriteLine(l);
- A. 运行抛
InvalidCastException - B. 输出 5:先按原类型
int拆箱,再做显式数值转换到long - C. 输出 0
- D. 编译失败
查看答案与解析
正确答案B
解析: (int)o 先把箱拆回原始类型 int(合法),(long) 再做 int → long 的显式数值转换,得到 5。这与 0941 的错误写法形成对照:拆箱必须还原原类型,数值转换必须放在拆箱之后显式完成。
关键点: 正确顺序是“先拆箱还原,再数值转换”。
0948 关于拆箱规则,哪项正确?
难度: 进阶
- A. 拆箱可以直接转换到任意数值类型
- B. 拆箱会自动完成数值收窄转换
- C. 拆箱目标类型必须与装箱类型完全一致,数值转换须在拆箱后显式进行;
o as int?在类型不符时得到 null 而不抛异常 - D. 拆箱不需要知道装箱时的原始类型
查看答案与解析
正确答案C
解析: 拆箱的硬规则是“类型完全一致”,任何数值转换都要拆箱后显式做。as 只对可空值类型生效:o as int? 类型不符时返回 null,是安全的判型方式;(int)o 类型不符则抛异常。两条路径的失败语义不同,按需选用。
关键点: (T) 抛异常,as T? 得 null,拆箱本身不转换数值。
0949 以下代码输出什么(关注装箱)?
难度: 实战
List<int> list = new() { 1, 2, 3 };
int sum = 0;
foreach (object o in list)
{
sum += (int)o;
}
Console.WriteLine(sum);
- A. 输出 6,且全程无装箱
- B. 编译失败
- C. 输出 0
- D. 输出 6:循环变量类型是
object,每个元素从int装箱再拆箱
查看答案与解析
正确答案D
解析: List<int> 的枚举器 Current 返回 int,但循环变量声明为 object,赋值时每个 int 都要装箱;(int)o 再拆箱回来。虽然是泛型集合,只要“形态”是 object 就会装箱。泛型消除的是“容器本身”的装箱,不是“目标类型”的装箱。
关键点: foreach 变量类型决定是否装箱,泛型容器不自动免疫。
0950 以下代码输出什么(关注装箱)?
难度: 实战
var list = new ArrayList { 1, 2, 3 };
int sum = 0;
foreach (int x in list)
{
sum += x;
}
Console.WriteLine(sum);
- A. 输出 6,且全程无装箱拆箱
- B. 输出 6:
ArrayList.Add(object)添加时逐个装箱,foreach (int x)读取时逐个拆箱 - C. 编译失败
- D. 运行抛
InvalidCastException
查看答案与解析
正确答案B
解析: ArrayList 是非泛型集合,集合初始值设定项走 Add(object),三个 int 全部装箱;foreach (int x in list) 走非泛型 IEnumerator.Current(返回 object),每次读取再拆箱。一装一拆共 6 次,这是非泛型容器的典型开销来源。
关键点: 非泛型集合的存取必然装箱/拆箱。
0951 以下代码输出什么(关注装箱)?
难度: 实战
IEnumerable<int> seq = new List<int> { 1, 2, 3 };
int sum = 0;
foreach (int x in seq)
{
sum += x;
}
Console.WriteLine(sum);
- A. 输出 6,且无装箱:
IEnumerable<int>是类型化接口,Current直接返回int - B. 输出 6,但每次读取都装箱
- C. 编译失败
- D. 运行抛异常
查看答案与解析
正确答案A
解析: 类型化接口 IEnumerable<int> 的 Current 返回 int,循环变量也是 int,全程没有 object 形态,无装箱拆箱。泛型接口和非泛型接口在装箱行为上是分水岭:同是接口,IEnumerable 与 IEnumerable<int> 的每次读取成本完全不同。
关键点: 泛型接口的枚举无装箱,非泛型接口每次读取装箱/拆箱。
0952 以下代码输出什么(关注装箱)?
难度: 进阶
IEnumerable boxed = new List<int> { 1, 2, 3 };
int sum = 0;
foreach (int x in boxed)
{
sum += x;
}
Console.WriteLine(sum);
- A. 输出 6,且无装箱拆箱
- B. 编译失败,
List<int>不能赋给非泛型IEnumerable - C. 输出 6:非泛型
IEnumerator.Current返回object,每次读取都拆箱 - D. 运行抛
InvalidCastException
查看答案与解析
正确答案C
解析: 变量类型是 object 形态的非泛型 IEnumerable,foreach (int x in boxed) 按非泛型枚举处理:Current 返回 object,int x 每次拆箱。注意此时 List 内部元素并未重新装箱(容器本身是泛型),但“读取路径”经过 object 就必然拆箱——容器泛型与枚举路径泛型是两回事。
关键点: 经非泛型接口枚举,读取路径产生拆箱。
0953 以下代码输出什么?
难度: 实战
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.Write(i));
}
foreach (var a in actions)
{
a();
}
- A. 输出
012 - B. 输出
123 - C. 输出
000 - D. 输出
333:for循环变量只有一份,三个闭包共享同一个i
查看答案与解析
正确答案D
解析: for 的循环变量 i 在整个循环中只有一份存储,三个 lambda 捕获的是同一个变量;循环结束后 i 为 3,三个委托执行时都读到 3,输出 333。这是闭包捕获“变量”而非“值”的直接体现。
关键点: for 循环变量被所有闭包共享。
0954 以下代码输出什么?
难度: 实战
var actions = new List<Action>();
foreach (int i in new[] { 0, 1, 2 })
{
actions.Add(() => Console.Write(i));
}
foreach (var a in actions)
{
a();
}
- A. 输出
222 - B. 输出
012:C# 5 起foreach每次迭代生成独立的循环变量 - C. 输出
333 - D. 输出
012,但这是未定义行为
查看答案与解析
正确答案B
解析: 从 C# 5 起,foreach 的迭代变量每次迭代都是新变量,三个闭包各捕获各的副本,输出 012。这与 for 的“共享同一变量”形成对比,也是把 for 改写为 foreach 时最容易引入/消除的 bug。
关键点: foreach 每轮独立变量,for 共享变量。
0955 以下代码输出什么(关注分配)?
难度: 进阶
static Func<int> Get() => () => 42;
Console.WriteLine(ReferenceEquals(Get(), Get()));
- A.
True:无捕获的 lambda 编译为缓存的静态委托,两次调用返回同一实例 - B.
False:每次调用都分配新委托 - C. 编译失败
- D. 运行抛异常
查看答案与解析
正确答案A
解析: 不捕获任何变量的 lambda 没有闭包状态,编译器把委托实例缓存到静态只读字段,每次执行 () => 42 都返回同一个缓存实例,ReferenceEquals 为 True。理解这一点才不会在“lambda 是否零分配”上误判:无捕获 = 缓存单例,有捕获 = 每次分配。
关键点: 无捕获 lambda 是编译器缓存的单例,零分配。
0956 以下代码输出什么(关注分配)?
难度: 进阶
int j = 0;
Action a1 = () => j++;
Action a2 = () => j++;
Console.WriteLine(a1 == a2);
- A.
True,两个 lambda 共享同一闭包对象 - B.
True,委托==只比较方法引用 - C.
False:两个 lambda 各生成一个委托实例(即使捕获同一变量),==比较引用不相等 - D. 编译失败
查看答案与解析
正确答案C
解析: 两个 lambda 表达式各编译成一个独立的委托创建指令,运行时生成两个不同实例;虽然它们捕获同一个变量 j(可能共享一个显示类对象),但委托实例不同,== 为 False。闭包共享“变量存储”不等于共享“委托实例”。
关键点: 捕获变量相同不代表委托实例相同。
0957 以下计时方法的结果是否可靠?
难度: 进阶
static int Foo() => 42;
var sw = Stopwatch.StartNew();
for (int i = 0; i < 1_000_000; i++)
{
Foo();
}
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
- A. 结果可靠,循环次数足够大
- B. 结果偏高但可接受,属于正常测量误差
- C. 一定抛
TimeoutException - D. 结果不可靠:无预热(首轮含 JIT 编译)、返回值被丢弃可能被整体优化、单次计时无统计意义
查看答案与解析
正确答案D
解析: 手写计时存在三重问题:第一次调用触发 JIT 编译,把预热成本算进结果;Foo() 的返回值被丢弃,JIT 可能把循环整体消除或内联成空操作;单次运行没有统计分布,无法排除 GC、调度等噪声。基准必须预热、保留结果(如累加到消耗变量)并多次采样。
关键点: 无预热 + 死代码消除 + 单次采样 = 不可信的基准。
0958 关于 BenchmarkDotNet 对该基准的处理,哪项正确?
难度: 实战
[MemoryDiagnoser]
public class Bench
{
[Benchmark]
public int Sum() => Enumerable.Range(0, 100).Sum();
}
- A. 在独立进程运行:先预热再统计迭代,
[MemoryDiagnoser]同时报告托管分配,结果可复现 - B. 与手写
Stopwatch循环完全等价 - C. 必须把方法改成
async才能测量 - D. 返回
int的方法会被框架跳过
查看答案与解析
正确答案A
解析: BenchmarkDotNet 会把基准编译进独立进程,先执行预热迭代让 JIT 完成分层编译,再按统计收敛规则运行测量迭代;[MemoryDiagnoser] 基于 GC 分配计数报告每次调用分配多少字节。这些机制专门解决 0957 里手写计时的三大缺陷。
关键点: 独立进程 + 预热 + 统计迭代是可信基准的前提。
0959 关于以下 BenchmarkDotNet 基准方法,哪项正确?
难度: 进阶
[Benchmark]
public int Add() => 1 + 1;
- A. 结果会被 JIT 常量折叠为 0
- B. 基准方法在预热后被反复调用,返回值被基准框架使用,避免死代码消除
- C. 必须加
[MethodImpl(MethodImplOptions.NoInlining)]才能运行 - D. 运行抛
InvalidBenchmarkException
查看答案与解析
正确答案B
解析: BenchmarkDotNet 生成的宿主代码会接收返回值并存储(累加或写入消耗点),使基准调用不可被 JIT 消除;即使 1 + 1 被常量折叠,调用本身仍被测到。[MethodImpl(NoInlining)] 只是可选手段,不是运行前提。理解“返回值被消费”才能解释为什么框架不丢调用。
关键点: 基准框架消费返回值,防止死代码消除。
0960 关于性能测量的工程实践,哪项正确?
难度: 进阶
- A. 只要用 Release 构建,任何计时都准确
- B. 单次测量比多次采样更可靠
- C. 手写计时前应预热、防止死代码消除、多次采样取统计值;测量分配用
[MemoryDiagnoser]或 GC 分配计数 - D. 基准方法应包含
Console.WriteLine输出结果以保证不被优化
查看答案与解析
正确答案C
解析: 可信测量 = 预热(排除 JIT/分层编译)+ 结果消费(防消除)+ 多次采样(抗噪声);分配测量要借助 GC 分配计数或诊断器,肉眼估算不可靠。Console.WriteLine 本身是巨大 I/O 开销,写进基准只会污染结果。
关键点: 预热、防消除、采样三件套,分配要量化。