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 以通过区分大小写的搜索解决冲突,回退到第一个)。
我现在看到两个选项:
- 实施自定义 iterator/enumerator,mscs/Roslyn 现在还不够好,无法看清并做好它的工作。 (这里不怪我,我能理解,没那么重要的功能。)
- 放弃优化并仅对其进行两次索引(一次用于
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
是否需要越界(这里不需要)。或任何其他看起来可行的工作方式(我确实理解我想象的更难实现并期望答案是:罗斯林人有更好的事情要做。再一次,没有冒犯,完全有效的答案.)
预期答案:
- 是的,也许在未来/不值得(创建一个 Issue - 如果你觉得它值得,就会这样做)。
- 有更好的方法(请分享,我需要解决方案)。
如果你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 |
给定代码中是否存在此错误的真正原因,或者只是 在跨交互步骤需要引用的一般用法中可能会出错(这在这种情况)?
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 以通过区分大小写的搜索解决冲突,回退到第一个)。
我现在看到两个选项:
- 实施自定义 iterator/enumerator,mscs/Roslyn 现在还不够好,无法看清并做好它的工作。 (这里不怪我,我能理解,没那么重要的功能。)
- 放弃优化并仅对其进行两次索引(一次用于
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
是否需要越界(这里不需要)。或任何其他看起来可行的工作方式(我确实理解我想象的更难实现并期望答案是:罗斯林人有更好的事情要做。再一次,没有冒犯,完全有效的答案.)
预期答案:
- 是的,也许在未来/不值得(创建一个 Issue - 如果你觉得它值得,就会这样做)。
- 有更好的方法(请分享,我需要解决方案)。
如果你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 |