C# 试题 32:释放模式与终结器
0621 以下代码输出什么?
难度: 实战
class R : IDisposable
{
public void Dispose()
{
Console.WriteLine("Dispose");
GC.SuppressFinalize(this);
}
~R() => Console.WriteLine("Finalize");
}
var r = new R();
r.Dispose();
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("done");
- A.
Dispose/Finalize/done - B.
Dispose/done,SuppressFinalize 阻止终结器运行 - C.
done - D.
Finalize/done
查看答案与解析
正确答案B
解析: Dispose 中调用 GC.SuppressFinalize(this) 会把对象从终结队列摘除;对象已被显式释放,之后即使不可达也不会执行终结器,因此不输出 Finalize。
关键点: 显式释放后必须 SuppressFinalize,否则终结器还会白跑一次。
0622 以下代码输出什么?
难度: 进阶
class R
{
~R() => Console.WriteLine("F");
}
void Create() => _ = new R();
Create();
GC.Collect();
GC.WaitForPendingFinalizers();
- A. 无输出
- B.
F,对象不可达后被放入终结队列并执行终结器 - C. 抛
NullReferenceException - D. 编译失败
查看答案与解析
正确答案B
解析: 对象未被显式释放,Create 返回后不可达;GC.Collect 把它移入终结队列,WaitForPendingFinalizers 等待终结器线程执行,最终输出 F。
关键点: 有终结器且未 SuppressFinalize 的对象,回收前必然执行终结器。
0623 关于终结器中访问其他托管对象,哪项正确?
难度: 进阶
- A. 完全安全,只要对象还被引用
- B. 语法上不允许
- C. 终结顺序不受控制,被引用对象可能已被终结或处于不确定状态,因此终结器不应访问其他托管对象
- D. 只有多线程下才有风险
查看答案与解析
正确答案C
解析: 终结器运行时无法保证它引用的其他对象是否已被终结或处于何种状态(终结顺序不受控制),因此规范要求终结器只释放自己直接持有的非托管资源,不要依赖其他托管对象。
关键点: 终结器里“顺手清理别人的资源”是经典的时序 bug。
0624 以下代码运行结果是什么?
难度: 进阶
class Bad
{
~Bad() => throw new InvalidOperationException("boom");
}
_ = new Bad();
GC.Collect();
GC.WaitForPendingFinalizers();
- A. 异常被运行时静默吞掉
- B. 异常抛回主线程
- C. 编译失败
- D. 进程终止,终结器线程上的未处理异常会终止进程
查看答案与解析
正确答案D
解析: 终结器在线程池专用线程上运行,未捕获的异常不会被吞掉,而是导致进程终止(.NET 4.0 起的行为)。终结器内必须 try/catch 吞掉所有异常。
关键点: 终结器抛异常 = 进程崩溃,比普通线程的未观察异常更严重。
0625 以下代码输出什么?
难度: 实战
class R : IDisposable
{
private bool _disposed;
public void Dispose()
{
if (_disposed) return;
_disposed = true;
GC.SuppressFinalize(this);
Console.WriteLine("release");
}
}
var r = new R();
r.Dispose();
r.Dispose();
- A. 输出两次
release - B. 输出一次
release - C. 第二次调用抛
ObjectDisposedException - D. 编译失败
查看答案与解析
正确答案B
解析: 标准的 Dispose 模式要求幂等:第一次调用执行释放,第二次因 _disposed 已置位直接返回。释放方法抛异常或重复执行都违反契约。
关键点: Dispose 必须可重入,靠 disposed 标志短路。
0626 以下代码输出什么?
难度: 实战
class R : IDisposable
{
public void Dispose() { }
~R() => Console.WriteLine("F");
}
void M()
{
using (var r = new R()) { }
}
M();
GC.Collect();
GC.WaitForPendingFinalizers();
- A. 无输出
- B. 抛
ObjectDisposedException - C. 输出
F,Dispose 未调用 SuppressFinalize,对象仍会被终结 - D. 编译失败
查看答案与解析
正确答案C
解析: using 只保证调用 Dispose,不会自动抑制终结。这里 Dispose 是空实现,没有 GC.SuppressFinalize,对象仍留在终结队列,最终执行终结器输出 F——这是“实现了 IDisposable 却漏掉 SuppressFinalize”的经典表现。
关键点: using 与终结器抑制是两件事,SuppressFinalize 必须显式调用。
0627 以下代码输出什么?
难度: 实战
class R : IAsyncDisposable
{
public ValueTask DisposeAsync()
{
Console.WriteLine("D");
return ValueTask.CompletedTask;
}
}
await using (var r = new R())
{
Console.WriteLine("body");
}
- A.
body/D - B.
D/body - C. 编译失败
- D. 只输出
body
查看答案与解析
正确答案A
解析: await using 先执行块体,退出时调用 DisposeAsync(),因此先输出 body 再输出 D,与同步 using 的时序一致。
关键点: await using 的释放发生在作用域末尾,且是异步的 DisposeAsync。
0628 类型同时实现 IDisposable 与 IAsyncDisposable,以下代码输出什么?
难度: 进阶
class R : IDisposable, IAsyncDisposable
{
public void Dispose() => Console.WriteLine("sync");
public ValueTask DisposeAsync()
{
Console.WriteLine("async");
return ValueTask.CompletedTask;
}
}
await using (var r = new R()) { }
- A. 输出
sync - B. 输出
async,await using 优先使用 IAsyncDisposable - C. 输出
sync和async - D. 编译失败
查看答案与解析
正确答案B
解析: 类型同时实现两个接口时,await using 优先选择 IAsyncDisposable.DisposeAsync,只输出 async;同步 using 才会调用 Dispose。两者不会都被调用。
关键点: 同一资源不要“双份释放”,await using 只走异步路径。
0629 以下代码能编译吗?
难度: 进阶
class R : IDisposable
{
public void Dispose() { }
}
await using (var r = new R()) { }
- A. 正常运行,回退到同步 Dispose
- B. 运行抛
InvalidOperationException - C. 编译警告但正常运行
- D. 编译失败,await using 要求类型可异步释放(实现 IAsyncDisposable 或提供 DisposeAsync)
查看答案与解析
正确答案D
解析: await using 要求资源类型支持异步释放:实现 IAsyncDisposable,或提供可访问的无参 DisposeAsync() 模式;只有 IDisposable 的类无法通过编译。反过来,只实现 IAsyncDisposable 也不能用于同步 using。
关键点: 同步/异步释放接口互不兼容,实现时要成对提供。
0630 为什么包装原生句柄时应继承 SafeHandle,而不是自己写终结器释放句柄?
难度: 进阶
- A. SafeHandle 会自动释放任意内存
- B. 自定义终结器更快
- C. SafeHandle 使用临界终结(critical finalization),在进程关闭、OOM 等极端场景下仍保证句柄被释放,且避免普通终结器的顺序问题
- D. SafeHandle 不需要任何显式释放
查看答案与解析
正确答案C
解析: SafeHandle 基于临界终结机制,即使出现 OOM、AppDomain 卸载或进程关闭也能保证释放原生句柄;普通终结器在这些场景可能不执行,且存在与 SafeHandle 终结顺序交错导致句柄泄漏或误关的风险。
关键点: 原生句柄一律走 SafeHandle,别自己写终结器关句柄。
0631 以下代码为什么需要两次 GC.Collect 才能完成回收?
难度: 实战
class F
{
~F() { }
}
for (int i = 0; i < 1000; i++) _ = new F();
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
- A. 第一次 GC 把对象放入终结队列并使其存活一代,终结器运行后才能回收,需要第二次 GC
- B. 终结器线程太慢,需要多次尝试
- C. 只需要一次就足够
- D. 两次 Collect 与终结器无关
查看答案与解析
正确答案A
解析: 有终结器的对象不可达后先进入终结队列,此时仍被视为“存活”并被提升到下一代;终结器运行完毕后对象才真正可回收,因此必须再来一次 GC 才能释放内存。这就是终结器让对象“多活一代”的成本来源。
关键点: 终结器延迟回收并抬高代龄,能 SuppressFinalize 就 Suppress。
0632 以下代码输出什么?
难度: 进阶
class R
{
~R() => Console.WriteLine("F");
}
static R? Keep;
Keep = new R();
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("done");
- A.
F/done - B.
done,对象仍被静态字段引用,不可达之前不会终结 - C. 只输出
F - D. 抛
NullReferenceException
查看答案与解析
正确答案B
解析: 静态字段是 GC 根,Keep 仍引用该对象,对象依然可达;终结器只对不可达对象运行,因此不输出 F,只输出 done。
关键点: 终结器触发条件是“不可达”,不是“调用了 GC.Collect”。
0633 以下代码输出什么?
难度: 实战
Stream? s = null;
using (s)
{
Console.WriteLine("in");
}
Console.WriteLine("out");
- A. 抛
NullReferenceException - B. 编译失败,不能 using null
- C. 只输出
out - D. 输出
in/out,using 对 null 是空操作
查看答案与解析
正确答案D
解析: using 语句在资源为 null 时直接跳过释放,不会抛异常;块体正常执行。这是框架代码里常见的“可选资源用 using 包裹”写法的基础。
关键点: using (null) 合法且无害,释放阶段被安全跳过。
0634 以下代码能编译吗?
难度: 实战
class R
{
public void Dispose() { }
}
using (var r = new R()) { }
- A. 正常运行,using 支持鸭子类型
- B. 运行抛
InvalidCastException - C. 编译失败,普通类用于 using 必须实现 IDisposable(Dispose 模式只适用于 ref struct)
- D. 编译警告但正常运行
查看答案与解析
正确答案C
解析: 普通引用类型用于 using 必须实现 IDisposable(编译错误 CS1674);“有 Dispose 方法就行”的鸭子类型只适用于 ref struct(如 Span<T>)。只有 await using 对普通类型放宽为 DisposeAsync() 模式。
关键点: 类必须显式实现 IDisposable;模式匹配是 ref struct 的特权。
0635 using 块体内抛了异常,同时 Dispose 也抛了异常,最终抛出哪个?
难度: 实战
- A. Dispose 的异常,finally 语义下释放异常覆盖原异常,因此 Dispose 不应抛异常
- B. 原异常,Dispose 异常被忽略
- C. 两个异常合并为 AggregateException
- D. 进程崩溃
查看答案与解析
正确答案A
解析: using 展开后释放代码位于 finally 中:finally 抛出的异常会替换 try 块中尚未抛出的原始异常,导致原始异常信息丢失。因此 Dispose 的实现必须捕获并吞掉自己的异常(幂等、不抛)。
关键点: 释放路径抛异常会掩盖业务异常,这是 Dispose 必须不抛的原因之一。
0636 以下设计有什么问题?
难度: 进阶
class RawHandleOwner
{
private IntPtr _handle; // 通过 P/Invoke 获得的原生句柄
~RawHandleOwner()
{
CloseHandle(_handle); // P/Invoke 关闭句柄
}
}
- A. 这是推荐写法
- B. 编译失败
- C. 只影响性能
- D. 危险:普通终结器不保证在 SafeHandle 之后运行,句柄可能已被关闭或复用;应改为持有 SafeHandle 并实现 Dispose 模式
查看答案与解析
正确答案D
解析: 裸 IntPtr 句柄配合普通终结器无法保证终结顺序与时机:对象的终结器可能晚于(或早于)它持有的 SafeHandle 终结器执行,造成句柄泄漏、重复关闭或误关已复用的句柄。正确做法是把句柄包进 SafeHandle。
关键点: 手写终结器管句柄 = 竞态与泄漏的温床。
0637 以下代码输出什么?
难度: 进阶
SafeFileHandle handle;
using (var fs = new FileStream("t.bin", FileMode.Create))
{
handle = fs.SafeFileHandle;
}
Console.WriteLine(handle.IsClosed);
- A.
False,句柄归调用方所有 - B.
True,FileStream 释放时关闭其 SafeFileHandle - C. 抛
ObjectDisposedException - D. 结果不确定
查看答案与解析
正确答案B
解析: FileStream.SafeFileHandle 返回的是 FileStream 持有的句柄包装,释放 FileStream 时会关闭这个 SafeFileHandle,因此 IsClosed 为 True。把 SafeFileHandle 取出来“转交所有权”并不会让 FileStream 放弃释放责任。
关键点: SafeFileHandle 的释放仍由持有它的 FileStream 负责。
0638 以下代码输出什么?
难度: 实战
class R : IAsyncDisposable
{
private bool _disposed;
public ValueTask DisposeAsync()
{
if (_disposed) return ValueTask.CompletedTask;
_disposed = true;
GC.SuppressFinalize(this);
return ValueTask.CompletedTask;
}
~R() => Console.WriteLine("F");
}
await using (var r = new R()) { }
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("done");
- A.
F/done - B.
done/F - C.
done,DisposeAsync 中调用了 SuppressFinalize - D. 编译失败
查看答案与解析
正确答案C
解析: 异步释放路径同样必须调用 GC.SuppressFinalize(this):这里 DisposeAsync 正确抑制了终结器,因此不输出 F,只输出 done。实现 IAsyncDisposable 时漏掉 SuppressFinalize 是常见失误。
关键点: 无论是 Dispose 还是 DisposeAsync,释放后都要抑制终结器。
0639 以下代码输出什么?
难度: 实战
class R : IDisposable
{
public void Dispose() => Console.WriteLine("d");
}
for (int i = 0; i < 2; i++)
{
using var r = new R();
Console.Write(i);
}
- A.
0d1d - B.
01dd - C.
dd01 - D. 编译失败
查看答案与解析
正确答案A
解析: using var 声明的作用域是包含它的块(这里每次循环迭代的块体):每轮先输出序号,迭代结束时释放,因此是 0d1d。若把 using var 放在循环外,才会在循环结束后统一释放。
关键点: using 声明的生命周期 = 所在作用域,在循环内就是“每轮释放”。
0640 以下代码输出什么?(R 的 Dispose 打印自身名字)
难度: 实战
using (var a = new R("A"))
using (var b = new R("B"))
{
Console.WriteLine("body");
}
- A.
body/A/B - B.
A/B/body - C.
body/B/A,嵌套 using 按声明逆序释放 - D.
B/A/body
查看答案与解析
正确答案C
解析: 嵌套 using 展开为 try/finally 嵌套,释放顺序与声明顺序相反:先释放后声明的 b,再释放 a,块体先执行,因此是 body / B / A。
关键点: 多个资源按“后进先出”释放,与资源间的依赖方向保持一致。