为什么我在使用非短路 AND 运算符时会收到可能为空引用的取消引用警告?
Why do I get dereference of a possibly null reference warning when using non-short-circuit AND operator?
我有一个这样的方法,它是生产代码中一些逻辑的简化版本:
static void Foo(int a, int b, string x, string y)
{
if (a > 100 && b < 50 && (x != null) & (y != null))
{
Console.WriteLine(y.Length);
}
}
访问 y.Length
时出现 "Dereference of a possibly null reference" 错误。
如果我将 &
更改为 &&
,警告就会消失。乍一看我不明白为什么,然后我认为这可能是因为 &
运算符是可重载的,所以行为可能会改变并且可能计算为 true
即使 y
为空。我的假设是正确的还是我遗漏了什么?
注意:我可以使用 Visual Studio 2019 在面向 .NET Core 3.1 或 .NET Standard 2.0 的应用程序中重现此内容。
编译器无法识别此模式的原因有几个。最明显的只是工程上的努力。将 &
/|
用于布尔逻辑被认为不如 &&
/||
常见,因此其他事情优先于完全支持 &
/|
在流量分析中。
另一个原因是,在可空分析中支持这一点实际上会产生复杂性成本。考虑以下公认的人为示例:SharpLab
func(null, null, out _);
void func(C? x, C? y, out C z)
{
if (x != null & func1(z = y = x)) // should warn on 'y = x'
{
x.ToString(); // warns, but perhaps shouldn't
y.ToString(); // warns, but perhaps shouldn't
}
}
bool func1(object? obj) => true;
class C
{
public C Inner { get; set; }
public C()
{
Inner = this;
}
}
- 访问表达式
x != null
后,x
的状态为“为真时不为空”,“为假时可能为空”。
- 当我们访问
func1(z = y = x)
时,我们必须假设最坏的情况--x
可能为空,因为无论 x != null
是真还是假,我们都会到达那里。因此,我们必须在分配给不可为空的输出参数 z
. 时发出警告
- 然后,在我们访问完
&
运算符后,我们必须假设如果结果为真,则所有操作数都返回真,否则任何操作数都可能返回假。
- 但是坚持住!现在,如果我们必须假设所有操作数都返回 true,那意味着
x
一直都是非空的,并且因为 x
被分配给 y
,所以 y
一直都不是空的。在这种特殊类型的分析中,唯一可行的方法是访问 func1(z = y = x)
第二次 ,但初始状态 x != null
为真。
换句话说,访问完全处理可为空条件状态的 &
运算符需要访问右侧两次——一次假设左侧的最坏情况结果为其生成诊断,并再次假设左侧为真,这样我们就可以为运算符生成最终状态。在人为设计的示例中,这种效果可能是复合的,例如 x != null & (y != null & z != null)
,我们最终不得不访问 z != null
4 次。
为了避免这种复杂性,我们选择此时不对 &
/|
进行可空分析。 &&
/||
的短路行为意味着它们不会遇到上述问题,因此使它们正常工作实际上更简单。
我有一个这样的方法,它是生产代码中一些逻辑的简化版本:
static void Foo(int a, int b, string x, string y)
{
if (a > 100 && b < 50 && (x != null) & (y != null))
{
Console.WriteLine(y.Length);
}
}
访问 y.Length
时出现 "Dereference of a possibly null reference" 错误。
如果我将 &
更改为 &&
,警告就会消失。乍一看我不明白为什么,然后我认为这可能是因为 &
运算符是可重载的,所以行为可能会改变并且可能计算为 true
即使 y
为空。我的假设是正确的还是我遗漏了什么?
注意:我可以使用 Visual Studio 2019 在面向 .NET Core 3.1 或 .NET Standard 2.0 的应用程序中重现此内容。
编译器无法识别此模式的原因有几个。最明显的只是工程上的努力。将 &
/|
用于布尔逻辑被认为不如 &&
/||
常见,因此其他事情优先于完全支持 &
/|
在流量分析中。
另一个原因是,在可空分析中支持这一点实际上会产生复杂性成本。考虑以下公认的人为示例:SharpLab
func(null, null, out _);
void func(C? x, C? y, out C z)
{
if (x != null & func1(z = y = x)) // should warn on 'y = x'
{
x.ToString(); // warns, but perhaps shouldn't
y.ToString(); // warns, but perhaps shouldn't
}
}
bool func1(object? obj) => true;
class C
{
public C Inner { get; set; }
public C()
{
Inner = this;
}
}
- 访问表达式
x != null
后,x
的状态为“为真时不为空”,“为假时可能为空”。 - 当我们访问
func1(z = y = x)
时,我们必须假设最坏的情况--x
可能为空,因为无论x != null
是真还是假,我们都会到达那里。因此,我们必须在分配给不可为空的输出参数z
. 时发出警告
- 然后,在我们访问完
&
运算符后,我们必须假设如果结果为真,则所有操作数都返回真,否则任何操作数都可能返回假。 - 但是坚持住!现在,如果我们必须假设所有操作数都返回 true,那意味着
x
一直都是非空的,并且因为x
被分配给y
,所以y
一直都不是空的。在这种特殊类型的分析中,唯一可行的方法是访问func1(z = y = x)
第二次 ,但初始状态x != null
为真。
换句话说,访问完全处理可为空条件状态的 &
运算符需要访问右侧两次——一次假设左侧的最坏情况结果为其生成诊断,并再次假设左侧为真,这样我们就可以为运算符生成最终状态。在人为设计的示例中,这种效果可能是复合的,例如 x != null & (y != null & z != null)
,我们最终不得不访问 z != null
4 次。
为了避免这种复杂性,我们选择此时不对 &
/|
进行可空分析。 &&
/||
的短路行为意味着它们不会遇到上述问题,因此使它们正常工作实际上更简单。