C#关联类中如何正确处理PropertyChanged事件
我正在将一款遗留应用从C语言迁移至C#(.NET Core 5),落地更现代的软件开发实践。原C应用几乎将所有数据存储在内存的结构体中,且全部为全局可访问状态。这种模式对简单应用十分友好,但严重违反封装原则。面向对象实现中最接近该模式的方案是将数据存储设为static,出于可维护性考虑,我不打算采用这种方案。
当前应用专业性极强,涉及大量数学计算。旧版本会按需计算大量派生值(有时会重复计算),无法利用多线程与缓存能力提升性能。
为了用简单的类比示例说明问题,假设我们需要开发一个程序管理棒球队的相关成本:一支球队有固定运营成本,配备1名经理(有薪资),以及0到多名球员(每名球员也有对应薪资)。
初始数据模型设计
以下是我重新设计的数据模型:
public class Team { public int Costs { get; set; } public Manager Manager { get; set; } public HashSet<Player> Players { get; set; } private int _operatingCosts; public int OperatingCosts { get { return this._operatingCosts; } private set { // 注意这里是私有setter -- 仅允许在类内部基于自身值重算时赋值 this._operatingCosts = value; } } } public class Manager { public string Name { get; set; } public int Salary { get; set; } } public class Player { public string Name { get; set; } public int Salary { get; set; } }
原C代码会通过函数遍历所有数组、找到对应元素后求和完成计算。在重写的C#代码中,大部分这类计算逻辑十分简单,最适合实现为只读计算属性。
public class Manager { public int MonthlySalary { get { return this.Salary / 12; } } }
基于INotifyPropertyChanged的缓存方案
难点出现在球队运营总成本的计算场景。这类计算可能逻辑复杂,我不希望每次访问都执行运算,因此需要缓存计算值,仅在事件触发时重新计算。我们明确知晓哪些变量变更会导致结果变化,因此可以在依赖属性的setter中触发事件。
public class Team : INotifyPropertyChanged { private int _costs; public int Costs { get { return this._costs; } set { this._costs = value; NotifyPropertyChanged(); // 成本变更时触发重算 } } public int Name { get; set; // 不需要实现INotifyPropertyChanged;缓存值不依赖该属性 } private int _totalOperatingCost; public int TotalOperatingCost { get { return this._totalOperatingCost; } private set { this._totalOperatingCost = value; } } protected void RecalculateCosts() { this._totalOperatingCost = this.Team.Cost + this.Manager.Salary + this.Players.Sum(p => p.Salary); } }
基于INotifyPropertyChanged的实现目前运行良好:我在会触发重算的字段上触发PropertyChanged事件,重算逻辑统一收敛在一处,没有问题。
核心卡点:子对象变更无法通知父级
现在问题出现了:如果经理的薪资发生变更要如何处理?
public class Manager : INotifyPropertyChanged { private int _salary; public int Salary { get { return this._salary; } set { this._salary = value; NotifyParentPropertyChanged(); } } }
Manager类并不持有其所属Team的引用。当父类已经持有子类引用时,在子类中保留指向父类的反向引用很容易引发各类错误。如果我在此处调用NotifyPropertyChanged(),Manager只会更新自身的计算逻辑,完全不知道需要通知父类更新。这个问题理论上可以通过事件解决,但存在一个明显缺陷:
public class Team { public string Name { ... } private Manager _manager; public Manager Manager { get { return this._manager; } set { if (this._manager is not null) { this._manager.PropertyChanged -= ProcessParentPropertyChangedEvent; } this._manager = value; NotifyPropertyChanged(); if (this._manager is not null) { this._manager.PropertyChanged += ProcessParentPropertyChangedEvent; } } } public Team() { if (this.Manager is not null) { this.Manager.PropertyChanged += ProcessParentPropertyChangedEvent; } } public static void ProcessParentPropertyChangedEvent(object sender, EventArgs e) { // ^^^^^^ 这是静态方法,无法访问当前实例的this // // 当Manager.Salary触发该方法时,sender类型是Manager,事件参数e只能携带Manager的信息,完全不知道所属Team的存在 // // 这个方法里没有任何途径能获取当前经理所属的Team实例 } } public class Manager { private int _salary; public int Salary { get { return this._salary; } set { this._salary = value; NotifyParentPropertyChanged(); } } }
事件可以正常触发,但由于回调方法是static类型,无法获取到刚更新了经理薪资的Team实例引用。Player类也存在类似问题:由于重算逻辑位于静态方法中,无法通知Team类有球员的薪资发生了变更。
除此之外,球队新增或移除球员时也需要触发球队成本重算。我曾尝试使用INotifyCollectionChanged实现该逻辑,但没有成功——最终还是会落到静态方法中,无法获取需要应用变更的目标实例信息。
已评估的可选方案
我认为目前可选方案如下:
- 加入指向父元素的反向引用。该方案看起来实现直接,但存在很多潜在风险,垃圾回收就是其中之一。内存中预计会有近100万个元素,我不确定这种全双向连接图的结构性能表现如何。
- 中介者(Mediator)模式。我研究过该模式,但认为它需要大量容易出错的登记逻辑,无法很好解决问题。同时该模式要求在所有数据元素中注入Mediator类,还需要为.NET基础泛型集合类型编写扩展——我不希望被迫使用自定义集合类型替代常用的
List<Player>,尤其是如果忘记使用自定义集合,会出现重算值错误这类隐蔽且无报错的bug。
我还有什么可选方案?难道必须在整个模型中添加大量反向引用吗?
是否存在我不了解的其他事件处理模型?我之前一直使用INotifyPropertyChanged是因为在WPF开发中已经习惯了该接口,而且它的性能非常好——在我的机器上执行100万次调用仅需60ms。不过我不确定是否存在其他事件处理范式可以解决这类场景。
这类问题看起来非常基础,肯定早就有人遇到并提出了解决方案,非常感谢大家的解答!
内容的提问来源于stack exchange,提问作者James Cronen

