为什么结构中的迭代器可以修改它?

时间:2010-12-22 23:06:04

标签: c# .net iterator this value-type

I discovered that iterator methods in value types are allowed to modify this
但是,由于CLR的限制,调用方法看不到修改。 (this按值传递)

因此,迭代器和非迭代器中的相同代码会产生不同的结果:

static void Main() {
    Mutable m1 = new Mutable();
    m1.MutateWrong().ToArray();     //Force the iterator to execute
    Console.WriteLine("After MutateWrong(): " + m1.Value);

    Console.WriteLine();

    Mutable m2 = new Mutable();
    m2.MutateRight();
    Console.WriteLine("After MutateRight(): " + m2.Value);
}

struct Mutable {
    public int Value;

    public IEnumerable<int> MutateWrong() {
        Value = 7;
        Console.WriteLine("Inside MutateWrong(): " + Value);
        yield break;
    }
    public IEnumerable<int> MutateRight() {
        Value = 7;
        Console.WriteLine("Inside MutateRight(): " + Value);
        return new int[0];
    }
}

输出:

Inside MutateWrong(): 7
After MutateWrong(): 0

Inside MutateRight(): 7
After MutateRight(): 7

为什么在迭代器中改变结构不是编译器错误(或至少是警告)? 这种行为是一个微妙的陷阱,不容易理解。

匿名方法,它们具有相同的限制,cannot use this at all

注意:mutable structs are evil;这应该永远不会出现在实践中。

4 个答案:

答案 0 :(得分:2)

为了证明警告的合理性,应该是程序员可能会得到意外结果的情况。根据Eric Lippert的说法,“we try to reserve warnings for only those situations where we can say with almost certainty that the code is broken, misleading or useless.”这是一个警告误导的实例。

让我们说你有这个完全有效 - 如果不是非常有用 - 对象:

struct Number
{
    int value;
    public Number(int value) { this.value = value; }
    public int Value { get { return value; } }
    // iterator that mutates "this"
    public IEnumerable<int> UpTo(int max)
    {
        for (; value <= max; value++)
            yield return value;
    }
}

你有这个循环:

var num = new Number(1);
foreach (var x in num.UpTo(4))
    Console.WriteLine(num.Value);

您希望此循环打印1,1,1,1,而不是1,2,3,4,对吗?所以课程的工作方式完全符合您的预期。这是警告不合理的情况。

由于这显然不是代码被破坏,误导或无用的情况,您如何建议编译器生成错误或警告?

答案 1 :(得分:2)

引用自己“可变的结构是邪恶的”:) 如果为结构实现扩展方法,则会遇到与您相同的事情。 如果您尝试在扩展方法中修改结构,您仍将保持原始结构不变。 由于扩展方法签名如下所示,它有点不太令人惊讶:

static void DoSideEffects(this MyStruct x) { x.foo = ...

看着它我们意识到像参数传递之类的东西发生了,因此结构被复制了。但是当你使用扩展时它看起来像:

x.DoSideEffects()

你会惊讶的是不要对变量x产生任何影响。 我认为在幕后你的yield构造做了类似于扩展的事情。 我会更难说出头句: “结构是邪恶的”..一般来说;)

答案 2 :(得分:0)

我对Gabe said有类似的想法。似乎至少理论可能选择使用struct来表现类似方法,将该方法的局部变量封装为实例字段:

struct Evens
{
    int _current;

    public IEnumerable<int> Go()
    {
        while (true)
        {
            yield return _current;
            _current += 2;
        }
    }
}

我的意思是,显然有点奇怪。这让我想起过去遇到的一些想法,开发人员编写了一些奇怪的方法来调用方法,例如将方法的参数包装到对象中

我并不是说这是明智的要做的事情,但它至少是一种以你描述的方式使用this的方式,可能是故意的,从技术上讲会表现出正确的行为。

答案 3 :(得分:0)

虽然我认为.net缺乏不需要修改'this'的方法的特殊属性,但不清楚应该发生什么。这样的属性可以用于不可变的类类型以及可变的结构。使用此类属性标记的方法应仅可用于结构变量,字段和参数,而不能用于临时值。

我认为没有办法避免迭代器按值捕获结构。完全有可能在使用迭代器时,它所基于的原始结构可能不再存在。另一方面,如果struct实现了继承IEnumerable&lt; int&gt;的接口,但也包含了一个返回Value的函数,那么在使用枚举器之前将struct转换为该接口类型理论上可以让迭代器保持对结构而不必重新复制其值;如果枚举器按值复制结构,即使在这种情况下,我也不会感到惊讶。