VS2017中ObsCol<T>构造函数虚方法警告解决及循环事件处理疑问
关于循环/重复事件处理的情况与解决方案
咱们先来说说循环、重复事件触发的常见场景和对应的处理办法:
常见触发场景
- 重复订阅事件:比如你在按钮点击的方法里反复执行
obj.CollectionChanged += MyHandler,每次点击都会多绑一次Handler,后续集合变更时,这个Handler就会被执行多次。 - 集合操作引发的循环触发:比如在
CollectionChanged的处理逻辑里又修改了集合本身(比如添加、删除元素),这会再次触发CollectionChanged事件,形成无限循环,严重的话会导致栈溢出。 - 双向绑定的循环反馈:在MVVM场景中,父组件的属性变更触发子组件更新,子组件更新又反过来触发父组件属性变更,来回往复停不下来。
对应的处理方案
- 避免重复订阅:
- 订阅前先取消订阅:每次绑定事件前先执行
obj.CollectionChanged -= MyHandler,再执行+=,确保每次只有一个绑定实例;或者把事件订阅放在只执行一次的地方,比如类的构造函数里,别放在按钮点击这类会重复执行的代码块中。 - 用弱事件模式:如果担心内存泄漏或者重复绑定,可以用
WeakEventManager来管理事件,它会自动处理对象回收和重复绑定的问题。
- 订阅前先取消订阅:每次绑定事件前先执行
- 打破集合事件循环:
- 临时禁用事件:在修改集合前先移除事件处理,操作完成后再重新绑定,示例代码如下:
this.CollectionChanged -= OnCollectionChanged; // 执行集合修改操作,比如添加元素 this.Add(new T()); this.CollectionChanged += OnCollectionChanged; - 添加状态标记:在事件处理方法里加一个布尔标记,判断当前是否正在处理事件,如果是就直接返回,避免递归触发:
private bool _isProcessing = false; private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (_isProcessing) return; _isProcessing = true; try { // 这里写你的集合变更处理逻辑,哪怕修改集合也不会循环触发了 } finally { _isProcessing = false; } }
- 临时禁用事件:在修改集合前先移除事件处理,操作完成后再重新绑定,示例代码如下:
- 解决双向绑定循环:在ViewModel的属性设置方法里,先判断新值和旧值是否相等,只有当值真正变化时才触发
PropertyChanged事件,比如:private string _name; public string Name { get => _name; set { if (_name == value) return; _name = value; OnPropertyChanged(); } }
VS2017中构造函数调用虚方法警告的根源与解决办法
再聊聊你遇到的这个VS警告,咱们先搞清楚根源,再讲解决办法:
警告根源
这个警告的核心问题是在子类的构造函数中,直接或间接调用了虚方法。在C#的类初始化逻辑里,子类构造函数执行时会先跑父类的构造函数,此时子类的实例还没完全初始化完成。如果这个时候调用了虚方法,实际执行的是子类重写的版本,但子类的成员可能还没赋值,很容易出现意料之外的bug。
回到你的代码,ObsCol<T>继承自ObservableCollection<T>,你在构造函数里操作了CollectionChanged事件——而ObservableCollection<T>里的OnCollectionChanged是个虚方法,当你订阅CollectionChanged或者操作集合(比如添加元素)时,会触发这个虚方法的调用,VS检测到这个调用链,就会抛出警告提醒你潜在的风险。
解决办法
- 延迟初始化逻辑:把原本在构造函数里的事件订阅、集合操作等逻辑,移到一个单独的初始化方法里,等实例完全创建好之后再手动调用。示例代码:
使用的时候就这么写:[Serializable] public class ObsCol<T> : ObservableCollection<T> { public ObsCol() { // 构造函数只做最基础的初始化,不碰任何会触发虚方法的逻辑 } public void Setup() { // 在这里执行原本构造函数里的逻辑,比如订阅事件 this.CollectionChanged += OnCollectionChanged; // 或者添加初始元素 this.Add(default(T)); } }var myCol = new ObsCol<MyType>(); myCol.Setup(); - 重写父类的虚方法代替事件订阅:与其直接订阅
CollectionChanged事件,不如重写父类的OnCollectionChanged方法,这是符合类初始化规范的做法,哪怕在构造阶段被调用,逻辑也更可控:[Serializable] public class ObsCol<T> : ObservableCollection<T> { public ObsCol() { // 这里可以正常操作集合,重写的OnCollectionChanged会被调用 this.Add(default(T)); } protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { // 在这里处理集合变更逻辑 // 记得调用父类的方法,保证CollectionChanged事件能正常对外触发 base.OnCollectionChanged(e); } } - 临时禁用警告(不推荐):如果你确认当前的逻辑不会有问题,也可以在代码里禁用这个特定警告。先看VS警告窗口里的警告代码(比如CSXXX),然后在构造函数前后加上:
不过这个办法是下策,毕竟警告是VS在帮你规避潜在bug,能从根源解决最好。#pragma warning disable CSXXX // 替换成实际的警告代码 public ObsCol() { this.CollectionChanged += OnCollectionChanged; } #pragma warning restore CSXXX
内容的提问来源于stack exchange,提问作者Erik
相关产品推荐
相关产品推荐

