关于c#:具有IDisposable的无限状态机

Infinite state machine with an IDisposable

假设我有一个无限状态机来生成随机MD5哈希:

1
2
3
4
5
6
7
8
public static IEnumerable<string> GetHashes()
{
    using (var hash = System.Security.Cryptography.MD5.Create())
    {
        while (true)
            yield return hash.ComputeHash(Guid.NewGuid().ToByteArray());
    }
}

在上面的示例中,我使用了using语句。是否会调用.Dispose()方法?cq,非托管资源是否会被释放?

例如,如果我使用机器如下:

1
2
3
4
5
6
7
8
9
10
11
12
public static void Test()
{
    int counter = 0;
    var hashes = GetHashes();
    foreach(var md5 in hashes)
    {
        Console.WriteLine(md5);
        counter++;
        if (counter > 10)
            break;
    }
}

由于hashes变量将超出作用域(我假定是垃圾收集的),将调用Dispose方法释放System.Security.Cryptography.MD5使用的资源,还是这是内存泄漏?


让我们稍微改变一下您的原始代码块,将其简化为基本的代码块,同时仍然保持足够有趣的代码块来进行分析。这并不完全等同于您发布的内容,但我们仍在使用迭代器的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Disposable : IDisposable {
    public void Dispose() {
        Console.WriteLine("Disposed!");
    }
}

IEnumerable<int> CreateEnumerable() {
    int i = 0;
    using (var d = new Disposable()) {
       while (true) yield return ++i;
    }
}

void UseEnumerable() {
    foreach (int i in CreateEnumerable()) {
        Console.WriteLine(i);
        if (i == 10) break;
    }
}

这将在打印Disposed!之前打印1到10之间的数字。

封面下面到底发生了什么?更多。我们先解决外层问题,UseEnumerableforeach是用于以下内容的句法糖:

1
2
3
4
5
6
7
8
9
10
var e = CreateEnumerable().GetEnumerator();
try {
    while (e.MoveNext()) {
        int i = e.Current;
        Console.WriteLine(i);
        if (i == 10) break;
    }
} finally {
    e.Dispose();
}

关于确切的细节(因为即使这样也简化了一点),我建议您参考C语言规范,第8.8.4节。这里重要的一点是,foreach需要隐式调用枚举器的Dispose

其次,CreateEnumerable中的using语句也是句法上的糖分。事实上,让我们用原始语句写出整件事,这样我们以后就能对翻译有更多的理解:

1
2
3
4
5
6
7
8
9
10
11
12
IEnumerable<int> CreateEnumerable() {
    int i = 0;
    Disposable d = new Disposable();
    try {
       repeat:
       i = i + 1;
       yield return i;
       goto repeat;
    } finally {
       d.Dispose();
    }
}

迭代器块的具体实现规则在语言规范的第10.14节中有详细说明。它们是用抽象操作给出的,而不是代码。关于C编译器生成什么类型的代码以及每个部分所做的工作,我们将进行深入的讨论,但我将给出一个简单的翻译,而不是仍然符合规范的翻译。重申一下,这不是编译器实际产生的结果,但它是一个很好的近似值,足以说明正在发生的事情,并忽略了处理线程和优化的更复杂的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
class CreateEnumerable_Enumerator : IEnumerator<int> {
    // local variables are promoted to instance fields
    private int i;
    private Disposable d;

    // implementation of Current
    private int current;
    public int Current => current;
    object IEnumerator.Current => current;

    // State machine
    enum State { Before, Running, Suspended, After };
    private State state = State.Before;

    // Section 10.14.4.1
    public bool MoveNext() {
        switch (state) {
            case State.Before: {
                    state = State.Running;
                    // begin iterator block
                    i = 0;
                    d = new Disposable();
                    i = i + 1;
                    // yield return occurs here
                    current = i;
                    state = State.Suspended;
                    return true;
                }
            case State.Running: return false; // can't happen
            case State.Suspended: {
                    state = State.Running;
                    // goto repeat
                    i = i + 1;
                    // yield return occurs here
                    current = i;
                    state = State.Suspended;
                    return true;
                }
            case State.After: return false;
            default: return false;  // can't happen
        }
    }

    // Section 10.14.4.3
    public void Dispose() {
        switch (state) {
            case State.Before: state = State.After; break;
            case State.Running: break; // unspecified
            case State.Suspended: {
                    state = State.Running;
                    // finally occurs here
                    d.Dispose();
                    state = State.After;
                }
                break;
            case State.After: return;
            default: return;    // can't happen
        }
    }

    public void Reset() { throw new NotImplementedException(); }
}

class CreateEnumerable_Enumerable : IEnumerable<int> {
  public IEnumerator<int> GetEnumerator() {
    return new CreateEnumerable_Enumerator();
  }

  IEnumerator IEnumerable.GetEnumerator() {
    return GetEnumerator();
  }
}

IEnumerable<int> CreateEnumerable() {
  return new CreateEnumerable_Enumerable();
}

这里最重要的一点是,代码块在出现yield returnyield break语句时被拆分,迭代器负责在中断时记住"我们在哪里"。体中的任何finally块推迟到Dispose块。代码中的无限循环实际上不再是无限循环了,因为它被周期性的yield return语句中断了。注意,由于finally块实际上不再是finally块了,所以在处理迭代器时,执行它的把握就不那么确定了。这就是为什么使用foreach或任何其他确保迭代器的Dispose方法在finally块中被调用的方式是必要的。

这是一个简化的例子;当您使循环更复杂、引入异常等时,事情会变得更有趣。"只做这个工作"的负担在编译器上。


很大程度上,这取决于您如何编写代码。但在您的示例中,将调用Dispose

下面是如何编译迭代器的解释。

具体来说,说到finally

Iterators pose an awkward problem. Instead of the whole method executing before the stack frame is popped, execution effectively pauses each time a value is yielded. There's no way of guaranteeing that the caller will ever use the iterator again, in any way, shape or form. If you require some more code to be executed at some point after the value is yielded, you're in trouble: you can't guarantee it will happen. To cut to the chase, code in a finally block which would normally be executed in almost all circumstances before leaving the method can't be relied on quite as much.

The state machine is built so that finally blocks are executed when an iterator is used properly, however. That's because IEnumerator implements IDisposable, and the C# foreach loop calls Dispose on iterators (even the nongeneric IEnumerator ones, if they implement IDisposable). The IDisposable implementation in the generated iterator works out which finally blocks are relevant to the current position (based on the state, as always) and execute the appropriate code.