CS8176:迭代器不能有局部引用

CS8176: Iterators cannot have by-reference locals

给定代码中是否存在此错误的真正原因,或者只是 在跨交互步骤需要引用的一般用法中可能会出错(这在这种情况)?

IEnumerable<string> EnumerateStatic()
{
    foreach (int i in dict.Values)
    {
        ref var p = ref props[i]; //< CS8176: Iterators cannot have by-reference locals
        int next = p.next;
        yield return p.name;
        while (next >= 0)
        {
            p = ref props[next];
            next = p.next;
            yield return p.name;
        }
    }
}

struct Prop
{
    public string name;
    public int next;
    // some more fields like Func<...> read, Action<..> write, int kind
}
Prop[] props;
Dictionary<string, int> dict;

dict 是名称索引映射,不区分大小写
Prop.next 指向 下一个要迭代的节点(-1 作为终止符;因为 dict 不区分大小写并且此 链接添加了 -list 以通过区分大小写的搜索解决冲突,回退到第一个)。

我现在看到两个选项:

  1. 实施自定义 iterator/enumerator,mscs/Roslyn 现在还不够好,无法看清并做好它的工作。 (这里不怪我,我能理解,没那么重要的功能。)
  2. 放弃优化并仅对其进行两次索引(一次用于 name,第二次用于 next)。也许编译器会得到它并生成最佳机器代码。 (我正在为 Unity 创建脚本引擎,这确实对性能至关重要。也许它只检查一次边界并在下次免费使用 ref/pointer-like 访问。)

也许是 3。(2b, 2+1/2) 只需复制结构 (x64 上的 32B,三个对象引用和两个整数,但可能会增长,看不到未来).可能不是很好的解决方案(我要么关心并编写迭代器,要么它和 2 一样好。)

我理解的是:

ref var p 不能在 yield return 之后存在,因为编译器正在构建迭代器 - 状态机,ref 不能传递给下一个 IEnumerator.MoveNext()。但这里不是这样。

不明白的地方:

为什么要强制执行这样的规则,而不是尝试实际生成 iterator/enumerator 来查看这样的 ref var 是否需要越界(这里不需要)。或任何其他看起来可行的工作方式(我确实理解我想象的更难实现并期望答案是:罗斯林人有更好的事情要做。再一次,没有冒犯,完全有效的答案.)

预期答案:

  1. 是的,也许在未来/不值得(创建一个 Issue - 如果你觉得它值得,就会这样做)。
  2. 有更好的方法(请分享,我需要解决方案)。

如果你want/need更多上下文,这是针对这个项目的:https://github.com/evandisoft/RedOnion/tree/master/RedOnion.ROS/Descriptors/Reflect(Reflected.cs和Members.cs)

可重现的例子:

using System.Collections.Generic;

namespace ConsoleApp1
{
    class Program
    {
        class Test
        {
            struct Prop
            {
                public string name;
                public int next;
            }
            Prop[] props;
            Dictionary<string, int> dict;
            public IEnumerable<string> Enumerate()
            {
                foreach (int i in dict.Values)
                {
                    ref var p = ref props[i]; //< CS8176: Iterators cannot have by-reference locals
                    int next = p.next;
                    yield return p.name;
                    while (next >= 0)
                    {
                        p = ref props[next];
                        next = p.next;
                        yield return p.name;
                    }
                }
            }
        }
        static void Main(string[] args)
        {
        }
    }
}

编译器想要用局部变量重写迭代器块作为字段,以保留状态,而你不能有ref-types作为字段。是的,你是对的,它没有越过 yield,所以从技术上讲,它 可以 可能被重写为 re-declare 它 as-needed,但是使得 非常 复杂的规则供人类记住,其中 simple-looking 更改破坏了代码。一个笼统的“不”更容易理解。

这种情况下的解决方法(或与 async 方法类似)通常是辅助方法;例如:

    IEnumerable<string> EnumerateStatic()
    {
        (string value, int next) GetNext(int index)
        {
            ref var p = ref props[index];
            return (p.name, p.next);
        }
        foreach (int i in dict.Values)
        {
            (var name, var next) = GetNext(i);
            yield return name;
            while (next >= 0)
            {
                (name, next) = GetNext(next);
                yield return name;
            }
        }
    }

    IEnumerable<string> EnumerateStatic()
    {
        string GetNext(ref int next)
        {
            ref var p = ref props[next];
            next = p.next;
            return p.name;
        }
        foreach (int i in dict.Values)
        {
            var next = i;
            yield return GetNext(ref next);
            while (next >= 0)
            {
                yield return GetNext(ref next);
            }
        }
    }

局部函数不受iterator-block规则的约束,所以可以使用ref-locals.

the ref cannot be passed to next IEnumerator.MoveNext(). But that is not the case here.

编译器创建一个状态机class来保存运行时需要的数据以继续下一次迭代。 class 不能包含 ref 成员。

编译器可以检测到该变量仅在有限范围内需要,不需要添加到该状态class,但正如 Marc 在他们的回答是,这是一项昂贵的功能,却几乎没有额外的好处。记住,features start at -100 points。所以你可以要求它,但一定要解释它的用途。

就其价值而言,对于此设置,Marc 的版本快了约 4%(根据 BenchmarkDotNet):

public class StructArrayAccessBenchmark
{
    struct Prop
    {
        public string name;
        public int next;
    }

    private readonly Prop[] _props = 
    {
        new Prop { name = "1-1", next = 1 }, // 0
        new Prop { name = "1-2", next = -1 }, // 1

        new Prop { name = "2-1", next = 3 }, // 2
        new Prop { name = "2-2", next = 4 }, // 3
        new Prop { name = "2-2", next = -1 }, // 4
    };

    readonly Dictionary<string, int> _dict = new Dictionary<string, int>
    {
        { "1", 0 },
        { "2", 2 },
    };

    private readonly Consumer _consumer = new Consumer();

    // 95ns
    [Benchmark]
    public void EnumerateRefLocalFunction() => enumerateRefLocalFunction().Consume(_consumer);

    // 98ns
    [Benchmark]
    public void Enumerate() => enumerate().Consume(_consumer);

    public IEnumerable<string> enumerateRefLocalFunction()
    {
        (string value, int next) GetNext(int index)
        {
            ref var p = ref _props[index];
            return (p.name, p.next);
        }

        foreach (int i in _dict.Values)
        {
            var (name, next) = GetNext(i);
            yield return name;

            while (next >= 0)
            {
                (name, next) = GetNext(next);
                yield return name;
            }
        }
    }

    public IEnumerable<string> enumerate()
    {
        foreach (int i in _dict.Values)
        {
            var p = _props[i];
            int next = p.next;
            yield return p.name;
            while (next >= 0)
            {
                p = _props[next];
                next = p.next; 
                yield return p.name;
            }
        }
    }

结果:

|                    Method |      Mean |    Error |   StdDev |
|-------------------------- |----------:|---------:|---------:|
| EnumerateRefLocalFunction |  94.83 ns | 0.138 ns | 0.122 ns |
|                 Enumerate |  98.00 ns | 0.285 ns | 0.238 ns |