自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<TWebSocketClient>.Current
的返回类型是可为空的引用类型。
IEnumerator
接口的设计方式是,一个“应该”调用IEnumerator<T>.MoveNext()
方法来事先知道枚举数是否有下一个值,从而实现某种无效性,但显然对编译器没有任何意义,调用MoveNext()
方法本质上不能保证枚举器的Current
属性不为null。
我希望我的库在没有警告的情况下进行编译,并且如果未将其声明为可为空的引用类型,并且如果声明为可为空,则编译器将不允许我在构造函数中将this.curCli
保留为null
值检查空引用的“负担”被转移到库的客户端。诚然,枚举数通常通过foreach
语句使用,因此它主要由运行时处理,可能没什么大不了的。确实,从语义上来说,将枚举器的Current
属性设置为null
是有意义的,因为可能没有要枚举的数据,但是我确实确实看到IEnumerator<T>
接口和可为空的引用类型功能之间存在冲突。我真的很想知道是否有办法在保持功能的同时使编译器满意。而且,其他一些语言中具有无效安全机制的约定又有哪些呢?
我意识到这是一个开放性的问题,但我仍然认为这对SO是合适的。预先感谢!
我绝对建议将其声明为非null版本。 documentation for Current
指出行为是在Current
实际上为空的情况下定义的。我认为在这种情况下阅读curCli
的任何人的代码中都有错误,因此最好通过异常来掩盖该错误……这确实很容易做到:
Current
[您可能还想在返回public TWebSocketClient Current => curCli ??
throw new InvalidOperationException("Current should not be used in the current state");
时将curCli
设置为null
,所以如果在用尽枚举器之后代码访问false
,也会引发异常。
[那时,我认为您的代码比Current
的编译器生成的代码更好,后者不会引发异常。