是否可以使用 ref getter 编写脏标志?

Is it possible to write dirty flags with ref getters?

是否可以在 C# 中使用返回的 ref 只获取属性来编写脏标志?

    public class ByRef<T> where T : struct
    {
        private bool _dirty;
        private T    _value;

        public ref T Value
        {
            get
            {
                var oldValue = _value;
                Task.Run(() => //Possible bad attempt at executing code after return.
                {
                    Task.Delay(TimeSpan.FromTicks(1));
                    if (!_value.Equals(oldValue))
                    {
                        _dirty = true;
                    }
                });
                return ref _value;
            }
        }

        public bool Dirty
        {
            get => _dirty;
            set => _dirty = value;
        }
    }
    public class Node2D : Node
    {
        private ByRef<          float   > _rotation;
        private ByRef<(float X, float Y)> _position;
        private ByRef<(float X, float Y)> _scale;

        public ref           float    Rotation => ref _rotation.Value;
        public ref (float X, float Y) Position => ref _position.Value;
        public ref (float X, float Y) Scale    => ref _scale   .Value;
        
        protected override void OnUpdate(NodeEventArgs args)
        {
            if (_rotation.Dirty || _position.Dirty || _scale.Dirty)
            {
                //Update
            }
        }

我想这样做的主要原因是允许元组中的可变成员,这样我就可以分别修改 X 和 Y。

我也不想每一帧都更新位置、旋转和缩放,所以我想知道是否可以两全其美?

Is it possible to write dirty flags with ref returned get only properties in C#?

确实如此,而且从技术上讲,您确实成功地实现了完全符合该要求的东西。

然而,尽管如此,启动一个 Task,由 TaskScheduler 调度它,并检查该值是否已更改会导致很多问题。

通常我不会对实施细节发表意见,如果它们有效的话。但是您实现此功能的方式 导致竞争条件和 意外行为 这可能会给您的用户带来灾难性的错误 and/or难以在线调试时序和其他同步问题。

我们可以完全消除 TaskScheduler 额外的支持字段的小代价。

要实现此更改,您必须了解 CLR 如何处理 ref 值类型。例如,当您说:

ref x = ref node.Rotation;

你的意思是,“转到 node,然后转到 属性 Rotation,然后转到字段 _rotation, return 存储 _rotation 的托管内存地址。"

这允许您在同一存储位置拥有一个可变结构,这听起来像是您的意图。

有了这些知识,我们可以得出一种相当可靠的方法来为他们提供 &address 并检查他们是否更改了 &address 处的值。我们可以使用另一个支持字段来完成此操作,以存储我们将其提供给他们时 &address 处的内容的副本。稍后,如果我们想知道对象是否为 'dirty',我们只需将 &address 的当前值与我们之前存储的值进行比较。如果它们不同,我们就知道调用者更改了我们给它们的 &address 处的值。 (这是假设没有其他调用者同时访问它,如果是这种情况,我们会知道它是否发生了变化,但不知道是哪个调用者改变了它,以及其他托管内存的怪癖)。

public class ByRef<T> where T : struct
{
    private T _value;
    private T oldValue;

    public ref T Value
    {
        get
        {
            // since the address to the backing field is being accessed we should store a copy of the value of the
            // backing field before it was accessed, so in the future, if dirty is checked, we can determine
            // if the value in the backing field has changed
            oldValue = _value;
            return ref _value;
        }
    }

    public bool Dirty => _value.Equals(oldValue) is false;

    // alternatively if you want the Dirty flag to auto-reset every time it's checked you could do
    public bool Dirty
    {
        get
        {
            bool wasDirty = _value.Equals(oldValue) is false;

            if (wasDirty)
            {
                // since it was dirty, we should now save the current value, so subsequent calls to .Dirty are false
                // this is optional, if this functionality is needed
                oldValue = _value;
            }

            return wasDirty;
        }
    }
}

这个实现可能看起来相当简单,但我们可以测试支持字段可变性的有效性,以获得对象在托管内存中存储的任何地方都发生了就地突变的证据。 (这忽略了不可变结构可能已被 CLR 复制、更改和重新放置到同一地址,但这应该没有什么区别)。

public class Node2D
{
    private ByRef<float> _rotation = new();
    private ByRef<(float x, float y)> _position = new();
    private ByRef<(float X, float Y)> _scale = new();

    public ref float Rotation => ref _rotation.Value;

    public ref (float x, float y) Position => ref _position.Value;

    public ref (float x, float y) Scale => ref _scale.Value;

    public void DumpInfo()
    {
        Console.WriteLine($"Check Dirty Statuses of all Fields");
        Console.WriteLine($"Position ({_position.Dirty}) Rotation ({_rotation.Dirty}) Scale ({_scale.Dirty})");
        Console.WriteLine(string.Empty);

        Console.WriteLine($"Verifying the backing fields have not changed addresses and have not been moved by GC or CLR");
        unsafe
        {
            fixed (float* pointer = &_rotation.Value)
            {
                DumpAddress(nameof(Rotation), (long)pointer, _rotation.Value);
            }
            fixed ((float x, float y)* pointer = &_position.Value)
            {
                DumpAddress(nameof(Position), (long)pointer, _position.Value);
            }
            fixed ((float x, float y)* pointer = &_scale.Value)
            {
                DumpAddress(nameof(Scale), (long)pointer, _scale.Value);
            }
        }
        Console.WriteLine(string.Empty);
    }
    private unsafe void DumpAddress(string Name, long pointer, object Value)
    {
        Console.WriteLine($"{Name}\n\r\t Address:{pointer:X} Value:{Value}");
    }
}

然后我们可以使用它来测试字段是否可变,并且我们有最新的,但不是原子的,关于值是否与上次我们检查过。

// create a node
var node = new Node2D();

// dump initial info for comparison
node.DumpInfo();
/*
Position (False) Rotation (False) Scale (False)
Rotation
         Address: 1F440C8DF10 Value:0
Position
         Address: 1F440C8DF28 Value:(0, 0)
Scale
         Address: 1F440C8DF48 Value:(0, 0)
*/

// access field but do not change value
ref float x = ref node.Rotation;

_ = x * 2;

// check to make sure nothing changed
node.DumpInfo();
/*
Position (False) Rotation (False) Scale (False)
Rotation
         Address: 1F440C8DF10 Value:0
Position
         Address: 1F440C8DF28 Value:(0, 0)
Scale
         Address: 1F440C8DF48 Value:(0, 0)
*/

// change a single field
x = 12f;

// check to make sure the address is still the same, and the value changed
node.DumpInfo();
/*
Position (False) Rotation (True) Scale (False)
Rotation
         Address: 1F440C8DF10 Value: 12
Position
         Address: 1F440C8DF28 Value:(0, 0)
Scale
         Address: 1F440C8DF48 Value:(0, 0)
*/

// change the tuples to ensure they are mutable as well
node.Position.x = 1.22f;
node.Scale.y = 0.78f;

// check to make sure the address is still the same, and the value changed
node.DumpInfo();
/*
Position (True) Rotation (False) Scale (True)
Rotation
         Address:1F440C8DF10 Value:12
Position
         Address:1F440C8DF28 Value:(1.22, 0)
Scale
         Address:1F440C8DF48 Value:(0, 0.78)
*/

// this is optional, but check again to see if the dirty flags have cleared
node.DumpInfo();
/*
Position (False) Rotation (False) Scale (False)
Rotation
         Address:1F440C8DF10 Value:12
Position
         Address:1F440C8DF28 Value:(1.22, 0)
Scale
         Address:1F440C8DF48 Value:(0, 0.78)
*/