Enumerator.MoveNext() 的奇怪行为
Strange behavior of Enumerator.MoveNext()
谁能解释一下为什么这段代码 运行 在无限循环中?为什么总是 MoveNext()
return true
?
var x = new { TempList = new List<int> { 1, 3, 6, 9 }.GetEnumerator() };
while (x.TempList.MoveNext())
{
Console.WriteLine("Hello World");
}
List<T>.GetEnumerator()
returns a mutable value type (List<T>.Enumerator
)。您将该值存储在匿名类型中。
现在,让我们看看它的作用:
while (x.TempList.MoveNext())
{
// Ignore this
}
这相当于:
while (true)
{
var tmp = x.TempList;
var result = tmp.MoveNext();
if (!result)
{
break;
}
// Original loop body
}
现在请注意我们在 MoveNext()
上调用什么 - 匿名类型的值的 copy。您实际上无法更改匿名类型中的值 - 您所拥有的只是一个可以调用的 属性,它将为您提供该值的副本。
如果将代码更改为:
var x = new { TempList = (IEnumerable<int>) new List<int> { 1, 3, 6, 9 }.GetEnumerator() };
... 那么您最终会得到匿名类型的 reference。对包含可变值的框的引用。当您对该引用调用 MoveNext()
时,框中的值将发生变化,因此它会执行您想要的操作。
有关非常相似情况的分析(再次使用 List<T>.GetEnumerator()
),请参阅 my 2010 blog post "Iterate, damn you!"。
虽然 C# 中的 foreach
构造和 VB.NET 中的 For Each
循环通常与实现 IEnumerable<T>
的类型一起使用,但它们将接受任何包含GetEnumerator
方法,其 return 类型提供了合适的 MoveNext
函数和 Current
属性。 GetEnumerator
return 值类型在许多情况下允许 foreach
比 returned IEnumerator<T>
.[=29 更有效地实现=]
不幸的是,因为无法通过一种方式在从 foreach
调用时提供值类型枚举器而不在被 GetEnumerator
方法调用时提供值类型枚举器,作者List<T>
面临性能与语义的轻微权衡。当时,因为 C# 不支持变量类型推断,任何使用从 List<T>.GetEnumerator
编辑的值 return 的代码都必须声明类型为 IEnumerator<T>
或 List<T>.Enumerator
的变量.使用前一种类型的代码表现得好像 List<T>.Enumerator
是引用类型,而使用后者的程序员可以假定意识到它是一种结构类型。但是,当 C# 添加类型推断时,该假设不再成立。代码很容易最终使用类型 List<T>.Enumerator
而程序员不知道该类型的存在。
如果 C# 曾经定义一个结构方法属性,该属性可用于标记不应在只读结构上调用的方法,并且如果 List<T>.Enumerator
使用它,则代码如下你可以在调用 MoveNext
时正确地产生编译时错误,而不是产生虚假行为。但是,我知道没有特别计划添加这样的属性。
谁能解释一下为什么这段代码 运行 在无限循环中?为什么总是 MoveNext()
return true
?
var x = new { TempList = new List<int> { 1, 3, 6, 9 }.GetEnumerator() };
while (x.TempList.MoveNext())
{
Console.WriteLine("Hello World");
}
List<T>.GetEnumerator()
returns a mutable value type (List<T>.Enumerator
)。您将该值存储在匿名类型中。
现在,让我们看看它的作用:
while (x.TempList.MoveNext())
{
// Ignore this
}
这相当于:
while (true)
{
var tmp = x.TempList;
var result = tmp.MoveNext();
if (!result)
{
break;
}
// Original loop body
}
现在请注意我们在 MoveNext()
上调用什么 - 匿名类型的值的 copy。您实际上无法更改匿名类型中的值 - 您所拥有的只是一个可以调用的 属性,它将为您提供该值的副本。
如果将代码更改为:
var x = new { TempList = (IEnumerable<int>) new List<int> { 1, 3, 6, 9 }.GetEnumerator() };
... 那么您最终会得到匿名类型的 reference。对包含可变值的框的引用。当您对该引用调用 MoveNext()
时,框中的值将发生变化,因此它会执行您想要的操作。
有关非常相似情况的分析(再次使用 List<T>.GetEnumerator()
),请参阅 my 2010 blog post "Iterate, damn you!"。
虽然 C# 中的 foreach
构造和 VB.NET 中的 For Each
循环通常与实现 IEnumerable<T>
的类型一起使用,但它们将接受任何包含GetEnumerator
方法,其 return 类型提供了合适的 MoveNext
函数和 Current
属性。 GetEnumerator
return 值类型在许多情况下允许 foreach
比 returned IEnumerator<T>
.[=29 更有效地实现=]
不幸的是,因为无法通过一种方式在从 foreach
调用时提供值类型枚举器而不在被 GetEnumerator
方法调用时提供值类型枚举器,作者List<T>
面临性能与语义的轻微权衡。当时,因为 C# 不支持变量类型推断,任何使用从 List<T>.GetEnumerator
编辑的值 return 的代码都必须声明类型为 IEnumerator<T>
或 List<T>.Enumerator
的变量.使用前一种类型的代码表现得好像 List<T>.Enumerator
是引用类型,而使用后者的程序员可以假定意识到它是一种结构类型。但是,当 C# 添加类型推断时,该假设不再成立。代码很容易最终使用类型 List<T>.Enumerator
而程序员不知道该类型的存在。
如果 C# 曾经定义一个结构方法属性,该属性可用于标记不应在只读结构上调用的方法,并且如果 List<T>.Enumerator
使用它,则代码如下你可以在调用 MoveNext
时正确地产生编译时错误,而不是产生虚假行为。但是,我知道没有特别计划添加这样的属性。