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

时间:2015-09-29 10:39:14

标签: c# .net visual-studio visual-studio-2015 .net-4.6

我在VS2015中发现了一个奇怪的行为以下是详细信息:

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

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

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

enter image description here

更糟糕的是,我注意到在运行时,在仅使用第一个参数调用方法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:使用CORRECT默认值WriteString调用flushAndENDtrue表示刷新缓冲区并结束命令。)

VS2015:WriteString使用WRONG默认值flushAndEND调用,它给出了超时异常。

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

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

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

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

这种行为的唯一解释是VS2015编译器无法从程序集中读取正确的默认值。

1 个答案:

答案 0 :(得分:25)

好的,我找到了一种方法来重现这个任何人都可以看到的错误。最重要的是,在Roslyn上工作的微软程序员需要解决这个问题。在问题中有足够的领先优势,这是一个特定于COM互操作库的问题。那个淘汰了。

我搜索了一个广泛使用的类型库,其方法具有 bool 参数,默认值为 true 。只有一个,赔率是多少:)它是SWbemQualifierSet.Add() method,它需要3个布尔参数,它们的默认值都是真的。

我首先通过从Visual Studio命令提示符运行此命令来生成interop库:

   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程序集。

表征错误并不容易。当我反编译“坏”互操作库时,我没有看到明显的缺陷,它显示了声明的更正默认值:

.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不同意对象浏览器和智能感知并不令人惊讶,它肯定会自行反汇编并且不依赖于.NET元数据接口,因此将 true 显示为默认值。

因此,我必须假设Roslyn对目标运行时版本很敏感。换句话说,只有使用早于.NET 4.0的工具创建的旧COM互操作库才会出错。否则不是很奇怪,C#在v4之前没有开始支持默认参数,并且存在指定默认值的不兼容方式。最坏情况是必须使用由供应商提供的PIA。减轻情况是除了0 / false / null之外的默认值并不常见。查看有问题的库的最简单方法是使用ildasm.exe查看程序集,双击Manifest。第一行:

  // Metadata version: v2.0.50727

对于使用VS2015重建的现有项目,这肯定会破坏行为,请report the bug。链接到此Q + A,因此您无需重复所有内容。

解决方法很简单,只需用Tlbimp.exe重新创建互操作库,如我所示。或者删除互操作库并添加对COM组件的引用,以便在构建时即时生成互操作库。如果您依赖供应商的PIA,那么您将不得不要求他们更新或创建新的互操作库的正确程序。