CS8176:迭代器不能有按引用的局部变量

fir*_*rda 3 c# ref enumerator

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

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;

Run Code Online (Sandbox Code Playgroud)

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

我现在看到两个选项:

  1. 实现自定义迭代器/枚举器,mscs/Roslyn 现在还不够好,无法看清并完成其工作。(这里没有责怪,我能理解,不是那么重要的功能。)
  2. 放弃优化,只需将其索引两次(一次为name,第二次为next)。也许编译器会得到它并产生最佳的机器代码。(我正在为 Unity 创建脚本引擎,这确实对性能至关重要。也许它只检查一次边界并在下次免费使用类似 ref/指针的访问。)

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

我所理解的:

ref var p不能现场后yield return,因为编译器正在建设中的迭代器-一个状态机,ref不能被传递到下一个IEnumerator.MoveNext()。但这里的情况并非如此。

我不明白的是:

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

预期答案:

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

如果您想要/需要更多上下文,则适用于该项目: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)
        {
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Cod*_*ter 6

ref 不能传递给下一个 IEnumerator.MoveNext()。但这里的情况并非如此。

编译器创建一个状态机类来保存运行时所需的数据,以继续下一次迭代。这是该类不能包含一个参考成员。

编译器可以检测到变量只在有限的范围内需要,而不需要添加到该状态类中,但正如 Marc 在他们的回答中所说,这是一个昂贵的功能,几乎没有额外的好处。请记住,功能从 -100 点开始。所以你可以要求它,但一定要解释它的用途。

对于它的价值,对于此设置,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;
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

结果:

|                    Method |      Mean |    Error |   StdDev |
|-------------------------- |----------:|---------:|---------:|
| EnumerateRefLocalFunction |  94.83 ns | 0.138 ns | 0.122 ns |
|                 Enumerate |  98.00 ns | 0.285 ns | 0.238 ns |
Run Code Online (Sandbox Code Playgroud)


Mar*_*ell 5

编译器想用局部变量作为字段重写迭代器块,以保留状态,并且不能将引用类型作为字段。是的,你是对的,它没有越过yield技术上它可能会被重写以根据需要重新声明它,但这使得人类记住非常复杂的规则,其中简单的更改会破坏代码。一揽子“不”更容易理解。

这种情况下的解决方法(或类似的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;
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

或者

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

local 函数不受迭代器块规则的约束,因此您可以使用 ref-locals。