IDisposable 字段怎么能在这里为空?
How can IDisposable field be null here?
我有很多例子,一次性 class 使用空条件运算符(如 _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()
,因为它会抛出空引用异常。
你是对的,_bar
在此特定示例中不能为 null。这是几个因素的组合:
- 只有一个构造函数总是初始化一个
Bar
对象
_bar
是只读的,这保证之后没有人可以将其设置为 null
但不难想出一个它可能为空的示例(例如,更改上述任何要点)。即使对于这个简单的代码示例,我已经争辩说,如果您希望这是一个干净的代码库,那么 Bar
很可能应该被注入,这抛出了您可以保证 _bar
不会的论点。 t 为空。
专门创建 null-protecting 和 non-null 保护代码变体,而不是始终防止空值,这有什么好处?这需要更多的努力,您可能会选择错误的选项;有什么好处?没有。
所以如果故意省略空值保护没有任何好处,为什么还要费心弄清楚是否有必要防止空值?只包括 null 保护比花时间弄清楚在这种特殊情况下是否不需要 null 保护要容易得多。
另请注意,您具体指的是在线演示代码或 Resharper 生成的模板,这两种情况都旨在覆盖广泛的受众并保持广泛适用性。
虽然对于类型的大多数“正常”使用而言,另一个答案是正确的,但至少有两个稍微不寻常的场景,其中 _bar
可以 在 Dispose()
中为 null,尽管似乎总是通过唯一的构造函数进行初始化。
值得理解的是,在 .NET 中创建对象本质上是一个两步过程:
- 分配对象
- 调用适当的构造函数
步骤 1 可以通过多种方式发生 而没有 步骤 2,这使您得到一个对象,其所有字段都处于相应类型的默认状态(例如,null
代表引用类型,零代表数字类型,等等)。
第一个是有意通过 FormatterServices.GetUninitializedObject()
,它主要由旨在反序列化对象的代码使用(例如,在持久化到文件之后)。顾名思义,它分配一个对象的实例(步骤 1),但 不 执行构造函数。对于您的示例 Foo
class,这将导致 _bar
保持为空。如果随后在实例上调用 Dispose()
而没有通过其他方式(例如反射)初始化 _bar
,则将在 Dispose()
方法中抛出空引用异常,而无需_bar
.
空检查
另一种更不常见的可能性是 long-standing(将近 5 岁)bug in .NET itself。虽然它不会出现在您的示例中的确切代码上,但稍作修改(引入终结器)就可以实现。
这是一个例子:
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");
}
}
并触发它:
static void Main(string[] args)
{
try
{
// We intentionally trigger an exception when getting the arg to pass to `Foo()`
// to trigger the bug, however in real-life example you might call a method
// here that sometimes throws
using (var foo = new Foo(args[-1]))
{
// ...
}
}
catch
{
}
// These two lines aren't required for the bug to be triggered,
// they simply allow us to see it without waiting for a GC.
GC.Collect();
GC.WaitForPendingFinalizers();
}
当我 运行 时的输出是:
Finalizing and _bar is null
解释相对简单,也是从创建对象的两个步骤过程得出的。人们可能期望 .NET 总是会在步骤 #1 之后立即执行步骤 #2,但实际上它将确定构造函数参数值的代码放在 两个步骤之间(如果有的话)该代码的一部分抛出异常,就像我们(有意)在这里所做的那样,然后跳过步骤 #2。虽然我们不能访问分配的对象(因为它永远不会分配给 foo
变量),因为它是可终结的,.NET 仍然跟踪它以便稍后调用它的终结器。
虽然这两个例子看起来有些假设,但多年来我在生产代码库中都遇到过它们,所以值得一提!
我有很多例子,一次性 class 使用空条件运算符(如 _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()
,因为它会抛出空引用异常。
你是对的,_bar
在此特定示例中不能为 null。这是几个因素的组合:
- 只有一个构造函数总是初始化一个
Bar
对象 _bar
是只读的,这保证之后没有人可以将其设置为 null
但不难想出一个它可能为空的示例(例如,更改上述任何要点)。即使对于这个简单的代码示例,我已经争辩说,如果您希望这是一个干净的代码库,那么 Bar
很可能应该被注入,这抛出了您可以保证 _bar
不会的论点。 t 为空。
专门创建 null-protecting 和 non-null 保护代码变体,而不是始终防止空值,这有什么好处?这需要更多的努力,您可能会选择错误的选项;有什么好处?没有。
所以如果故意省略空值保护没有任何好处,为什么还要费心弄清楚是否有必要防止空值?只包括 null 保护比花时间弄清楚在这种特殊情况下是否不需要 null 保护要容易得多。
另请注意,您具体指的是在线演示代码或 Resharper 生成的模板,这两种情况都旨在覆盖广泛的受众并保持广泛适用性。
虽然对于类型的大多数“正常”使用而言,另一个答案是正确的,但至少有两个稍微不寻常的场景,其中 _bar
可以 在 Dispose()
中为 null,尽管似乎总是通过唯一的构造函数进行初始化。
值得理解的是,在 .NET 中创建对象本质上是一个两步过程:
- 分配对象
- 调用适当的构造函数
步骤 1 可以通过多种方式发生 而没有 步骤 2,这使您得到一个对象,其所有字段都处于相应类型的默认状态(例如,null
代表引用类型,零代表数字类型,等等)。
第一个是有意通过 FormatterServices.GetUninitializedObject()
,它主要由旨在反序列化对象的代码使用(例如,在持久化到文件之后)。顾名思义,它分配一个对象的实例(步骤 1),但 不 执行构造函数。对于您的示例 Foo
class,这将导致 _bar
保持为空。如果随后在实例上调用 Dispose()
而没有通过其他方式(例如反射)初始化 _bar
,则将在 Dispose()
方法中抛出空引用异常,而无需_bar
.
另一种更不常见的可能性是 long-standing(将近 5 岁)bug in .NET itself。虽然它不会出现在您的示例中的确切代码上,但稍作修改(引入终结器)就可以实现。
这是一个例子:
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");
}
}
并触发它:
static void Main(string[] args)
{
try
{
// We intentionally trigger an exception when getting the arg to pass to `Foo()`
// to trigger the bug, however in real-life example you might call a method
// here that sometimes throws
using (var foo = new Foo(args[-1]))
{
// ...
}
}
catch
{
}
// These two lines aren't required for the bug to be triggered,
// they simply allow us to see it without waiting for a GC.
GC.Collect();
GC.WaitForPendingFinalizers();
}
当我 运行 时的输出是:
Finalizing and _bar is null
解释相对简单,也是从创建对象的两个步骤过程得出的。人们可能期望 .NET 总是会在步骤 #1 之后立即执行步骤 #2,但实际上它将确定构造函数参数值的代码放在 两个步骤之间(如果有的话)该代码的一部分抛出异常,就像我们(有意)在这里所做的那样,然后跳过步骤 #2。虽然我们不能访问分配的对象(因为它永远不会分配给 foo
变量),因为它是可终结的,.NET 仍然跟踪它以便稍后调用它的终结器。
虽然这两个例子看起来有些假设,但多年来我在生产代码库中都遇到过它们,所以值得一提!