需要一种解决方法来访问返回 IEnumerable 的函数内的 ReadOnlySpan<T>

net*_*fan 8 c# ienumerable yield-return .net-core

在处理本机代码互操作性时,我决定是时候学习和尝试 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 };
    }
}
Run Code Online (Sandbox Code Playgroud)

我得到的错误是

错误 CS4013:“Span”类型的实例不能在嵌套函数、查询表达式、迭代器块或异步方法中使用

现在,我读到有解决方法。这些文章仅显示了异步方法的用法,但也说明了它适用于迭代器。不幸的是我没能完成这项工作。我只需要读取跨度的特定元素,然后生成不包含任何元素或对跨度的引用的结果。只是无论我尝试做什么,只要我尝试访问某些内容,编译器就会失败。

我读过有关 的内容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, int size, out bytesRead);

@Ian Kemp 这就是我尝试文章中提到的解决方法的方法:

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 };
}
Run Code Online (Sandbox Code Playgroud)

net*_*fan 21

要点就在这篇文章中。基本上,为了将 IEnumerable 与 Yield 一起使用,Memory<T>需要使用 IEnumerable ,因为Span<T>是在堆栈上分配的 ref 结构,并且不能跨 Yield 边界使用。

另一方面,Memory<T>可以驻留在堆上,同时仍然提供Span<T>访问。本文作者展示了使用纯 Span 可以实现的最大功能,以及使用LINQ启用 IEnumerable 所需的示例代码以及不同实现的基准。

IEnumerable+memory<T>与其他解决方案相比非常慢,但对于仍然需要 LINQ 的任何人,我建议查看NetFabric.Hyperlinq,它是由本文同一作者开发的 LINQ 替代版本,以填补这一空白。我不确定即使使用此解决方案,与 for/foreach 循环与纯 Span 相结合是否仍会存在很大差异,但我认为这主要取决于调用次数、查询类型等(请参阅LinqBenchmarks以了解每个循环的情况)查询执行)。

Ps 如果文章无法访问,则有网络存档捕获

  • 感谢您记录您找到的解决方案并提供有用的资源!如果您不需要其他答案,请随时将您自己的答案标记为已接受的答案,以便问题“关闭”:) (3认同)