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

Posted

技术标签:

【中文标题】.NET 使用可为空的引用类型实现 IEnumerator【英文标题】:.NET Implement IEnumerator with nullable reference types 【发布时间】:2020-02-10 19:13:10 【问题描述】:

自从release of C# 8.0 之后,我真的很享受nullable reference types 带来的“虚空安全”。然而,在调整我的库以支持新功能时,我偶然发现了一个“问题”,我真的无法在任何地方找到答案。我查看了 Microsoft 的发行说明和 .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; 
    

并假设以下代码消耗枚举器:

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
        
    

    // ...

代码会产生警告,因为WebSocketClientEnumerator&lt;TWebSocketClient&gt;.Current 的返回类型显然是可以为空的引用类型。

IEnumerator 接口的设计方式是“应该”调用IEnumerator&lt;T&gt;.MoveNext() 方法来事先知道枚举数是否有下一个值,从而实现某种无效安全,但显然,在编译器这意味着什么,调用MoveNext() 方法并不能本质上保证枚举器的Current 属性不为空。

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

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

【问题讨论】:

【参考方案1】:

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

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

当您返回false 时,您可能还想将curCli 设置为null,这样如果在枚举数耗尽后代码访问Current,也会引发异常。

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

【讨论】:

是的,我没有考虑过引发错误,但它实际上很有意义,我还认为在 MoveNext() 返回 false 之后阅读 Current 是客户端代码中的错误.好决定!泰!

以上是关于.NET 使用可为空的引用类型实现 IEnumerator的主要内容,如果未能解决你的问题,请参考以下文章

c#静态类中的8个可为空的引用类型

将 XDocument.Descendants 与合并运算符一起使用??和可为空的类型

如何识别泛型类型的可为空引用类型?

如何将 CheckBox 绑定到可为空的布尔类型 DbColumn?

C# 8.0 不可为空的引用类型和选项模式

在 Entity Framework 6 查询中取消引用可能为空的引用