.NET 使用可为空引用类型实现 IEnumerator

Gab*_*ik1 5 .net c# ienumerator nullable-reference-types

自从C# 8.0 发布以来,我真的很享受可空引用类型的“无效安全” 。然而,在调整我的库以支持新功能时,我偶然发现了一个“问题”,我真的在任何地方都找不到答案。我看过微软的发行说明和 .NET 源代码,但没有运气。

TL;DR:问题本质上是一个IEnumerator<T>Current属性是否应该被声明为可以为空的引用类型。

假设以下实现IEnumerator<T>

public class WebSocketClientEnumerator<TWebSocketClient> : IEnumerator<TWebSocketClient> where TWebSocketClient : WebSocketClient
{
    private WebSocketRoom<TWebSocketClient> room;
    private int curIndex;
    private TWebSocketClient? curCli;

    public WebSocketClientEnumerator(WebSocketRoom<TWebSocketClient> room)
    {
        this.room = room;
        curIndex = -1;
        curCli = default(TWebSocketClient);
    }

    public bool MoveNext()
    {
        if (++curIndex >= room.Count)
        {
            return false;
        }
        else
        {
            curCli = room[curIndex];
        }

        return true;
    }

    public void Reset() { curIndex = -1; }

    void IDisposable.Dispose() { }

    public TWebSocketClient? Current
    {
        get { return curCli; }
    }

    object IEnumerator.Current
    {
        get { return Current; }
    }
}
Run Code Online (Sandbox Code Playgroud)

并假设以下代码使用枚举器:

public class WebSocketRoom<TWebSocketClient> : ICollection<TWebSocketClient> where TWebSocketClient : WebSocketClient
{
    // ...

    public void UseEnumerator()
    {
        var e = new WebSocketClientEnumerator<TWebSocketClient>(this);
        bool hasNext = e.MoveNext();
        if (hasNext)
        {
            WebSocketClient c = e.Current; // <= Warning on this line
        }
    }

    // ...
}
Run Code Online (Sandbox Code Playgroud)

代码本身会生成警告,因为很明显, 的返回类型是WebSocketClientEnumerator<TWebSocketClient>.Current可以为空的引用类型。

IEnumerator接口的设计方式是,“应该”调用该IEnumerator<T>.MoveNext()方法以事先知道枚举数是否具有下一个值,从而实现了某种无效的安全性,但显然,在编译器看来,这毫无意义,调用该MoveNext()方法本质上不保证枚举器的Current属性不为空。

我希望我的库在没有警告的情况下编译,如果它没有被声明为可空引用类型,并且如果它被声明为可为空,那么编译器不会让我在构造函数中留下this.curCli一个null值,那么检查一个的“负担” null 引用被传输到库的客户端。诚然,枚举器通常通过foreach语句使用,因此它主要由运行时处理,可能没什么大不了的。确实,从语义上讲,枚举器的Current属性是有意义的,null因为可能没有要枚举的数据,但我确实看到了IEnumerator<T>接口和可为空引用类型功能。我真的想知道是否有办法让编译器满意,同时仍然保持功能。而且,其他一些语言中具有一些无效安全机制的约定是什么?

我意识到这是一个开放式问题,但我仍然认为它适合 SO。提前致谢!

Jon*_*eet 6

我绝对建议将其声明为非空版本。的文档Current指出,该行为是在实际上为空的情况下定义的curCli。我认为任何Current在这些情况下阅读的人的代码中都有错误,最好通过异常来显示该错误......这非常容易做到:

public TWebSocketClient Current => curCli ??
    throw new InvalidOperationException("Current should not be used in the current state");
Run Code Online (Sandbox Code Playgroud)

您可能还希望在返回时设置curCli为,以便在耗尽枚举器后访问代码时也会引发异常。nullfalseCurrent

在这一点上,我认为您的代码比编译器生成的代码更好yield return,它不会引发异常。