.NET c# 编译器 (.NET 4.0)fixed
以一种相当特殊的方式编译语句。
这是一个简短但完整的程序,向您展示我在说什么。
using System;
public static class FixedExample {
public static void Main() {
byte [] nonempty = new byte[1] {42};
byte [] empty = new byte[0];
Good(nonempty);
Bad(nonempty);
try {
Good(empty);
} catch (Exception e){
Console.WriteLine(e.ToString());
/* continue with next example */
}
Console.WriteLine();
try {
Bad(empty);
} catch (Exception e){
Console.WriteLine(e.ToString());
/* continue with next example */
}
}
public static void Good(byte[] buffer) {
unsafe {
fixed (byte * p = &buffer[0]) {
Console.WriteLine(*p);
}
}
}
public static void Bad(byte[] buffer) {
unsafe {
fixed (byte * p = buffer) {
Console.WriteLine(*p);
}
}
}
}
如果您想继续,请使用“csc.exe FixedExample.cs /unsafe /o+”编译它。
这是该方法生成的 IL Good
:
好的()
.maxstack 2
.locals init (uint8& pinned V_0)
IL_0000: ldarg.0
IL_0001: ldc.i4.0
IL_0002: ldelema [mscorlib]System.Byte
IL_0007: stloc.0
IL_0008: ldloc.0
IL_0009: conv.i
IL_000a: ldind.u1
IL_000b: call void [mscorlib]System.Console::WriteLine(int32)
IL_0010: ldc.i4.0
IL_0011: conv.u
IL_0012: stloc.0
IL_0013: ret
这是该方法生成的 IL Bad
:
坏的()
.locals init (uint8& pinned V_0, uint8[] V_1)
IL_0000: ldarg.0
IL_0001: dup
IL_0002: stloc.1
IL_0003: brfalse.s IL_000a
IL_0005: ldloc.1
IL_0006: ldlen
IL_0007: conv.i4
IL_0008: brtrue.s IL_000f
IL_000a: ldc.i4.0
IL_000b: conv.u
IL_000c: stloc.0
IL_000d: br.s IL_0017
IL_000f: ldloc.1
IL_0010: ldc.i4.0
IL_0011: ldelema [mscorlib]System.Byte
IL_0016: stloc.0
IL_0017: ldloc.0
IL_0018: conv.i
IL_0019: ldind.u1
IL_001a: call void [mscorlib]System.Console::WriteLine(int32)
IL_001f: ldc.i4.0
IL_0020: conv.u
IL_0021: stloc.0
IL_0022: ret
这是做什么的Good
:
- 获取缓冲区[0]的地址。
- 取消引用该地址。
- 使用该取消引用的值调用 WriteLine。
这是“坏”的作用:
- 如果缓冲区为空,则转到 3。
- 如果 buffer.Length != 0,转到 5。
- 将值 0 存储在本地插槽 0 中,
- 转到 6。
- 获取缓冲区[0]的地址。
- 尊重该地址(在本地插槽 0 中,现在可能是 0 或缓冲区)。
- 使用该取消引用的值调用 WriteLine。
当buffer
既非空又非空时,这两个函数做同样的事情。请注意,在进行函数调用Bad
之前,它只是跳过了几圈。WriteLine
当buffer
为 null 时,在固定指针声明符( )中Good
抛出 a 。大概这是修复托管数组所需的行为,因为通常固定语句内的任何操作都将取决于被修复对象的有效性。否则为什么该代码会在块内?当传递一个空引用时,它会在块的开头立即失败,提供相关且信息丰富的堆栈跟踪。开发人员会看到这一点并意识到他应该在使用它之前进行验证,或者他的逻辑可能被错误地分配给. 无论哪种方式,清楚地进入一个带有NullReferenceException
byte * p = &buffer[0]
fixed
Good
fixed
buffer
null
buffer
fixed
null
托管数组是不可取的。
Bad
以不同的方式处理这种情况,甚至是不受欢迎的。您可以看到在取消引用Bad
之前实际上不会引发异常。p
它以迂回的方式将 null 分配给持有的同一个本地槽p
,然后在fixed
块语句取消引用时抛出异常p
。
以这种方式处理null
的优点是保持 C# 中的对象模型一致。也就是说,在fixed
块内部,在p
语义上仍然被视为一种“指向托管数组的指针”,当它为 null 时,不会导致问题,直到(或除非)它被取消引用。一致性很好,但问题是p 不是指向托管数组的指针。它是指向 的第一个元素的指针buffer
,任何编写此代码 ( Bad
) 的人都会将其语义解释为这样。你不能得到buffer
from的大小p
,也不能 call p.ToString()
,那么为什么要把它当作一个对象来对待呢?在为空的情况下buffer
,显然存在编码错误,我相信如果Bad
将在固定指针声明符处抛出异常,而不是在方法内部。
所以看起来Good
处理起来null
比做的好Bad
。空缓冲区呢?
当buffer
长度为 0 时,抛出Good
固定指针声明符。这似乎是一种处理越界数组访问的完全合理的方法。毕竟,代码应该被以同样的方式对待,显然应该抛出。IndexOutOfRangeException
&buffer[0]
&(buffer[0])
IndexOutOfRangeException
Bad
以不同的方式处理这种情况,并且再次不受欢迎。就像 if buffer
were null
, when buffer.Length == 0
,在被取消引用Bad
之前不会抛出异常的情况一样p
,那时它会抛出NullReferenceException,而不是 IndexOutOfRangeException! 如果p
从不取消引用,那么代码甚至不会抛出异常。同样,这里的想法似乎是赋予p
“指向托管数组的指针”的语义含义。再一次,我认为编写此代码的任何人都不会这么想p
。IndexOutOfRangeException
如果将代码放入固定指针声明器中,代码会更有帮助,从而通知开发人员传入的数组是空的,而不是null
.
看起来fixed(byte * p = buffer)
应该已经编译为与 was 相同的代码fixed (byte * p = &buffer[0])
。 另请注意,即使buffer
可能是任意表达式,它的类型 ( byte[]
) 在编译时是已知的,因此其中的代码Good
适用于任意表达式。
编辑
事实上,请注意, 的实现Bad
实际上对buffer[0]
两次进行了错误检查。它在方法开始时显式执行,然后在ldelema
指令处再次隐式执行。
所以我们看到Good
andBad
在语义上是不同的。 Bad
更长,可能更慢,并且当我们的代码中有错误时,当然不会给我们想要的异常,甚至在某些情况下失败的时间比它应该的要晚得多。
对于那些好奇的人,规范的第 18.6 节(C# 4.0)说在这两种失败情况下行为是“实现定义的”:
固定指针初始化器可以是以下之一:
• 代币“&” 后跟对非托管类型 T 的可移动变量(第 18.3 节)的变量引用(第 5.3.3 节),前提是类型 T* 可隐式转换为固定语句中给出的指针类型。在这种情况下,初始化程序计算给定变量的地址,并保证该变量在固定语句的持续时间内保持在固定地址。
• 具有非托管类型 T 元素的数组类型表达式,前提是类型 T* 可隐式转换为固定语句中给定的指针类型。在这种情况下,初始化程序计算数组中第一个元素的地址,并保证整个数组在固定语句的持续时间内保持在固定地址。如果数组表达式为空或数组有零个元素,则固定语句的行为是实现定义的。
...其他情况...
最后一点,MSDN 文档表明两者是“等价的”:
// 下面的两个赋值是等价的...
固定(双* p = arr){ / ... / }
固定 (double* p = &arr[0]) { / ... / }
如果两者应该是“等价的”,那么为什么对前一个语句使用不同的错误处理语义呢?
似乎还花费了额外的精力来编写Bad
. 编译后的代码在所有失败情况下都可以正常工作,并且与非失败情况下Good
的代码相同。Bad
为什么要实现新的代码路径,而不是只使用为 生成的更简单的代码Good
?
为什么以这种方式实施?