是否可以使用 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)
*/
是否可以在 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)
*/