You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

游戏开发中,能否实现Add操作无内存分配的INotifyCollectionChanged?

解决方案

保留INotifyCollectionChanged接口的优化方案

1. 复用自定义单元素列表,避免SingleItemReadOnlyList分配

.NET的NotifyCollectionChangedEventArgs在传入单个元素时会自动创建SingleItemReadOnlyList,这是主要的GC来源。你可以实现一个可复用的单元素只读列表,然后使用接受IList参数的构造函数创建EventArgs,避免每次创建新的只读列表:

public class ReusableSingleItemList<T> : IList<T>
{
    public T CurrentItem { get; set; }

    public T this[int index]
    {
        get => index == 0 ? CurrentItem : throw new IndexOutOfRangeException();
        set => throw new NotSupportedException();
    }

    public int Count => 1;
    public bool IsReadOnly => true;

    // 实现IList的其他方法,所有修改操作抛出NotSupportedException
    public void Add(T item) => throw new NotSupportedException();
    public void Clear() => throw new NotSupportedException();
    public bool Contains(T item) => EqualityComparer<T>.Default.Equals(CurrentItem, item);
    public void CopyTo(T[] array, int arrayIndex) => array[arrayIndex] = CurrentItem;
    public IEnumerator<T> GetEnumerator() => new SingleItemEnumerator(this);
    public int IndexOf(T item) => Contains(item) ? 0 : -1;
    public void Insert(int index, T item) => throw new NotSupportedException();
    public bool Remove(T item) => throw new NotSupportedException();
    public void RemoveAt(int index) => throw new NotSupportedException();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    private sealed class SingleItemEnumerator : IEnumerator<T>
    {
        private readonly ReusableSingleItemList<T> _list;
        private bool _moved;

        public SingleItemEnumerator(ReusableSingleItemList<T> list) => _list = list;

        public T Current => _moved ? _list.CurrentItem : throw new InvalidOperationException();
        object IEnumerator.Current => Current;

        public bool MoveNext()
        {
            if (_moved) return false;
            _moved = true;
            return true;
        }

        public void Reset() => _moved = false;
        public void Dispose() {}
    }
}

使用时池化该列表实例:

private readonly ReusableSingleItemList<YourItemType> _reusableList = new ReusableSingleItemList<YourItemType>();

private void OnItemAdded(YourItemType item, int index)
{
    _reusableList.CurrentItem = item;
    var args = new NotifyCollectionChangedEventArgs(
        NotifyCollectionChangedAction.Add, 
        _reusableList, 
        index);
    CollectionChanged?.Invoke(this, args);
}

2. 反射复用NotifyCollectionChangedEventArgs实例

虽然NotifyCollectionChangedEventArgs的属性是只读的,但可以通过反射修改其内部字段,复用预创建的实例:

// 预创建复用的EventArgs实例
private static readonly NotifyCollectionChangedEventArgs _reusableAddArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, null);
private static readonly NotifyCollectionChangedEventArgs _reusableRemoveArgs = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, null);

// 缓存反射字段(仅初始化一次)
private static readonly FieldInfo _actionField = typeof(NotifyCollectionChangedEventArgs)
    .GetField("_action", BindingFlags.NonPublic | BindingFlags.Instance);
private static readonly FieldInfo _newItemsField = typeof(NotifyCollectionChangedEventArgs)
    .GetField("_newItems", BindingFlags.NonPublic | BindingFlags.Instance);
private static readonly FieldInfo _oldItemsField = typeof(NotifyCollectionChangedEventArgs)
    .GetField("_oldItems", BindingFlags.NonPublic | BindingFlags.Instance);
private static readonly FieldInfo _newIndexField = typeof(NotifyCollectionChangedEventArgs)
    .GetField("_newStartingIndex", BindingFlags.NonPublic | BindingFlags.Instance);
private static readonly FieldInfo _oldIndexField = typeof(NotifyCollectionChangedEventArgs)
    .GetField("_oldStartingIndex", BindingFlags.NonPublic | BindingFlags.Instance);

// 触发添加事件时复用
private void RaiseAddEvent(YourItemType item, int index)
{
    _actionField.SetValue(_reusableAddArgs, NotifyCollectionChangedAction.Add);
    _newItemsField.SetValue(_reusableAddArgs, new[] { item }); // 可结合上面的复用列表进一步优化
    _newIndexField.SetValue(_reusableAddArgs, index);
    CollectionChanged?.Invoke(this, _reusableAddArgs);
}

注意:该方法依赖.NET内部实现细节,不同版本(如.NET Framework/.NET Core/.NET 5+)的字段名可能变化,需针对目标平台测试。

3. 批量合并变更

如果一帧内有多次增删操作,可收集这些变更,在帧末触发一次批量变更事件,减少EventArgs的创建次数:

private readonly List<YourItemType> _addedItems = new List<YourItemType>();
private readonly List<YourItemType> _removedItems = new List<YourItemType>();

public void AddItem(YourItemType item)
{
    _items.Add(item);
    _addedItems.Add(item);
}

public void RemoveItem(YourItemType item)
{
    _items.Remove(item);
    _removedItems.Add(item);
}

// 在帧末调用该方法
public void FlushChanges()
{
    if (_addedItems.Count > 0)
    {
        var args = new NotifyCollectionChangedEventArgs(
            NotifyCollectionChangedAction.Add, 
            _addedItems);
        CollectionChanged?.Invoke(this, args);
        _addedItems.Clear();
    }
    if (_removedItems.Count > 0)
    {
        var args = new NotifyCollectionChangedEventArgs(
            NotifyCollectionChangedAction.Remove, 
            _removedItems);
        CollectionChanged?.Invoke(this, args);
        _removedItems.Clear();
    }
}

若允许逻辑接受全量刷新,也可直接触发Reset事件,完全避免多次变更的分配,但会导致订阅者重新遍历整个集合,适合集合规模不大的场景。

自定义变更通知方案

如果上述优化仍无法满足内存要求,可放弃INotifyCollectionChanged,改用自定义的变更事件和可复用的EventArgs:

public class CustomCollectionChangeArgs<T> : EventArgs
{
    public NotifyCollectionChangedAction Action { get; set; }
    public T Item { get; set; }
    public int Index { get; set; }

    // 重置方法,用于实例复用
    public void Reset(NotifyCollectionChangedAction action, T item, int index)
    {
        Action = action;
        Item = item;
        Index = index;
    }
}

public class YourPooledCollection<T>
{
    private readonly ObjectPool<CustomCollectionChangeArgs<T>> _argsPool = new DefaultObjectPool<CustomCollectionChangeArgs<T>>(new DefaultPooledPolicy<CustomCollectionChangeArgs<T>>());

    public event EventHandler<CustomCollectionChangeArgs<T>> CustomCollectionChanged;

    private void RaiseCustomEvent(NotifyCollectionChangedAction action, T item, int index)
    {
        var args = _argsPool.Get();
        args.Reset(action, item, index);
        CustomCollectionChanged?.Invoke(this, args);
        _argsPool.Return(args);
    }
}

这种方案完全消除GC分配,但需要修改所有订阅变更的代码,不再兼容依赖INotifyCollectionChanged的组件(如WPF绑定)。

内容的提问来源于stack exchange,提问作者Victor Chelaru

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 16:01:29