C#编译器如何检测COM类型?

169

编辑:我已经把结果写成了一篇博客文章


C#编译器对COM类型有点神奇。例如,这个语句看起来很正常...

Word.Application app = new Word.Application();

直到你意识到Application是一个接口,调用接口的构造函数?糟糕!实际上这被转换为对Type.GetTypeFromCLSID()Activator.CreateInstance的调用。

此外,在C# 4中,您可以使用非ref参数作为ref参数,并且编译器只会添加一个本地变量来引用传递,丢弃结果:

// FileName parameter is *really* a ref parameter
app.ActiveDocument.SaveAs(FileName: "test.doc");

(是的,有很多参数缺失。可选参数不错吧?:)

我正在尝试调查编译器的行为,但我无法伪造第一部分。我可以轻松完成第二部分:

using System;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;

[ComImport, GuidAttribute("00012345-0000-0000-0000-000000000011")]
public interface Dummy
{
    void Foo(ref int x);
}

class Test
{
    static void Main()
    {
        Dummy dummy = null;
        dummy.Foo(10);
    }
}

我希望能够写:

Dummy dummy = new Dummy();

不过,显然它在执行时会崩溃,但没关系,我只是在尝试。

编译器为链接的COM PIAs添加的其他属性(CompilerGeneratedTypeIdentifier)似乎没有起作用......那么魔法酱汁是什么?


11
可选参数不好吗?在我看来,它们并不好。微软试图通过向C#添加臃肿的代码来修复Office COM接口中的缺陷。 - Mehrdad Afshari
19
可选参数在COM之外也很有用。当然,你需要小心默认值,但是通过它们和命名参数,构建可用的不可变类型就更容易了。 - Jon Skeet
1
正确。具体而言,命名参数在与某些动态环境的互操作性方面可能实际上是必需的。当然,毫无疑问,这是一个有用的特性,但并不意味着它是免费的。它付出了简单性(这是明确规定的设计目标)。就我个人而言,我认为C#非常棒,因为团队留下了一些功能(否则,它可能会成为C ++副本)。C#团队非常出色,但公司环境几乎不可能没有政治。我 Anders本人对此并不是很满意,正如他在PDC'08讲演中所说:“我们花了十年时间回到了最初的状态。” - Mehrdad Afshari
7
我同意团队需要密切关注复杂性。动态内容对于大多数开发人员来说增加了很多复杂性,但对于一些开发人员来说却具有很高的价值。 - Jon Skeet
1
我看到框架开发人员开始在许多地方讨论其用途。在我看来,只是时间问题,直到我们找到了dynamic的好用处...我们太习惯于静态/强类型,以至于看不出它在COM之外的重要性。 - chakrit
显示剩余4条评论
4个回答

146

我绝非这方面的专家,但最近偶然发现了我认为你需要的东西: CoClass 属性类。

[System.Runtime.InteropServices.CoClass(typeof(Test))]
public interface Dummy { }

一个 coclass 提供了一个或多个接口的具体实现。在 COM 中,这样的具体实现可以使用任何支持 COM 组件开发的编程语言编写,例如 Delphi、C++、Visual Basic 等。

请参阅我对类似于 Microsoft Speech API 的另一个问题的回答,您可以“实例化”接口 SpVoice(但实际上,您正在实例化 SPVoiceClass)。

[CoClass(typeof(SpVoiceClass))]
public interface SpVoice : ISpeechVoice, _ISpeechVoiceEvents_Event { }

2
非常有趣 - 以后会尝试。然而,链接的 PIA 类型并没有 CoClass。也许这与链接过程有关 - 我将在原始 PIA 中查看... - Jon Skeet
65
当Eric Lippert和Jon Skeet也回答时,你写下了被接受的答案,真的很棒,所以加一分支持。不过,真正让我加一分是因为提到了CoClass。 - OregonGhost

61

你和Michael几乎已经拼凑出了这个问题的答案。我认为它是这样工作的。(我没有编写代码,所以可能有些表述不太准确,但我相信大致就是这样.)

如果:

  • 你正在“new”一个接口类型,并且
  • 该接口类型具有已知的coclass,并且
  • 你正在为此接口使用“no pia”功能

那么生成的代码为(IPIAINTERFACE)Activator.CreateInstance(Type.GetTypeFromClsid(GUID OF COCLASSTYPE))

如果:

  • 你正在“new”一个接口类型,并且
  • 该接口类型具有已知的coclass,并且
  • 你没有使用“no pia”功能为此接口

那么生成的代码就像你说“new COCLASSTYPE()”一样。

Jon, 如果你对这些内容有疑问,请随时直接找我或Sam。FYI,Sam是这个功能的专家。


36

好的,这只是对Michael的答案进行更详细的说明(如果他想要添加内容,他可以这样做,这样我就会删除这个)。

查看Word.Application的原始PIA,涉及到三种类型(忽略事件):

[ComImport, TypeLibType(...), Guid("..."), DefaultMember("Name")]
public interface _Application
{
     ...
}

[ComImport, Guid("..."), CoClass(typeof(ApplicationClass))]
public interface Application : _Application
{
}

[ComImport, ClassInterface(...), ComSourceInterfaces("..."), Guid("..."), 
 TypeLibType((short) 2), DefaultMember("Name")]
public class ApplicationClass : _Application, Application
{
}

为什么会有两个界面,这是Eric Lippert在另一个答案 中谈到的原因。正如你所说,这里有CoClass——既包括类本身,也包括Application接口上的属性。

现在,如果我们在C# 4中使用PIA链接,则其中的一些部分将嵌入到生成的二进制文件中……但不是全部。只创建Application实例的应用程序最终会得到这些类型:

[ComImport, TypeIdentifier, Guid("..."), CompilerGenerated]
public interface _Application

[ComImport, Guid("..."), CompilerGenerated, TypeIdentifier]
public interface Application : _Application

没有ApplicationClass - 这可能是因为它将在执行时从真实的COM类型中动态加载。

另一个有趣的事情是链接版本和非链接版本之间的代码差异。如果您反编译该行:

Word.Application application = new Word.Application();

在所引用的版本中,它最终变成:

Application application = new ApplicationClass();

而在链接版本中,它最终变成了

Application application = (Application) 
    Activator.CreateInstance(Type.GetTypeFromCLSID(new Guid("...")));

看起来“真正的” PIA 需要 CoClass 属性,但是链接版本不需要,因为没有编译器可以实际引用的 CoClass。它必须动态地执行。

我可能会尝试使用这些信息伪造一个 COM 接口,看看是否可以让编译器将其链接...


27

仅为证实Michael的答案,补充一点:

以下代码可以编译并运行:

public class Program
{
    public class Foo : IFoo
    {
    }

    [Guid("00000000-0000-0000-0000-000000000000")]
    [CoClass(typeof(Foo))]
    [ComImport]
    public interface IFoo
    {
    }

    static void Main(string[] args)
    {
        IFoo foo = new IFoo();
    }
}

为使其正常工作,您需要同时使用ComImportAttributeGuidAttribute

另请注意,当您将鼠标悬停在new IFoo()上时的信息:Intellisense正确地捕捉了这些信息:不错!


谢谢,我一直在尝试,但是我缺少了 ComImport 属性,但是当我进入源代码时,我使用 F12 工具只显示了 CoClassGuid,这是为什么呢? - Ehsan Sajjad

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