需要一种解决方法来访问返回 IEnumerable

问题描述

在处理本机代码互操作时,我决定是时候学习和尝试 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 以确保将来始终可以访问。