C# 试题 18:事件
0341 以下代码能否编译?
难度: 实战
class Pub
{
public event Action? Changed;
}
var pub = new Pub();
pub.Changed = () => { };
- A. 编译通过
- B. 编译失败,事件在类外只能通过
+=/-=访问 - C. 运行抛
InvalidOperationException - D. 编译通过,但订阅无效
查看答案与解析
正确答案B
解析: event 的封装语义是:外部只能 +=/-=,不能赋值、不能调用。pub.Changed = ... 报 CS0070。字段式事件在类内暴露的是私有后备委托字段,在类外暴露的只有 add/remove 两个访问器。
关键点: 事件 = 受限委托字段,类外只许 +=/-=。
0342 以下代码能否编译?
难度: 实战
class Base
{
public event Action? Changed;
}
class Derived : Base
{
public void Raise() => Changed?.Invoke();
}
- A. 编译通过并正常触发
- B. 运行抛
NullReferenceException - C. 编译失败,派生类不能直接调用基类事件
- D. 编译通过,但事件不触发
查看答案与解析
正确答案C
解析: 字段式事件的后备委托字段是声明类的私有成员,派生类对基类事件同样只能 +=/-=,不能调用或赋值,报 CS0070。正确做法是基类提供 protected virtual void OnChanged() 供派生类调用。
关键点: 派生类不能引发基类事件,需要 protected 引发方法。
0343 以下代码中,假设 pub 仍被引用,sub 实例能否被回收?
难度: 进阶
class Pub
{
public event Action? E;
}
class Sub
{
public void On() { }
}
var pub = new Pub();
var sub = new Sub();
pub.E += sub.On;
sub = null;
GC.Collect();
GC.WaitForPendingFinalizers();
- A. 不能,
pub.E的委托目标仍强引用sub实例 - B. 能,事件不持有订阅者
- C. 能,GC 会忽略事件引用
- D. 编译失败
查看答案与解析
正确答案A
解析: 委托内部持有目标对象引用:pub.E 指向的委托其 Target 是 sub 实例,于是 pub → 委托 → sub 形成强引用链,sub 不可达(无法回收)。事件源强引用订阅者,是内存泄漏的经典来源。
关键点: 发布者强引用订阅者;生命周期长的发布者会让订阅者无法回收。
0344 以下代码输出什么?
难度: 实战
class Pub
{
public event Action? E;
}
var pub = new Pub();
pub.E += () => Console.Write("x");
pub.E -= () => Console.Write("x");
pub.E?.Invoke();
- A. 什么都不输出
- B. 编译失败
- C. 运行抛异常
- D. 输出
x,两个 lambda 是不同委托实例,-=无法匹配
查看答案与解析
正确答案D
解析: += 与 -= 右侧是两个不同的 lambda 表达式,生成不同方法、不同委托实例,Delegate.Remove 找不到匹配项,退订无效,事件照常触发输出 x。匿名 lambda 无法退订,是订阅生命周期管理的头号坑。
关键点: 匿名 lambda 订阅后无法用新 lambda 退订。
0345 关于以下 Raise 方法在并发下的行为,哪项正确?
难度: 进阶
class Pub
{
public event Action? E;
public void Raise() => E?.Invoke();
}
- A. 存在”检查为 null 后、调用前被移除”的竞态,可能抛
NullReferenceException - B.
?.Invoke()只读取一次事件字段,检查与调用使用同一份委托快照,不会因中途移除而抛 NRE - C. 必须先加锁,否则编译失败
- D.
?.Invoke()会展开并复制所有订阅者
查看答案与解析
正确答案B
解析: 空条件调用对目标表达式只求值一次:E?.Invoke() 读取一次字段,非空则用同一份快照调用。即使另一线程同时 -= 置空,也不影响本次调用。真正会踩 NRE 的是下面这种”两次读取”写法。
关键点: E?.Invoke() 是单次求值的原子快照调用。
0346 以下 Raise 方法在并发下可能发生什么?
难度: 实战
class Pub
{
public event Action? E;
public void Raise()
{
if (E != null)
{
E.Invoke();
}
}
}
- A. 总能正常运行
- B. 编译失败
- C. 并发下可能抛
InvalidOperationException - D. 并发下可能抛
NullReferenceException,两次读取字段之间E可能被置空
查看答案与解析
正确答案D
解析: if (E != null) 和 E.Invoke() 是对字段的两次独立读取:另一线程在检查之后把 E 设为 null,第二次读取得到 null,调用抛 NullReferenceException。这就是推荐 E?.Invoke() 或先拷贝局部变量的原因。
关键点: 检查与调用分开写 = 两次读取 = 竞态窗口。
0347 关于标准 EventHandler 模式,哪项正确?
难度: 进阶
- A.
sender必须是字符串 - B. 事件参数必须为 null
- C.
sender是引发事件的对象,e携带事件数据;无数据用EventArgs.Empty - D.
EventHandler不能用于静态事件
查看答案与解析
正确答案C
解析: EventHandler(object? sender, EventArgs e) 约定:sender 是引发者(无引发者时为 null),e 是事件数据;无数据时传 EventArgs.Empty 而不是 null。静态事件可以存在,但因其生命周期问题一般应避免。
关键点: sender = 引发者,e = 数据,空数据用 EventArgs.Empty。
0348 以下代码能否编译?
难度: 实战
class MyArgs { }
class Pub
{
public event EventHandler<MyArgs>? Changed;
}
- A. 编译失败,
EventHandler<TEventArgs>要求TEventArgs : EventArgs - B. 编译通过
- C. 编译通过,但订阅时报错
- D. 编译通过,运行抛异常
查看答案与解析
正确答案A
解析: EventHandler<TEventArgs> 委托声明带 where TEventArgs : EventArgs 约束,MyArgs 不派生自 EventArgs,声明处直接编译失败。自定义事件参数类必须继承 EventArgs。
关键点: 泛型 EventHandler<T> 的 T 必须继承 EventArgs。
0349 以下代码输出什么?
难度: 实战
class Pub
{
public event Action? E;
public void Raise() => E?.Invoke();
}
var pub = new Pub();
pub.E += () => Console.Write("1");
pub.E += () => throw new InvalidOperationException();
pub.E += () => Console.Write("3");
try
{
pub.Raise();
}
catch (InvalidOperationException)
{
}
- A. 输出
13 - B. 输出
123 - C. 输出
1,异常中止后续订阅者 - D. 编译失败
查看答案与解析
正确答案C
解析: 事件本质上就是多播委托调用:按订阅顺序执行,“1” 输出后第二个订阅者抛异常,调用链中止,“3” 永远不执行,异常传播到 Raise() 的调用方。事件机制不会自动隔离订阅者异常。
关键点: 订阅者异常会中止后续订阅者并传播给引发方。
0350 关于事件中的异常传播,哪项正确?
难度: 进阶
- A. 订阅者异常中止后续订阅者,并沿
Raise()调用栈传播给引发者;事件不包装异常 - B. 事件会把订阅者异常包装为
TargetInvocationException - C. 事件框架自动忽略订阅者异常
- D. 异常只影响抛异常的订阅者
查看答案与解析
正确答案A
解析: 事件引发就是普通委托调用,异常原样沿调用栈传播,不包装、不捕获。TargetInvocationException 只出现在反射调用(MethodInfo.Invoke)等场景。因此引发方要自行决定在哪一层 try/catch。
关键点: 事件异常原样传播,没有自动包装或隔离。
0351 以下代码输出什么?
难度: 进阶
class Pub
{
public event Action? E;
public void Raise() => E?.Invoke();
}
var pub = new Pub();
Action handler = null!;
handler = () =>
{
Console.Write("x");
pub.E -= handler;
};
pub.E += handler;
pub.Raise();
pub.Raise();
- A.
xx - B. 什么都不输出
- C. 运行抛异常
- D.
x,第一次触发后处理器自我退订
查看答案与解析
正确答案D
解析: 事件处理器内可以安全地退订自身(-= 操作的是委托字段,与正在执行的快照无关):第一次 Raise() 输出 x 并退订,第二次触发时列表已空,只输出一次 x。这是实现”一次性事件”的标准手法。
关键点: 处理器内自我退订安全且立即生效于后续触发。
0352 以下代码哪些行能编译?
难度: 实战
class Pub
{
public event Action? E;
}
var pub = new Pub();
pub.E += M; // ①
pub.E?.Invoke(); // ②
pub.E = null; // ③
static void M() { }
- A. 只有①能编译,类外只能
+=/-= - B. ①②能编译
- C. ①③能编译
- D. ①②③都能编译
查看答案与解析
正确答案A
解析: 类外对字段式事件只有 add/remove 两个访问器可见:① += 合法;② 调用 Invoke 报 CS0070;③ 赋值也报 CS0070。事件的封装就是为了禁止外部任意触发或清空。
关键点: 类外只能 +=/-=,调用与赋值都编译失败。
0353 以下 Raise 中的锁能否保证与其它线程的订阅/退订互斥?
难度: 进阶
class Pub
{
private readonly object _sync = new();
public event Action? E;
public void Raise()
{
lock (_sync)
{
E?.Invoke();
}
}
}
- A. 能,锁覆盖了所有事件访问
- B. 不能,字段事件的默认
add/remove不加锁,Raise里的锁无法与+=/-=互斥 - C. 编译失败
- D. 能,但会死锁
查看答案与解析
正确答案B
解析: 字段式事件由编译器生成简单的 add/remove(Delegate.Combine/Remove 赋值),不经过 _sync;Raise 里加锁只是让锁内的读取与其他加锁代码互斥,而 +=/-= 根本不同步。要真正互斥,必须用自定义事件访问器在 add/remove 里也加同一把锁。
关键点: 只有 Raise 加锁是无用的,add/remove 也要同步。
0354 以下代码能否编译?
难度: 实战
class Pub
{
private Action? _changed;
public event Action? Changed
{
add => _changed += value;
remove => _changed -= value;
}
public void Raise() => Changed?.Invoke();
}
- A. 编译通过并正常工作
- B. 运行抛
NullReferenceException - C. 编译失败,
Changed此时是事件而非字段,应调用_changed?.Invoke() - D. 编译通过,但事件无法订阅
查看答案与解析
正确答案C
解析: 一旦提供自定义 add/remove,类内的 Changed 就只指事件本身,不再有自动后备字段;Changed?.Invoke() 报 CS0070。必须改为 _changed?.Invoke()。这是”从字段事件改自定义访问器”时最常踩的编译错误。
关键点: 自定义访问器后,事件名在类内也不再可调用,要用后备字段。
0355 弱事件(weak event)模式要解决什么问题?
难度: 进阶
- A. 让订阅者不再持有发布者
- B. 让事件只触发一次
- C. 消除所有事件引用
- D. 避免长生命周期发布者长期强引用订阅者,导致订阅者无法被回收
查看答案与解析
正确答案D
解析: 普通事件让发布者强引用订阅者;当发布者生命周期很长(如全局服务)而订阅者是短生命周期 UI 控件时,订阅者永远无法回收,形成泄漏。弱事件用弱引用登记订阅者,发布者不再强引用它们。代价是复杂度与事件可能”静默失效”。
关键点: 弱事件切断”发布者 → 订阅者”的强引用,治理事件泄漏。
0356 以下代码输出什么?
难度: 实战
class Base
{
public event EventHandler? Changed;
protected virtual void OnChanged() => Changed?.Invoke(this, EventArgs.Empty);
public void Fire() => OnChanged();
}
class Derived : Base
{
protected override void OnChanged()
{
// 未调用 base.OnChanged()
}
}
var d = new Derived();
d.Changed += (_, _) => Console.Write("sub");
d.Fire();
- A. 输出
sub - B. 什么都不输出,派生类覆盖后未调用
base.OnChanged() - C. 运行抛异常
- D. 编译失败
查看答案与解析
正确答案B
解析: Fire() 调用的 OnChanged() 已被派生类覆盖,而覆盖版本没有调用 base.OnChanged(),基类中触发订阅者的代码从未执行,订阅者收不到事件。覆盖 On* 引发方法时是否调用 base 是刻意设计,也是常见疏漏。
关键点: 覆盖 OnChanged 不调用 base 会静默吞掉基类订阅者。
0357 以下代码输出什么?
难度: 进阶
class Pub
{
public event Action? E;
public void Raise() => E?.Invoke();
}
var pub = new Pub();
pub.E += () => throw new InvalidOperationException();
try
{
pub.Raise();
}
catch (Exception ex)
{
Console.WriteLine(ex.GetType().Name);
}
- A.
InvalidOperationException - B.
TargetInvocationException - C.
AggregateException - D. 编译失败
查看答案与解析
正确答案A
解析: 事件引发走的是直接委托调用,异常原样抛出,类型不变。TargetInvocationException 只来自反射 Invoke,AggregateException 只来自任务等待——把事件异常想象成被包装是常见误判。
关键点: 事件抛出的就是订阅者抛出的那个异常,无包装。
0358 以下代码中,widget 实例能否被回收?
难度: 实战
class Service
{
public static event Action? Updated;
}
class Widget
{
public void OnUpdated() { }
}
var widget = new Widget();
Service.Updated += widget.OnUpdated;
widget = null;
GC.Collect();
GC.WaitForPendingFinalizers();
- A. 能,静态事件不会持有订阅者
- B. 能,GC 会清理静态引用
- C. 编译失败
- D. 不能,静态事件由类型持有,订阅者实例在进程存续期间无法被回收
查看答案与解析
正确答案D
解析: 静态事件挂在类型上,类型一旦加载就存活到进程结束;委托的 Target 强引用 widget 实例,即使变量置空也无法回收。SomeService.Updated += widget.Handle 这类代码是典型泄漏:订阅者寿命与进程相同。
关键点: 静态事件是”永久性泄漏源”,订阅必须配对退订。
0359 以下代码输出什么?
难度: 进阶
class Base
{
public event EventHandler? Changed;
protected virtual void OnChanged() => Changed?.Invoke(this, EventArgs.Empty);
public void Fire() => OnChanged();
}
class Derived : Base
{
protected override void OnChanged()
{
base.OnChanged();
Console.Write("derived");
}
}
var d = new Derived();
d.Changed += (_, _) => Console.Write("sub");
d.Fire();
- A.
subderived - B.
derivedsub - C.
sub - D.
derived
查看答案与解析
正确答案A
解析: 覆盖方法先调用 base.OnChanged(),基类先触发订阅者输出 sub,随后覆盖方法继续输出 derived,最终 subderived。On* 模式的约定是:派生类扩展行为,但通常应保留 base 调用以继续通知订阅者。
关键点: 基类订阅者先于派生类扩展逻辑执行(base 先调用时)。
0360 以下代码输出什么?
难度: 实战
class Pub
{
public event Action? E;
public void Raise() => E?.Invoke();
}
var pub = new Pub();
int n = 0;
void Handler() => n++;
pub.E += Handler;
pub.E += Handler;
pub.E -= Handler;
pub.Raise();
Console.WriteLine(n);
- A.
0 - B.
2 - C.
1,多播列表允许重复项,-=只移除最后一个匹配项 - D. 编译失败
查看答案与解析
正确答案C
解析: 同一处理器订阅两次,列表是 [Handler, Handler];-= Handler 用 Delegate.Remove 移除最后一个匹配项,剩一个。Raise() 调用一次,n == 1。若想完全退订,重复订阅几次就要移除几次。
关键点: 委托/事件允许多次订阅同一处理器,退订按次计数。