我正在尝试实现 C# 7.3 中引入的新模式,该模式支持使用fixed
语句固定自定义类型。请参阅有关文档的文章
但是,我担心在下面的代码中,我返回指向字符串的指针,然后离开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(之后,您可以执行以下操作:
var strSpan = _String.AsSpan();
return ref strSpan[_Index];
正如函数名称所指定的那样,这将返回对基础字符串的第一个字符的可固定引用。 您在上面得到的内容返回对指向基础字符串的固定指针的第一个取消引用元素的引用,但固定的生存期仅限于固定语句的范围(可能是错误的,但我认为返回的 ref 不会将其固定(。 对 Span 元素使用 ref 可避免不必要的固定。
更新 - 我更多地研究了固定的指针(一堆没有成果的谷歌搜索 - 回退到反编译/实验(。 事实证明,将引用返回到固定指针的元素不会保留引脚。 返回的引用是对已解析地址的引用 - 固定的所有上下文都将丢失。
我想也许将_String
更改为Memory<char>
(在构造函数中使用AsMemory
扩展(可以工作。 然后,您的GetPinnableReference
可以return ref _String[0];
但AsMemory
为您提供只读内存对象(无法返回可写引用(。
如果没有使用GCHandle
和IDisposable
或MemoryManager<T>
(肯定比您想要的重(实现IPinnable
,我认为我上面的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
表示的 fixed 关键字,由char* V_0
表示的指向 char(来自代码(的指针,以及由char& V_2
表示的返回引用。 由三个单独的代码构造表示的三个独立概念。
因此,您的 fixed 语句固定字符串并将固定引用分配给V_1
atIL_0002
到IL_0007
,然后加载V_1
将其转换为指针(转换为无符号整数(并将其存储在V_0
(您的char* p
(中,IL_0008
到IL_000a
,最后它加载V_0
,计算_index
指定的char
的偏移量, 并将其存放在V_0
中,IL_000e
IL_0015
。 在固定语句的范围内,它加载V_0,通过Unsave.AsRef<char>
将其转换为引用,并将其存储在IL_0017
到IL_001d
的V_2
(将返回的引用(中。 我不确定 C# 编译器为什么要这样做,但从IL_001e
到IL_0021
的最后一点通常是编译器如何处理返回值;它向将返回值的块插入一个分支(即使在这种情况下它是下一条指令(,然后它从局部变量加载返回值(它创建并 IMO 不必要地分配给它,但也许有一个合乎逻辑的解释?(,最后返回你几条指令前获得的结果。
终于有了答案! 无法维护固定语句中的固定,因为它正在取消引用非托管指针V_0
/p
在这种情况下恰好由固定引用支持,V_1
,这是取消引用指针的说明不知道的事情(。 CLR 团队本可以在 JIT 中很聪明,并通过将 char 引用与固定引用相关联来专门通过分析来处理这种情况,但我认为这在体系结构上很奇怪,难以解释/记录,我怀疑他们走了那条路,所以我没有去查看 CLR 代码进行验证。