为什么可选参数在 Visual Studio 2015 中传递了错误的值?

Why do optional parameters get passed wrong values in Visual Studio 2015?

我在 VS2015 中发现了一个奇怪的行为,详情如下:

我有一个引用 3.5 程序集的 .Net 4.6 项目。该程序集在其中一个接口中定义了以下方法,我可以使用 Resharper 反编译器检查该方法。

void WriteString([MarshalAs(UnmanagedType.BStr), In] string data, [In] bool flushAndEND = true);

注意最后一个可选参数 flushAndEND,它的默认值为 true。现在的问题是当我在我的项目中使用这个方法时,将鼠标悬停在方法名称上会显示通常的 VS 工具提示,其中详细说明了方法签名,除了对我来说它显示了可选参数 flushAndEND 的错误默认值。这是截图

更糟糕的是,我注意到在 运行 期间,当仅使用第一个参数调用方法 WriteString 时,flushAndEND 被设置为 false 而不是它在我引用的 DLL 中定义的默认值。这对我们的项目影响很大,因为它使我们应用程序的一个重要功能变得无用并阻止了我们回归测试的很大一部分。

我能够通过在调用该方法时将可选参数的值强制为 true 来解决这个问题,但我担心项目中其他地方的其他调用也遇到同样的问题。所以我需要一个更好的解决方案,或者至少了解这种行为背后的原因。

我们几周前刚刚升级了环境。在我们使用 VS2013 之前一切正常。

我知道 confirmed .Net 4.6 bug 会导致某些参数被传递错误的值,我可以将它与我的问题联系起来这里,但正如文章中所说,该错误仅在编译时发生x64 架构。我的项目是一个 WPF 应用程序,我们将其编译为 x32。

为什么调用 WriteString 时使用了错误的默认参数?

稍后我将尝试在一个小项目中隔离问题,看看是否可以重现该问题。

编辑:我设法找出了问题,并发现了一些有趣的东西!

我创建了一个简单的 .Net 4.6 控制台应用程序,添加了对我的 Dll 的引用并编写了以下简单代码,其中包括向设备发送命令并读取响应:

private static void Main(string[] args)
    {

        //Init managers
        ResourceManager ioMgr = new ResourceManagerClass();
        FormattedIO488 instrument = new FormattedIO488Class();

        //Connect to the USB device
        instrument.IO = (IMessage)ioMgr.Open("USB0::0x0957::0x0909::MY46312358::0::INSTR");


        string cmd = "*IDN?";

        //This is the problematic method from my dll
        instrument.WriteString(cmd);

        //Read the response
        string responseString = instrument.ReadString();
        Console.WriteLine(responseString);
        Console.ReadKey();
    }

我接下来做的是从 VS 2013 和 VS 2015 打开这个项目。在这两个版本的 VS 中我都重建了项目并 运行 它。以下是结果:

VS2013:使用 flushAndEND 的正确默认值调用 WriteStringtrue 意味着刷新缓冲区并结束命令)。

VS2015:使用错误的默认值 flushAndEND 调用了 WriteString,导致超时异常。

进一步检查两个版本Visual Studio发现VS2013中的对象浏览器查看器显示方法签名为:

void WriteString(string data, [bool flushAndEND = True])

而 VS2015 中的对象浏览器将方法签名显示为:

void WriteString(string data, [bool flushAndEND = False])

对此行为的唯一解释是 VS2015 编译器存在问题,无法从程序集中读取正确的默认值。

好的,我找到了一种任何人都可以自己看到的重现此错误的方法。最重要的是,在 Roslyn 上工作的 Microsoft 程序员需要解决这个问题。这个问题有足够的线索,这是一个特定于 COM 互操作库的问题。成功了。

我搜索了一个广泛可用的类型库,该类型库的方法具有 bool 参数,默认值为 true。正好有一个,几率是多少:)它是 SWbemQualifierSet.Add() method,它需要 3 个布尔参数,所有参数都默认为 true。

我首先通过 运行 从 Visual Studio 命令提示符中执行此命令来生成互操作库:

   tlbimp C:\Windows\SysWOW64\wbem\wbemdisp.tlb

生成 WbemScripting.dll 互操作库。然后编写了一个调用该方法的小测试应用程序,添加 WbemScripting.dll 互操作库作为参考:

class Program {
    static void Main(string[] args) {
        var obj = new WbemScripting.SWbemQualifierSet();
        object val = null;
        obj.Add("foo", ref val);
    }
}

请注意,它实际上并不 运行,我们只对它生成的代码感兴趣。使用 ildasm.exe:

查看程序集
  IL_001e:  ldstr      "foo"
  IL_0023:  ldloca.s   val
  IL_0025:  ldc.i4.1
  IL_0026:  ldc.i4.1
  IL_0027:  ldc.i4.1
  IL_0028:  ldc.i4.0
  IL_0029:  callvirt   instance class WbemScripting.SWbemQualifier WbemScripting.ISWbemQualifierSet::Add(string,
                                                                                                         object&,
                                                                                                         bool,
                                                                                                         bool,
                                                                                                         bool,
                                                                                                         int32)

没问题,ldc.i4.1 操作码通过 true。对象浏览器和 IntelliSense 均正确显示 true 作为默认值。


然后我 运行 我可以在我的机器上找到的最旧版本的 Tlbimp.exe。它生成一个 .NET 2.0.50727 兼容程序集:

  "C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin\TlbImp.exe" c:\windows\syswow64\wbem\wbemdisp.tlb

重建测试项目,这次是这样的:

  IL_001e:  ldstr      "foo"
  IL_0023:  ldloca.s   val
  IL_0025:  ldc.i4.0
  IL_0026:  ldc.i4.0
  IL_0027:  ldc.i4.0
  IL_0028:  ldc.i4.0
  IL_0029:  callvirt   instance class WbemScripting.SWbemQualifier WbemScripting.ISWbemQualifierSet::Add(string,
                                                                                                         object&,
                                                                                                         bool,
                                                                                                         bool,
                                                                                                         bool,
                                                                                                         int32)

问题重现,请注意 ldc.i4.0 现在如何通过 false。您的确切情况。其他一切都按应有的方式运行,对象浏览器和 IntelliSense 均按应有的方式显示 false。它只是与 COM 类型库中指定的默认值不匹配。


我可用的每个其他版本的 Tlbimp.exe,SDK 版本 7.1 及更高版本生成良好的代码。它们都生成 .NET v4.0 程序集。

确定错误的特征并不那么容易。当我反编译 "bad" 互操作库时,我没有看到明显的缺陷,它显示声明了正确的默认值:

.method public hidebysig newslot virtual instance class WbemScripting.SWbemQualifier marshal(interface) Add([in] string marshal(bstr) strName, [in] object& marshal(struct) varVal, [in][opt] bool bPropagatesToSubclass, [in][opt] bool bPropagatesToInstance, [in][opt] bool bIsOverridable, [in][opt] int32 iFlags) runtime managed internalcall
{
    .custom instance void [mscorlib]System.Runtime.InteropServices.DispIdAttribute::.ctor(int32) = { int32(2) }
    .param [3] = bool(true)
    .param [4] = bool(true)
    .param [5] = bool(true)
    .param [6] = int32(0)
    .override WbemScripting.ISWbemQualifierSet::Add
}

因此,Resharper 不同意对象浏览器和 IntelliSense 也就不足为奇了,它肯定会自行反汇编并且不依赖 .NET 元数据接口,因此默认显示 true

因此我必须假设 Roslyn 对目标 运行time 版本敏感。换句话说,这只会对使用早于 .NET 4.0 的工具创建的旧 COM 互操作库出错。否则不是疯狂 st运行ge,C# 直到 v4 才开始支持默认参数,并且有不兼容的方法来指定默认值。最坏的情况是必须使用供应商提供的 PIA。缓解情况是 0/false/null 以外的默认值并不常见。查看有问题的库的最简单方法是查看带有 ildasm.exe 的程序集,双击清单。顶行:

  // Metadata version: v2.0.50727

对于使用 VS2015 重建的现有项目,这肯定是破坏行为,请report the bug。 Link 到此 Q+A,这样您就不必重复所有内容。

解决方法很简单,只需像我展示的那样使用 Tlbimp.exe 重新创建互操作库。或者删除互操作库并添加对 COM 组件的引用,以便在构建时即时生成互操作库。如果您依赖供应商提供的 PIA,则必须向他们索取更新或创建新互操作库的正确程序。