为什么MSFT C#对固定的“指针衰减数组”和“第一个元素的地址”的编译方式不同? - c#

.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 +”进行编译。
这是方法Good生成的IL:
好()

  .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

这是方法Bad生成的IL:
坏()

  .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。

以下是“错误”的功能:

如果buffer为null,则转到3。
如果buffer.Length!= 0,则转到5。
将值0存储在本地插槽0中,
转到6。
获取缓冲区[0]的地址。
引用该地址(在本地插槽0中,该地址可能为0或现在处于缓冲区中)。
使用该取消引用的值调用WriteLine。

buffer既非空又非空时,这两个函数执行相同的操作。请注意,在进入Bad函数调用之前,WriteLine只是跳了几圈。
buffer为null时,Good在固定指针声明符(NullReferenceException)中抛出byte * p = &buffer[0]。大概这是修复托管数组的理想行为,因为通常,固定语句内部的任何操作都取决于被修复对象的有效性。否则,为什么该代码会放在fixed块中?当Good传递空引用时,它会在fixed块开始时立即失败,从而提供相关且有用的堆栈跟踪。开发人员将看到这一点,并意识到他应该在使用buffer之前对其进行验证,否则他的逻辑可能会错误地将null分配给buffer。无论哪种方式,都不希望使用fixed托管数组明确输入null块。
Bad以不同的方式处理这种情况,甚至是不希望的。您可以看到,在取消引用Bad之前,p实际上不会引发异常。这样做是通过将null分配给包含p的同一本地插槽,然后在fixed块语句取消引用p时引发异常的一种about回方式。
以这种方式处理null具有使C#中的对象模型保持一致的优势。也就是说,在fixed块内,p在语义上仍被视为一种“指向托管数组的指针”,当为null时,直到(或除非)将其取消引用,它才会引起问题。一致性很好,但是问题是p不是指向托管数组的指针。它是buffer第一个元素的指针,任何编写此代码(Bad)的人都会这样解释其语义。您无法从buffer获取p的大小,也无法调用p.ToString(),那么为什么将其视为对象呢?在buffer为null的情况下,显然存在编码错误,并且我相信,如果Bad将在固定指针声明符而不是方法内部抛出异常,将大有帮助。
因此,似乎Goodnull的处理要好于Bad。空缓冲区呢?
buffer的长度为0时,Good在固定指针声明符处抛出IndexOutOfRangeException。这似乎是处理边界数组访问的一种完全合理的方法。毕竟,应该将代码&buffer[0]&(buffer[0])对待,显然应该抛出IndexOutOfRangeException
Bad以不同的方式处理这种情况,这也是不希望的。就像buffernull的情况一样,当buffer.Length == 0时,Bad在取消引用p之前不会引发异常,并且那时它会引发NullReferenceException,而不是IndexOutOfRangeException!如果从未取消引用p,则该代码甚至不会引发异常。同样,这里的想法似乎是赋予p“指向托管数组的指针”的语义。再一次,我认为编写此代码的人不会想到p这样的方式。如果将代码IndexOutOfRangeException放在定点声明符中,从而通知开发人员传入的数组为空而不是null,则该代码将更加有用。
看来fixed(byte * p = buffer)应该已经编译为与fixed (byte * p = &buffer[0])相同的代码。还要注意,即使buffer可以是任意表达式,其类型(byte[])在编译时也是已知的,因此Good中的代码适用于任意表达式。
编辑
实际上,请注意,Bad的实现实际上对buffer[0]进行了两次错误检查。它在方法开始时显式地执行,然后在ldelema指令中隐式地再次执行。

因此,我们看到GoodBad在语义上是不同的。 Bad更长,可能更慢,并且当我们的代码中有错误时,肯定不会给我们带来令人期望的异常,甚至比某些情况下的失败要晚得多。
对于那些好奇的人,规范(C#4.0)的18.6节指出,在这两种失败情况下,行为都是“实现定义的”:

固定指针初始化程序可以是以下之一:
•标记“&”后跟对非托管类型T的可移动变量(第18.3节)的变量引用(第5.3.3节),前提是类型T *可隐式转换为固定语句中给出的指针类型。在这种情况下,初始化程序将计算给定变量的地址,并保证该变量在固定语句的持续时间内保持在固定地址上。
•带有非托管类型T的元素的数组类型的表达式,条件是类型T *可隐式转换为固定语句中给出的指针类型。在这种情况下,初始化程序将计算数组中第一个元素的地址,并保证整个数组在固定语句期间保持在固定地址。如果数组表达式为null或数组具有零个元素,则固定语句的行为由实现定义。
...其他情况...

最后一点,MSDN documentation表明两者是“等效的”:

//以下两个分配是等效的...
固定(double * p = arr){/.../}
固定(double * p =&arr [0]){/.../}

如果这两个假定是“等效的”,那么为什么对前一个语句使用不同的错误处理语义?
似乎还付出了额外的精力来编写Bad中生成的代码路径。 Good中的已编译代码在所有失败情况下均能正常工作,在非失败情况下与Bad中的代码相同。为什么要实现新的代码路径,而不仅仅是使用为Good生成的简单代码?
为什么以这种方式实施?

参考方案

您可能会注意到,所包含的IL代码几乎逐行实现了规范。这包括在相关的情况下明确实现规范中列出的两个异常情况,在不相关的情况下不包括代码。因此,编译器以其行为方式工作的最简单原因是“因为规范如此规定”。

当然,这只会导致我们进一步提出两个问题:

为什么C#语言小组选择以这种方式编写规范?
为什么编译器团队选择了特定的实现定义的行为?

除了没有合适的团队成员出现之外,我们真的不能希望完全回答这两个问题。但是,我们可以尝试遵循他们的推理来回答第二个问题。

回想一下,该规范说,在将数组提供给固定指针初始化程序的情况下,

如果数组表达式为null或数组具有零个元素,则固定语句的行为由实现定义。

由于在这种情况下实现可以自由选择执行任何操作,因此我们可以假定,对于编译器团队而言,最合理,最便宜的行为就是任何合理的行为。

在这种情况下,编译器团队选择做的是“在代码做错事情的地方抛出异常”。考虑如果代码不在固定指针初始化器中,代码将如何处理,并考虑发生了什么。在“良好”示例中,您尝试获取一个不存在的对象的地址:空/空数组中的第一个元素。那实际上不是您可以做的,因此会产生异常。在“ Bad”示例中,您只是将参数的地址分配给指针变量。 byte * p = null是完全合法的声明。仅当您尝试WriteLine(*p)时才会发生错误。由于在这种例外情况下,允许固定指针初始化程序执行任何它想做的事情,所以最简单的事情就是允许赋值发生,尽管它是毫无意义的。

显然,这两个陈述并不完全等同。我们可以通过以下事实来说明这一点:标准对待它们的方式有所不同:

&arr[0]是:“标记“&”后跟变量引用”,因此编译器计算arr [0]的地址
arr是:“数组类型的表达式”,因此编译器计算该数组第一个元素的地址,但要注意的是,空或长度为0的数组会产生您所看到的实现定义的行为。

只要数组中有一个元素,这两者就会产生等效的结果,这就是MSDN文档试图克服的问题。提出有关为何未明确定义的行为或实施定义的行为以其行为方式行事的问题并不能真正帮助您解决任何特定问题,因为您将来不能依靠它成为真实。 (话虽如此,我当然很想知道思考过程是什么,因为您显然无法“修复”内存中的空值...)

ddl在服务器中未更新-asp.net - javascript

我在ASP.NET c#上工作。我有一个DropDownList。 (runat =“ server”)在$ {document).ready上,我更新了它的值:$(document).ready(function () { document.getElementById("ddl").value = "abc"; ……

在ASP.NET MVC中创建数据库回调的最有效方法 - c#

我有一个ASP.NET MVC网页,该网页基本上通过日期过滤器显示MS SQL数据库中表的行。当新行插入数据库表时,我想用新行列表更新网页视图。实现此目标的最有效方法是什么?基本上,我想从我的JavaScript创建一个到数据库服务器的回调,以用新结果更新UI。假设数据库表中的行数很大。(〜1百万)谢谢,cas 参考方案 如果数据库更新非常频繁,则可以按特定…

如何使用JavaScript访问嵌入式ASP.NET GlobalResources? - javascript

我正在开发一个遗留的ASP.NET项目,该项目正试图缓慢地进行调整,但是如果没有像巧克力手指屋一样塌陷的情况,我将无法进行重大更改。我试图为此找到解决方案,但由于术语的特定混合(“ javascript”,“ embedded”和/或“ resource”只是为我提供了有关如何嵌入.js文件的信息,而失败了)。 。),这可能是一种怪异的处理方式。该项目将Ap…

如何使用ASP.NET ViewState使用JavaScript - c#

我的页面中有UL,它为空。我开始使用JavaScript使用LI填充它。在回发阶段如何在asp.net中使用此新添加的动态数据?那是因为我的提交按钮是asp.net控件。我不想使用JS POST。谢谢 参考方案 我过去通过在隐藏字段中填充要发布的动态数据来完成此类操作,您可以-在回传之前触发JavaScript事件,该事件将数据从UL解析到隐藏字段中要么更新…

如何从.Net DLL获取公共出口列表? - c#

我可以使用“ dumpbin”和“ dll export”之类的工具来查看标准win32 DLL的公共入口点(“ exports”),例如Windows \ SYSTEM32 \ GDI32.dll。但是,当我在.Net DLL上使用这些相同的工具时,我看到的仅仅是 2000 .reloc 2000 .rsrc 48000 .text 我有一个C#/。Net…