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

Is it possible to write dirty flags with ref getters?

提问人:Shadowblitz16 提问时间:8/7/2021 更新时间:8/9/2021 访问量:156

问:

是否可以在 C# 中使用返回的 ref get only 属性编写脏标志?

    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。

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

c# 结构 可变

评论

0赞 8/7/2021
为什么在火焰中吸气剂会旋转?!只是为了检查它是否脏?设置二传手肯定更有效率吗?另外,我讨厌在调试器中运行您的代码,因为如果您碰巧在 AutosLocalsWatch 窗口中有一个实例,它很可能会发送垃圾邮件ByRef<>.ValueTask_dirtyTaskNode2D
0赞 Shadowblitz16 8/7/2021
@MickyD 它检查修改以设置脏标志。此外,ref 属性不能有 setter,即使可以,也可以更改该值并直接分配给它,因为它是一个 ref
0赞 Flydog57 8/7/2021
这两项任务有意义吗?
0赞 Shadowblitz16 8/7/2021
@Flydog57 是的,所以我既可以有可变的 X 和 Y 组件,也可以有一个脏标志
1赞 8/7/2021
“它要检查修改以设置肮脏的标志......我没有看到设计问题“——因此,如果每秒只更新一次,而其余的代码仅每毫秒读取一次,您将在 1 秒的间隔内启动 1,000 个任务。所以就是这样Value

答:

1赞 DekuDesu 8/9/2021 #1

是否可以在 C# 中使用返回的 ref get only 属性编写脏标志?

确实如此,从技术上讲,您确实成功地实现了可以做到这一点的东西。

话虽如此,但是,启动 ,让它由 调度,并检查值是否已更改,会给自己带来许多问题。TaskTaskScheduler

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

对于额外的背衬字段的低价,我们可以完全消除。TaskScheduler

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

ref x = ref node.Rotation;

你基本上说的是,“转到节点,然后转到属性 Rotation,然后转到字段 _rotation,返回存储_rotation的托管内存地址。

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

有了这些知识,我们可以得出一种相当可靠的方法来为它们提供 并检查它们是否更改了 .我们可以使用另一个支持字段来存储我们将其提供给他们时的副本。稍后,如果我们想知道对象是否“脏”,我们只需将 的当前值与之前存储的值进行比较。如果它们不同,我们知道调用方更改了我们给他们的值。(这是假设没有其他调用方同时访问它,如果是这种情况,我们将知道它是否更改,但不知道哪个调用者更改了它,以及其他具有托管内存的怪癖)。&address&address&address&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)
*/

评论

0赞 Shadowblitz16 8/12/2021
这正是我一直在寻找的答案类型,谢谢。