是否可以在 C# 中使用返回的 ref 编写脏标志只获取属性?
确实如此,从技术上讲,您确实成功地实现了一些功能。
话虽如此,但启动Task,让TaskScheduler 调度它,然后检查值是否已更改,这会给您带来很多问题。
通常我不会对 实施细节 发表意见,如果它们有效的话。但是您实现此功能的方式将导致竞争条件和意外行为,这可能会给您的用户带来灾难性的错误和/或非常难以调试计时和其他同步问题下线。
以额外支持字段的低廉价格,我们可以完全消除TaskScheduler。
要实现此更改,您必须了解 CLR 如何处理 ref 值类型。例如当你说:
ref x = ref node.Rotation;
你的意思是,“转到node,然后转到属性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)
*/