C# COM 实现可枚举而不引用 MSCORLIB

C# COM implement enumerable without referencing MSCORLIB

我正在创建一个 COM 接口,它应该允许在 Visual Basic 脚本中使用 For Each 并在 C++ 中使用 IEnumVariant。问题是我不希望 C++ 客户端应用程序需要导入 mscorlib.tlb.

到目前为止我的界面是:

[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface ICars : System.Runtime.InteropServices.ComTypes.IEnumVARIANT
{
    int Count { get; }
}

[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public class Cars : ICars
{
    int ICars.Count => throw new NotImplementedException();

    int IEnumVARIANT.Next(int celt, object[] rgVar, IntPtr pceltFetched)
    {
        throw new NotImplementedException();
    }

    int IEnumVARIANT.Skip(int celt)
    {
        throw new NotImplementedException();
    }

    int IEnumVARIANT.Reset()
    {
        throw new NotImplementedException();
    }

    IEnumVARIANT IEnumVARIANT.Clone()
    {
        throw new NotImplementedException();
    }
}

TlbExp 吐出这段代码:

// Generated .IDL file (by the OLE/COM Object Viewer)
// 
// typelib filename: carsIEnumerator.tlb

[
     uuid(3BBCEAA2-9498-48BF-8053-1CEFB3C1C86F),
     version(1.0),
     custom(90883F05-3D28-11D2-8F17-00A0C9A6186D,  "ClassLibraryIEnumerator, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")

 ]
 library ClassLibraryIEnumerator
 {
     // TLib :     // TLib : mscorlib.dll : {BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}
importlib("mscorlib.tlb");
// TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
importlib("stdole2.tlb");

// Forward declare all types defined in this typelib
interface ICars;

[
  odl,
  uuid(ABD2A9E4-D5C5-3ED9-88AF-4C310BD5792D),
  version(1.0),
  dual,
  oleautomation,
  custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, "ClassLibraryIEnumerator.ICars")    

]
interface ICars : IDispatch {
    [id(0x60020000), propget]
    HRESULT Count([out, retval] long* pRetVal);
};

我怎样才能避免这种情况?

即使我只有自定义界面和一个 class(不使用任何 .NET 类型),引用仍然存在。

IEnumVARIANT 类型声明必须来自某处。它不是每个编译器都知道的标准类型,如 int。如果您自己编写 IDL,那么您将使用 #import "oaidl.idl" 来包含定义。但这在 .NET 中行不通,因为类型库导出器不使用 IDL。所以它来自出口商确实知道的地方,mscorlib.tlb

解决方法是将接口声明放在您自己的代码中,而不是使用 mscorlib 中的接口声明。 Copy/paste 它来自 Reference Source 或这个:

[Guid("00020404-0000-0000-C000-000000000046")]   
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
[ComImport]
public interface IEnumVARIANT
{
    [PreserveSig]
    int Next(int celt, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0), Out] object[] rgVar, IntPtr pceltFetched);

    [PreserveSig]
    int Skip(int celt);

    [PreserveSig]
    int Reset();

    IEnumVARIANT Clone();
}

并在您的 ICars 声明中使用 YourNamespace.IEnumVARIANT。


声明自己的枚举器接口类型也是一种解决方案,IEnumVARIANT 不会赢得任何奖品。你可以放弃没有人使用的靠不住的方法,你可以让它成为类型安全的。如果您也控制客户端代码或者不必让脚本语言中的 foreach 满意,则这是一个可接受的替代方案。考虑:

[ComVisible(true)]
public interface ICarEnumerator {
    ICar Next();
}

以及ICars界面中的ICarEnumerator GetCars()


最后但同样重要的是,考虑完全不实现迭代器。在客户端代码中让它看起来像一个数组:

[ComVisible(true)]
public interface ICars
{
    int Count { get; }
    ICar this[int index] { get; }
}

我也有同样的issue/need,我找到了这篇好文章。

https://limbioliong.wordpress.com/2011/10/28/exposing-an-enumerator-from-managed-code-to-com/

“我不希望 C++ 客户端应用程序需要导入 mscorlib.tlb。”

这是不可能的,因为您正在使用 .NET 创建 COM 组件类,它会自动将 mscorlib.tlb 和 mscoree.dll 引入游戏。用一个只能加两个整数的简单对象试试吧。

正如 Hans Passant 指出的那样,您根本不需要接口 IEnumVARIANT。任何 COM 集合都必须基于 C# 集合,例如 List<T>。这个 C# 集合有一个方法 GetEnumeration(),它吐出一个 IEnumeration 对象作为 COM 中的 IEnumVARIANT。您所要做的就是在接口中包含 IEnumerator GetEnumerator(); 并将实现委托给 C# 集合的 GetEnumeration() 方法。

我在一个完整的例子中展示了这一点。考虑管理帐户集合的 Bank coclass。我需要 Bank、Account 和 AllAccounts 集合的 coclasses。

我从关键的 coclass AllAccounts 开始:

//AllAccounts.cs:
using System;
using System.Collections;
using System.Runtime.InteropServices;

namespace BankServerCSharp
{
  [ComVisible(true)]  // This is mandatory.
  [InterfaceType(ComInterfaceType.InterfaceIsDual)]
  public interface IAllAccounts
  {
    int Count{ get; }

    [DispId(0)]
    IAccount Item(int i);

    [DispId(-4)]
    IEnumerator GetEnumerator();

    Account AddAccount();

    void RemoveAccount(int i);

    void ClearAllAccounts();
  }

  [ComVisible(true)]  // This is mandatory.
  [ClassInterface(ClassInterfaceType.None)]
  public class AllAccounts:IAllAccounts 
  {
    private AllAccounts(){ } // private constructor, coclass noncreatable
    private List<IAccount> Al = new List<IAccount>();
    public static AllAccounts MakeAllAccounts() { return new AllAccounts(); }
    //public, but not exposed to COM

    public IEnumerator GetEnumerator() { return Al.GetEnumerator();  }

    public int Count { get { return Al.Count; } }

    public IAccount Item(int i)  { return (IAccount)Al[i - 1];  }

    public Account AddAccount() { Account acc = Account.MakeAccount();
                                        Al.Add(acc); return acc; }

    public void RemoveAccount(int i) {  Al.RemoveAt(i - 1);  }

    public void ClearAllAccounts() { Al.Clear(); }

  }
}

默认 Item 方法和 GetEnumerator() 方法需要 DispId 值 0 和 -4。其他两个文件是:

Account.cs:
using System.Runtime.InteropServices;

namespace BankServerCSharp
{
  [ComVisible(true)]  // This is mandatory.
  [InterfaceType(ComInterfaceType.InterfaceIsDual)]
  public interface IAccount
  {
    double Balance { get; } // A property
    void Deposit(double b); // A method
  }

  [ComVisible(true)]  // This is mandatory.
  [ClassInterface(ClassInterfaceType.None)]
  public class Account:IAccount
  {
    private double mBalance = 0;
    private Account() { }     // private constructor, coclass noncreatable

    public static Account MakeAccount() { return new Account(); }
    //MakeAccount is not exposed to COM, but can be used by other classes

    public double Balance  { get {  return mBalance; } }
    public void Deposit(double b) { mBalance += b; }
  }
}

Bank.cs:
using System.Runtime.InteropServices;

namespace BankServerCSharp
{
  [ComVisible(true)]  // This is mandatory.
  [InterfaceType(ComInterfaceType.InterfaceIsDual)]
  public interface IBank { IAllAccounts Accounts { get; }  }

  [ComVisible(true)]  // This is mandatory.
  [ClassInterface(ClassInterfaceType.None)]
  public class Bank:IBank
  {
    private readonly AllAccounts All;
    public Bank() {  All = AllAccounts.MakeAllAccounts(); } 
    public IAllAccounts Accounts {  get { return All; } }
  }
}

您必须使用 x64 版本的 Regasm 注册服务器。

使用 C++ 测试服务器:

#include "stdafx.h"
#include <string>
#import  "D:\Aktuell\CSharpProjects\BankServerCSharp\BankServerCSharp\bin\Release\BankServerCSharp.tlb"
//this is the path of my C# project's bin\Release folder
inline void TESTHR(HRESULT x) { if FAILED(x) _com_issue_errorex(x, nullptr, ID_NULL);}
int main()
{
  try
  {
    TESTHR(CoInitialize(0));
    BankServerCSharp::IBankPtr BankPtr = nullptr;
    TESTHR(BankPtr.CreateInstance("BankServerCSharp.Bank"));
    BankServerCSharp::IAllAccountsPtr AllPtr = BankPtr->Accounts;
    BankServerCSharp::IAccountPtr FirstAccountPtr = AllPtr->AddAccount();
    TESTHR(FirstAccountPtr->Deposit(47.11));
    AllPtr->AddAccount();
    TESTHR(AllPtr->Item[2]->Deposit(4711));

    CStringW out, add;
    for (int i = 1; i <= AllPtr->Count; i++)
    {
      add.Format(L"Balance of account %d: %.2f.\n", i, AllPtr->Item[i]->Balance);
      out += add;
    }

    out += L"\n";
    AllPtr->RemoveAccount(1);
    for (int i = 1; i <= AllPtr->Count; i++)
    {
      add.Format(L"Balance of account %d: %.2f.\n", i, AllPtr->Item[i]->Balance);
      out += add;
    }

    AllPtr->ClearAllAccounts();
    add.Format(L"Number of accounts: %ld.\n", AllPtr->Count);
    out += L"\n" + add;
    MessageBoxW(NULL, out, L"Result", MB_OK);

    //Raise an exception:
    AllPtr->RemoveAccount(1);
  }
  catch (const _com_error& e)
  {
    MessageBoxW(NULL, L"Oops! Index out of range!", L"Error", MB_OK);
  }
  CoUninitialize();// Uninitialize COM
  return 0;
}

备注:Item是C++中的vector。我不知道如何将其更改为通常的函数形式,即 Item(i) 而不是 Item[i].

在VBA中,你可以使用心爱的For Each循环:

Sub CSharpBankTest()
 On Error GoTo Oops

  Dim Out As String
  Dim Bank As New BankServerCSharp.Bank            'New!

  Dim AllAccounts As BankServerCSharp.AllAccounts  'No New!
  Set AllAccounts = Bank.Accounts

  Dim AccountOne As BankServerCSharp.Account       'No New
  Set AccountOne = AllAccounts.AddAccount
  AccountOne.Deposit 47.11

  AllAccounts.AddAccount
  AllAccounts(2).Deposit 4711

  Dim i As Long
  Dim ac As BankServerCSharp.Account
  For Each ac In AllAccounts
    i = i + 1
    Out = Out & "Balance of account " & i & ": " & ac.Balance & vbNewLine
  Next
  Exit Sub
Oops:
  MsgBox "Error Message : " & Err.Description, vbOKOnly, "Error"
End Sub