【问题标题】:CS8176: Iterators cannot have by-reference localsCS8176:迭代器不能有按引用的局部变量
【发布时间】:2020-09-03 17:33:14
【问题描述】:

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

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 是不区分大小写,并且添加了这个 linked-list 以通过区分大小写的搜索解决冲突,并回退到第一)。

我现在看到两个选项:

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

也许是 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)
        {
        }
    }
}

【问题讨论】:

    标签: c# ref enumerator


    【解决方案1】:

    引用不能传递给下一个 IEnumerator.MoveNext()。但这里不是这样。

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

    编译器可以检测到该变量仅在有限范围内需要,不需要添加到该状态类中,但正如 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 |
    

    【讨论】:

      【解决方案2】:

      编译器想用局部变量作为字段重写迭代器块,以保留状态,而您不能将引用类型作为字段。是的,你是对的,它没有跨越 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;
                  }
              }
          }
      

          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);
                  }
              }
          }
      

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

      【讨论】:

      • @firda 这取决于你;我希望它会“按设计”关闭,并且:我支持关闭
      • 我希望它是现在不值得追求的极端情况,但支持这种关闭?好的,我预计 积压。 (PS:我接受,甚至不会尝试)
      • @firda 因为编译器可以证明的案例数量有限,而且对人类来说很复杂
      猜你喜欢
      • 2012-12-24
      • 2016-06-04
      • 2020-11-22
      • 1970-01-01
      • 1970-01-01
      • 2016-09-06
      • 1970-01-01
      • 2021-06-28
      • 1970-01-01
      相关资源
      最近更新 更多