如何实现可感知依赖属性变更的只读计算属性?
解决多层依赖下计算属性重复计算的问题
这问题太典型了——每次调用TotalPoints都全量遍历计算,在多层依赖、数据量大的时候确实会拖慢性能。我给你几个实用的解决思路,从手动实现到框架辅助都有:
1. 手动实现:脏标记+观察者模式(最灵活)
核心思路是:只在依赖项变化时标记计算结果为“脏”,下次获取TotalPoints时再重新计算,平时直接返回缓存值。
步骤拆解:
- 给
Subject类添加属性变更事件,当Points修改时触发通知 Student类中维护一个脏标记(_totalPointsDirty),一旦收到Subject的变更通知,或者Subject集合有增减,就把标记设为trueTotalPoints的getter先检查脏标记:如果是true就重新计算并更新缓存,然后重置标记;否则直接返回缓存值
代码示例:
public class Subject { private int _points; public int Points { get => _points; set { if (_points != value) { _points = value; // 触发变更通知 PointsChanged?.Invoke(); } } } // 定义变更事件 public event Action PointsChanged; } public class Student { private readonly List<Subject> _subjects = new List<Subject>(); private int _totalPoints; private bool _totalPointsDirty = true; public IReadOnlyList<Subject> Subjects => _subjects.AsReadOnly(); public int TotalPoints { get { if (_totalPointsDirty) { RecalculateTotalPoints(); _totalPointsDirty = false; } return _totalPoints; } } // 添加科目时订阅变更事件 public void AddSubject(Subject subject) { _subjects.Add(subject); subject.PointsChanged += OnSubjectPointsChanged; _totalPointsDirty = true; } // 移除科目时取消订阅 public void RemoveSubject(Subject subject) { if (_subjects.Remove(subject)) { subject.PointsChanged -= OnSubjectPointsChanged; _totalPointsDirty = true; } } // 收到科目分数变更时标记脏值 private void OnSubjectPointsChanged() { _totalPointsDirty = true; } // 实际计算逻辑 private void RecalculateTotalPoints() { _totalPoints = 0; foreach (var subject in _subjects) { _totalPoints += subject.Points; } } }
2. 借助框架减少样板代码
如果不想手动写事件订阅和脏标记,可以用成熟的MVVM框架(比如CommunityToolkit.Mvvm),它们内置了依赖属性追踪和计算属性支持,能大大减少重复代码。
代码示例(用CommunityToolkit.Mvvm):
using CommunityToolkit.Mvvm.ComponentModel; using System.Collections.ObjectModel; using System.Linq; public partial class Subject : ObservableObject { // 自动生成带变更通知的Points属性 [ObservableProperty] private int points; } public partial class Student : ObservableObject { private readonly ObservableCollection<Subject> _subjects = new ObservableCollection<Subject>(); public ObservableCollection<Subject> Subjects => _subjects; // 计算属性,依赖Subjects中所有的Points public int TotalPoints => Subjects.Sum(s => s.Points); public Student() { // 监听科目集合变化 _subjects.CollectionChanged += (s, e) => { // 新加入的科目要监听其属性变化 if (e.NewItems != null) foreach (Subject sub in e.NewItems) sub.PropertyChanged += (_, __) => OnPropertyChanged(nameof(TotalPoints)); // 移除的科目要取消监听 if (e.OldItems != null) foreach (Subject sub in e.OldItems) sub.PropertyChanged -= (_, __) => OnPropertyChanged(nameof(TotalPoints)); // 集合变化时通知TotalPoints更新 OnPropertyChanged(nameof(TotalPoints)); }; } }
框架会帮你处理属性变更的通知逻辑,不用手动维护脏标记。
3. 即时更新(性能最优)
如果对性能要求极高,甚至不想等到获取TotalPoints时才计算,可以在依赖项变化时即时更新总分数,而不是延迟计算。
比如修改Subject的事件,传递旧值和新值,这样Student可以直接更新差值,不用全量遍历:
public class Subject { private int _points; public int Points { get => _points; set { if (_points != value) { int oldValue = _points; _points = value; PointsChanged?.Invoke(oldValue, value); } } } // 传递旧值和新值的事件 public event Action<int, int> PointsChanged; } // Student类中对应的处理逻辑 private void OnSubjectPointsChanged(int oldPoints, int newPoints) { // 直接更新差值,不用重新遍历所有科目 _totalPoints += newPoints - oldPoints; }
这种方式适合科目数量多、分数频繁变化的场景,能把计算开销降到最低。
内容的提问来源于stack exchange,提问作者Siow Chee MIng
相关产品推荐
相关产品推荐

