我正在尝试实现C#7.3中引入的新模式,该模式支持使用fixed
语句固定自定义类型。 See article on the Docs
但是,我担心在下面的代码中我将返回指向字符串的指针,然后离开fixed
范围。当然,整个例程将由将“修复”我的自定义类型的“父” fixed
语句使用,但是我不确定字符串(这是我的自定义类型中的一个字段)是否仍然会保持固定。所以我不知道这整个方法是否行得通。
readonly struct PinnableStruct {
private readonly string _String;
private readonly int _Index;
public unsafe ref char GetPinnableReference() {
if (_String is null) return ref Unsafe.AsRef<char>(null);
fixed (char* p = _String) return ref Unsafe.AsRef<char>(p + _Index);
}
}
上面的代码将被以下示例代码利用:
static void SomeRoutine(PinnableStruct data) {
fixed(char* p = data) {
//iterate over characters in data and do something with them
}
}
答案 0 :(得分:1)
此时无需在字符串或指针上进行固定,实际上这样做在概念上是不正确的。空检查/返回(还应该检查长度0)之后,您可以执行以下操作:
var strSpan = _String.AsSpan();
return ref strSpan[_Index];
正如函数名所指定的那样,它返回对基础字符串的第一个字符的可固定引用。上面的操作返回对固定字符串的第一个取消引用的引用,该固定指针指向基础字符串,但是固定的有效期仅限于固定语句的范围(可能是错误的,但我认为返回的ref使其保持固定)。对Span元素使用ref可以避免不必要的固定。
更新-我进一步研究了固定指针(一堆无效的Google搜索-退回到了反编译/实验)。事实证明,将ref返回到固定指针的元素不会保留该引脚。返回的引用是对已解析地址的引用-固定的所有上下文都将丢失。
我认为也许将_String
更改为Memory<char>
(在构造函数中使用AsMemory
扩展名)是可行的。这样,您的GetPinnableReference
就可以return ref _String[0];
了,但是AsMemory
给了您一个只读的内存对象(无法返回可写引用)。
用IPinnable
和GCHandle
或IDisposable
(比您确定的要重)来实现MemoryManager<T>
的简短内容,我想我上面的Span<char>
解决方案可能是完成此操作的最安全/正确的方法。
这里是对GetPinnableReference的反汇编(不要介意我的PowerPC.Test命名空间-我正在制作.NET Standard 1.1 PowerPC二进制反汇编程序/仿真器/调试器,并大量使用了这些概念,因此引起了我的兴趣):
.method public hidebysig instance char& GetPinnableReference() cil managed
{
// Code size 34 (0x22)
.maxstack 2
.locals init (char* V_0,
string pinned V_1,
char& V_2)
IL_0000: nop
IL_0001: ldarg.0
IL_0002: ldfld string PowerPC.Test.Console.Program/PinnableStruct::_string
IL_0007: stloc.1
IL_0008: ldloc.1
IL_0009: conv.u
IL_000a: stloc.0
IL_000b: ldloc.0
IL_000c: brfalse.s IL_0016
IL_000e: ldloc.0
IL_000f: call int32 [System.Runtime]System.Runtime.CompilerServices.RuntimeHelpers::get_OffsetToStringData()
IL_0014: add
IL_0015: stloc.0
IL_0016: nop
IL_0017: ldloc.0
IL_0018: call !!0& [System.Runtime.CompilerServices.Unsafe]System.Runtime.CompilerServices.Unsafe::AsRef<char>(void*)
IL_001d: stloc.2
IL_001e: br.s IL_0020
IL_0020: ldloc.2
IL_0021: ret
} // end of method SomeInfo::GetPinnableReference
我不希望大多数读者能够阅读IL,但这没关系。我们寻求的答案就在顶部(并在其后的IL中得到确认)。我们有三个本地人:char* V_0
,string pinned V_1
和char& V_2
。对我而言,这为固定语法的含义提供了一点启示。您拥有由string pinned V_1
表示的固定关键字,由char* V_0
表示的char指针(来自您的代码)和由char& V_2
表示的返回引用。由三个独立的代码构造代表的三个独立的概念。
因此,您的固定语句将字符串固定,并在V_1
处将对IL_0002
的固定引用分配给IL_0007
,然后加载V_1
将其转换为指针(转换到无符号整数),并将其存储在V_0
到char* p
的{{1}}(您的IL_0008
)中,最后加载IL_000a
,计算{{由V_0
指定的1}},并将其存储回char
的{{1}}到_index
中。在固定语句的范围内,它将加载V_0,并通过V_0
将其转换为引用,然后将其存储在IL_000e
到{{ 1}}。我不确定为什么C#编译器会这样做,但是从IL_0015
到Unsave.AsRef<char>
的最后一位通常是编译器如何处理返回值的方式。它在要返回该值的块中插入一个分支(即使在这种情况下,这是下一条指令),然后从本地变量(它创建并不必要地分配给IMO)加载返回值,但是也许逻辑解释?),最后返回您之前获得的几条指令的结果。
终于有了答案!无法维护固定语句的固定,因为它解引用了非托管指针V_2
/ IL_0017
,在这种情况下恰好由固定引用IL_001d
作为后盾,解引用指针的指令不知道的东西)。 CLR团队本来可以在JIT中很聪明,并且通过将char引用与固定引用相关联来通过分析来专门处理该案例,但是我认为这样做在结构上很奇怪,难以解释/记录,我怀疑他们这样做了路线,所以我没有去查看CLR代码进行验证。