问题描述
在处理本机代码互操作时,我决定是时候学习和尝试 C# 语言的新 Span 功能了。尽管进行了多次试验,但一切都非常顺利,直到我进入我的非常长的函数的最后阶段,我在下面插入了一个最小的可重现样本:
[DllImport(dll,SetLastError = true)]
internal static extern void GetNativeData(out byte lpBuffer,int size,out bytesRead);
ReadOnlySpan<T> ReadArray<T>(ReadOnlySpan<byte> buf,int Length) where T : unmanaged
{
var size = Length * Unsafe.SizeOf<T>();
if (buf.Length < size)
buf = new byte[size];
GetNativeData(out MemoryMarshal.GetReference(buf),size,out int read));
Dh.CreateError(ReadMemoryErr);
return MemoryMarshal.Cast<byte,T>(buf.Slice(0,size));
}
static IEnumerable<MyClass> GetResult()
{
// Here I allocate a buffer
Span<byte> buf = new byte[1000];
// After a long serie of calls to unmanaged DLL functions I end up with something like this:
ReadOnlySpan<uint> uintRes = data.ReadArray<uint>(buf,10);
ReadOnlySpan<ushort> shortRes = data.ReadArray<ushort>(buf,10);
for (int i = 0; i < uintRes.Length; i++)
{
// Any access to spans inside this loop result in Error CS4013
string r = GetFunRes(uintRes[i]);
IntPtr r2 = GetFunRes2(shortRes[i]);
yield return new MyClass() { Prop1 = r,Prop2 = r2 };
}
}
我得到的错误是
错误 CS4013:“跨度”类型的实例不能用于 嵌套函数、查询表达式、迭代器块或异步方法
现在,我已经读到有 workarounds 用于此。这些文章仅显示了异步方法的用法,但也说明了适用于迭代器的用法。不幸的是,我无法完成这项工作。我只需要读取跨度的特定元素,然后产生不包含任何元素或对跨度的引用的结果。只是无论我尝试什么,只要我尝试访问某些内容,编译器就会失败。
我已经阅读了关于 Memory<T>
的内容,可能这个方法可以工作,但我有一些担忧,因为我已经阅读了性能显着降低的信息。人们还首先推荐 Span。我希望我能找到解决方案,否则我将不得不从头开始我的项目并重写所有内容,因为现在与 Span 紧密相关。
预先感谢您的帮助
@00110001:
var uintRes = data.ReadArray<uint>(buf,10).ToArray();
我知道这会起作用,但我认为它会使 Span 的使用以及我试图利用的新泛型非托管功能 (ReadArray<T>
) 变得无用。如果我没记错的话,调用 ToArray() 与旧的编组样式相同,每次调用都会创建一个新副本:
internal static extern void GetNativeData(out uint[] lpBuffer,out bytesRead);
int len = uintRes.Length;
for (int i = 0; i < len; i++)
{
var res = ParseData(i);
if (res == ExpectedResult())
yield return res;
}
MyClass ParseData(int index)
{
// CS8175: Cannot use ref local 'uintRes,shortRes' inside an anonymous method,lambda expression,or query expression
string r = GetFunRes(uintRes[index]);
IntPtr r2 = GetFunRes2(shortRes[index]);
return new MyClass() { Prop1 = r,Prop2 = r2 };
}
解决方法
我要将问题标记为已结束,因为我通过阅读此 article 自己找到了答案。基本上,为了将 IEnumerable 与 yield 一起使用,需要 Memory<T>
,因为 Span<T>
是一个 ref 结构,它在堆栈上分配并且不能跨越 yield 边界使用。另一方面,Memory<T>
可以驻留在堆上,同时仍提供 Span<T>
访问。这篇文章的作者展示了使用纯 Span 可以完成的最大值以及使用 LINQ 启用 IEnumerable 所需的示例代码以及不同实现的基准测试。 IEnumerable+memory<T>
与其他解决方案相比非常慢,但对于仍然想要 LINQ 的任何人,我建议查看 NetFabric.Hyperlinq,这是由文章的同一作者开发的 LINQ 替代版本,以填补那个差距。我不确定即使使用此解决方案,for/foreach 循环与纯 Span 的结合是否仍然存在很大差异,但我认为这主要取决于调用次数、查询类型等(请参阅 LinqBenchmarks 以了解每个查询的执行方式)。
谢谢
附言我注意到有时托管文章的服务器无法访问,因此我在此处添加了 web archive capture 以确保将来始终可以访问。