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

24
我发现了VS2015中的一个奇怪行为,以下是详细信息:
我有一个.Net 4.6项目引用了一个3.5程序集。这个程序集在其中一个接口中定义了以下方法,我可以使用Resharper反编译器进行检查。
void WriteString([MarshalAs(UnmanagedType.BStr), In] string data, [In] bool flushAndEND = true);

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

enter image description here

为了让事情变得更糟,我注意到在运行时,当只调用带有第一个参数的方法WriteString时,flushAndEND被设为false而非其在我正在引用的DLL中定义的默认值。这对我们项目的影响很大,因为它使我们应用程序的一个重要功能无法使用,并阻止了我们的一大部分回归测试。
当我调用该方法时,我可以通过强制将可选参数的值设置为true来解决这个问题,但是我担心项目中其他地方也存在同样的问题。因此,我需要一个更好的解决方案或者至少了解这种行为背后的原因。
我们几周前刚刚升级了环境。之前我们使用的是VS2013,一切都工作正常。
我知道已确认的 .Net 4.6 错误会导致某些参数传递错误的值,我可以将其与我的问题联系起来,但是正如文章中所说,该错误仅在编译为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(即true,意味着刷新缓冲区并结束命令)调用了WriteString
VS2015:使用错误的默认值flushAndEND调用了WriteString,导致超时异常。
进一步检查发现,VS2013中的对象浏览器查看器显示方法签名为:
void WriteString(string data, [bool flushAndEND = True])

在VS2015中,对象浏览器显示的方法签名为:
void WriteString(string data, [bool flushAndEND = False])

唯一的解释是,VS2015编译器从程序集中读取的默认值不正确。

2
嗯,所以IntelliSense将其显示为false,编译器解释为false,只有Resharper反编译器显示为true。这不是一个Resharper的bug吗?看起来像一个COM组件,通过运行.NET 4版本的Tlbimp.exe刷新interop库。 - Hans Passant
1
很难看到重点。将“如何使用Tlbimp.exe工具”粘贴到Google查询框中。前两个结果非常好,如果您仍然有疑问,请单击“提问”按钮。 - Hans Passant
1
我找到的唯一资料是MSDN页面,该页面解释了这个工具的功能。看起来它可以做很多有趣的事情,并且有很多命令行参数,但我没有浪费时间去阅读。因此,如果您能提供正确的参数以实现您想要的功能,那将不胜感激。此外,我已经更新了我的帖子,其中包含可能帮助读者的额外信息。 - disklosr
4
我认为这是一个好地方,可以让感兴趣的业余爱好者先处理一些非致命性问题,以避免打扰专业人士。 - Rawling
1
@PatrickHofman 这不是一个真正的错误报告(我现在已经将标题更改为一个直接的问题),我只是想寻求帮助理解为什么会出现这种错误行为,我认为Stackoverflow是解决这类问题的正确地方。 - disklosr
显示剩余6条评论
1个回答

25

好的,我找到了一种方法来复现这个错误,任何人都可以看到。尤其是需要修复此问题的 Roslyn 的微软程序员们。问题描述中已经有足够的线索表明这是一个特定于 COM 互操作库的问题。这是可行的。

我搜索了一个类型库,其中包含一个具有默认为 true 的 bool 参数的方法,该类型库可以广泛使用。恰好只有一个,真是太巧了 :) 它就是SWbemQualifierSet.Add() 方法,它接受三个布尔参数,这三个参数都有默认值为 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 值。这就是你确切的场景。其他一切的表现都像应该的那样,Object Browser 和 IntelliSense 都显示出了它们应该有的 false 值,只是它与 COM 类型库中指定的默认值不匹配。


我可以使用可用的 Tlbimp.exe 的除版本 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与Object Browser和IntelliSense的不一致并不令人意外,它肯定会自行反汇编,而不依赖于.NET元数据接口,因此将true显示为默认值。 所以我必须假定Roslyn对目标运行时版本敏感。换句话说,只有使用早于.NET 4.0的工具创建的旧COM互操作库才会出现问题。否则,这并不奇怪,因为C#直到v4才开始支持默认参数,并且存在不兼容的指定默认值的方式。最糟糕的情况是不得不使用供应商提供的PIA。减轻的情况是除了0 / false / null之外的默认值并不常见。查看有问题的库最简单的方法是使用ildasm.exe查看程序集,然后双击清单。顶部一行:
  // Metadata version: v2.0.50727

对于使用VS2015重新构建的现有项目来说,这显然会破坏它们的行为,请报告此错误。 链接到此问答以避免重复所有内容。

解决方法很简单,只需像我展示的那样使用Tlbimp.exe重新创建交互库。或者删除交互库并添加对COM组件的引用,这样在构建时将动态生成交互库。如果您依赖于供应商的PIA,则必须向他们请求更新或要求正确的过程以创建新的interop库。


感谢您花费时间和精力分析并重现错误。不幸的是,该库来自供应商,因此我无法自行重新创建它。我将不得不联系他们。我已经报告了这个错误并链接到了这个线程。 - disklosr
深入研究一些文章后,看起来一些问题只会在启用优化器的发布编译期间发生。微软于2015年11月发布了几个官方CLR补丁。希望这个特定问题也得到了解决。 - Brain2000
再次在VS2015 Update 2中进行检查,现在会生成一个奇怪的错误信息。"error CS1729: 'SWbemQualifierSetClass'不包含使用0个参数的构造函数"。好吧,它不再生成错误的代码了。取得了进展。 - Hans Passant

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接