错误的 C# 编译器警告?
False C# compiler warning?
好吧,这是我们偶然发现的一个牵强的角落案例,但它让我很好奇。
考虑以下代码:
public class Foo
{
private int foo;
public int Reset() => foo = 0; //remember, assignment expressions
//return something!
}
这段代码可以编译吗?
不,如果您在所有警告上均未通过,则不会;你会收到 member foo is assigned but never used
警告。
此代码在所有方面都等同于:
public class Foo
{
private int foo;
public int Reset() { foo = 0; return foo; }
}
哪个编译正常,那么这里有什么问题呢?请注意,=>
语法不是问题,它返回的赋值表达式似乎让编译器感到困惑。
在第一个示例中,foo
已分配,但从未读取。 0
被分配给 foo
,然后 0
被返回,不管 foo
的值是什么(例如,如果另一个线程同时改变它)。
在第二个例子中,foo
被赋值,然后从中读取。如果同时有另一个线程修改了foo
,则返回修改后的值,而不是0
.
您可以通过比较已编译的 IL 来了解实际情况。给定以下代码:
public class Foo
{
private int foo;
public int Reset() { return (foo = 0); }
public int Reset2() { foo = 0; return foo; }
}
以下 IL 是为 Reset
和 Reset2
.
编译的
.method public hidebysig
instance int32 Reset () cil managed
{
.maxstack 3
.locals init (
[0] int32
)
IL_0000: ldarg.0
IL_0001: ldc.i4.0
IL_0002: dup
IL_0003: stloc.0
IL_0004: stfld int32 Foo::foo
IL_0009: ldloc.0
IL_000a: ret
}
.method public hidebysig
instance int32 Reset2 () cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldc.i4.0
IL_0002: stfld int32 Foo::foo
IL_0007: ldarg.0
IL_0008: ldfld int32 Foo::foo
IL_000d: ret
}
在Reset
中,我们只存储到Foo::foo
(stfld
).
在Reset2
中,你可以看到我们既向它存储又从它加载(stfld
+ ldfld
)。
要用纯 C# 术语回答这个问题,赋值表达式的值就是分配的值。我们可以这样证明:
public class Foo
{
public int Bar
{
get { return 2; }
set { /* do nothing */ }
}
}
/* … */
Foo foo = new Foo();
Console.WriteLine(foo.Bar = 23);
这会将 23
打印到控制台,因为 foo.Bar = 23
的值是 23
,即使 foo.Bar
的值始终是 2
。
大多数时候,这样做的唯一效果是性能上的小提升,因为大多数时候 属性 或字段的值将是分配给它的值,但我们可以重用我们已有的本地值,而不是访问字段的 属性。
这里的警告不仅在技术上是正确的,而且很有用:因为 foo
从来没有真正被读取过,拥有它只是浪费内存和分配给它的时间,你应该删除它作为废话。 (或者,当然,继续开发,以便出现尚未编码的情况)。
好吧,这是我们偶然发现的一个牵强的角落案例,但它让我很好奇。
考虑以下代码:
public class Foo
{
private int foo;
public int Reset() => foo = 0; //remember, assignment expressions
//return something!
}
这段代码可以编译吗?
不,如果您在所有警告上均未通过,则不会;你会收到 member foo is assigned but never used
警告。
此代码在所有方面都等同于:
public class Foo
{
private int foo;
public int Reset() { foo = 0; return foo; }
}
哪个编译正常,那么这里有什么问题呢?请注意,=>
语法不是问题,它返回的赋值表达式似乎让编译器感到困惑。
在第一个示例中,foo
已分配,但从未读取。 0
被分配给 foo
,然后 0
被返回,不管 foo
的值是什么(例如,如果另一个线程同时改变它)。
在第二个例子中,foo
被赋值,然后从中读取。如果同时有另一个线程修改了foo
,则返回修改后的值,而不是0
.
您可以通过比较已编译的 IL 来了解实际情况。给定以下代码:
public class Foo
{
private int foo;
public int Reset() { return (foo = 0); }
public int Reset2() { foo = 0; return foo; }
}
以下 IL 是为 Reset
和 Reset2
.
.method public hidebysig
instance int32 Reset () cil managed
{
.maxstack 3
.locals init (
[0] int32
)
IL_0000: ldarg.0
IL_0001: ldc.i4.0
IL_0002: dup
IL_0003: stloc.0
IL_0004: stfld int32 Foo::foo
IL_0009: ldloc.0
IL_000a: ret
}
.method public hidebysig
instance int32 Reset2 () cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldc.i4.0
IL_0002: stfld int32 Foo::foo
IL_0007: ldarg.0
IL_0008: ldfld int32 Foo::foo
IL_000d: ret
}
在Reset
中,我们只存储到Foo::foo
(stfld
).
在Reset2
中,你可以看到我们既向它存储又从它加载(stfld
+ ldfld
)。
要用纯 C# 术语回答这个问题,赋值表达式的值就是分配的值。我们可以这样证明:
public class Foo
{
public int Bar
{
get { return 2; }
set { /* do nothing */ }
}
}
/* … */
Foo foo = new Foo();
Console.WriteLine(foo.Bar = 23);
这会将 23
打印到控制台,因为 foo.Bar = 23
的值是 23
,即使 foo.Bar
的值始终是 2
。
大多数时候,这样做的唯一效果是性能上的小提升,因为大多数时候 属性 或字段的值将是分配给它的值,但我们可以重用我们已有的本地值,而不是访问字段的 属性。
这里的警告不仅在技术上是正确的,而且很有用:因为 foo
从来没有真正被读取过,拥有它只是浪费内存和分配给它的时间,你应该删除它作为废话。 (或者,当然,继续开发,以便出现尚未编码的情况)。