我有很多示例,一个可弃类使用诸如_field?.Dispose()
之类的空条件运算符来处理其可弃字段。 Resharper还会生成带有空检查的代码。
class Foo : IDisposable
{
private readonly Bar _bar;
public Foo()
{
_bar = new Bar();
}
public void Dispose()
{
_bar?.Dispose();
}
}
class Bar : IDisposable
{
public void Dispose()
{
}
}
我的问题是,为什么在此处使用对字段_bar
的空检查?此时不能为空。除非包含对象被垃圾收集,否则它不会被垃圾收集,因为包含对象的引用。而且如果包含的对象(即Foo
对象为null),我们将永远无法对其调用.Dispose()
,因为它将引发null引用异常。
答案 0 :(得分:4)
是的,_bar
在此特定示例中不能为null。这是一些因素的组合:
Bar
对象_bar
是只读的,可以确保以后没有人可以将其设置为null 但是要举一个可能为null的示例并不难(例如,更改以上任何要点)。即使对于这个简单的代码示例,我也已经争辩说,如果您希望将Bar
用作干净的代码库,则很可能应该注入_bar
,这引发了您可以保证id="text"
会得到保证的论点。 t为空。
专门创建代码的空值保护和非空值保护的变体,而不是总是防止空值,这有什么好处?这需要花费更多的精力,您可能会选择错误的选项。有什么好处?没事。
因此,如果故意省略无效保护没有好处,那么为什么还要弄清楚是否有必要防止无效保护呢?仅包括空保护比花时间确定在这种情况下是否不需要空保护要容易得多。
还请注意,您所指的是在线演示代码或Resharper生成的模板,在这两种情况下,其目的都是为了吸引广泛的受众并保持广泛的适用性。
答案 1 :(得分:2)
尽管其他答案对于某种类型的大多数“正常”用法是正确的,但至少有两种稍微不同的情况,其中_bar
在{{ 1}},尽管它似乎总是通过唯一的构造函数进行初始化。
值得理解的是,在.NET中创建对象本质上是一个两步过程:
有多种方法可以使步骤1发生不步骤2,这为您提供了一个对象,该对象的所有字段均处于相应类型的默认状态(例如{{1 }}用于引用类型,零用于数字类型,等等。
第一个是有意通过FormatterServices.GetUninitializedObject()
进行的,该代码主要由旨在反序列化对象的代码使用(例如,在持久化到文件之后)。顾名思义,它分配一个对象的实例(步骤1),但是不执行构造函数。对于您的示例Dispose()
类,这将导致null
保持为空。如果随后要在实例上调用Foo
而不用其他方法(例如反射)初始化_bar
,则在Dispose()
方法中将引发空引用异常,而没有对_bar
进行空检查。
另一种不太常见的可能性是由长期(近5岁)bug in .NET itself造成的。尽管在您的示例中的确切代码上不会发生此错误,但只需进行一些修改(引入Finalizer)就可以实现。
这是一个例子:
Dispose()
并触发它:
_bar
我运行它时的输出是:
Finalizing and _bar is null
解释是相对简单的,也是由创建对象的两步过程引起的。人们可能希望.NET总是在步骤#1之后立即执行步骤#2,但实际上,它将代码确定确定构造函数自变量的值 到两个步骤之间(如果有)就像我们在这里有意地抛出一个异常一样,然后跳过步骤2。尽管我们无法访问已分配的对象(因为它永远不会分配给class Foo : IDisposable
{
private readonly Bar _bar;
// The constructor needs to have at least one argument
public Foo(string someArg)
{
_bar = new Bar();
}
public void Dispose()
{
Dispose(true);
}
// And we need a finalizer
~Foo()
{
Dispose(false);
}
private void Dispose(bool disposing)
{
Console.WriteLine("{0} and _bar is {1}", disposing ? "Disposing" : "Finalizing", _bar == null ? "null" : "not null");
}
}
变量),因为它是可终结的,.NET仍会对其进行跟踪,以便稍后调用其终结器。
虽然这两个似乎都是假设的示例,但多年来,我在生产代码库中都遇到了这两个示例,所以值得一提!