提问人:Shadowblitz16 提问时间:8/7/2021 更新时间:8/9/2021 访问量:156
是否可以用 ref getter 编写脏标志?
Is it possible to write dirty flags with ref getters?
问:
是否可以在 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# 中使用返回的 ref get only 属性编写脏标志?
确实如此,从技术上讲,您确实成功地实现了可以做到这一点的东西。
话虽如此,但是,启动 ,让它由 调度,并检查值是否已更改,会给自己带来许多问题。Task
TaskScheduler
通常,如果它们有效,我不会对实施细节发表意见。但是,实现此功能的方式将导致争用条件和意外行为,这些行为可能会给用户带来灾难性的错误和/或非常难以调试计时和其他同步问题。
对于额外的背衬字段的低价,我们可以完全消除。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)
*/
评论
ByRef<>.Value
Task
_dirty
Task
Node2D
个任务
。所以就是这样Value