使用C#实现可枚举对象而不引用MSCORLIB

4
我正在创建一个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);
};

我该如何避免这个问题?

即使我只有自定义接口和一个类(没有使用任何 .NET 类型),引用仍然存在。

3个回答

2

IEnumVARIANT类型声明必须来自于某个地方。它不像每个编译器都知道的标准类型int那样。如果您自己撰写IDL,则可以使用#import "oaidl.idl"来包含定义。但是在.NET中无法使用该方法,因为类型库导出程序不使用IDL。因此,它来自于导出程序已知的一个位置,即mscorlib.tlb。

解决方法是将接口声明放入您自己的代码中,而不是使用mscorlib中的声明。可以从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; }
}

我实现了 ICarEnumerator 接口,但当我调用 For Each car in cars.GetCars() 时,VBscript 报错:对象不支持此属性或方法。有什么想法吗? - peval27
正如答案和我的先前评论所指出的那样,脚本语言中的foreach需要IEnumVARIANT。 - Hans Passant
顺便问一下,我怎样可以在VBscript中使用运算符[i]?我找不到方法。我尝试了cars[i]、cars(i)、cars.Item[i]。 - peval27
使用数组语法,Cars(ix)。当您有更多问题时,请单击“提问”按钮。 - Hans Passant
让我们在聊天中继续这个讨论 - peval27
显示剩余4条评论

1

0

“关键问题在于我不希望C++客户端应用程序需要导入mscorlib.tlb。”

这是不可能的,因为您正在使用.NET创建您的COM coclass,它会自动引入mscorlib.tlb和mscoree.dll。尝试使用一个只能将两个整数相加的简单对象进行测试。

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

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

我从关键的AllAccounts coclass开始:

//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(); }

  }
}

DispId 值 0 和 -4 是默认 Item 方法和 GetEnumerator() 方法所必需的。另外两个文件是:

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;
}

备注:在C++中,Item是一个向量。我不知道如何将其更改为通常的函数形式,即使用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

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