VSTO:操纵 COM 对象 ("one dot good, two dots bad")

VSTO: manipulating COM objects ("one dot good, two dots bad")

来自 Excel VBA 背景,我经常编写如下代码:

Range("myRange").Offset(0, 1).Resize(1, ccData).EntireColumn.Delete

我现在转向 VSTO,并且一直在阅读有关 RCW 计数器等的内容,以及显式释放 COM 对象的需要。基本建议似乎是:不要将对 Excel 对象的引用链接在一起(正如我上面所说的)——因此 "one dot good, two dots bad"。 我的问题是,上面的代码不是进入 VSTO 的方式,我是否正确?如果是这样,是否意味着我需要明确声明上述链中隐含的 3 个范围(Offset、Resize 和 EntireColumn)?

或者甚至像这样的东西怎么样:

rng.Columns.Count

其中 rng 是声明的范围?我应该为 rng.Columns 分配一个名称以获得范围内的列数吗?

am I correct that the above code is not the way to go in VSTO?

是的,您走对了路。您需要声明不同的对象以便稍后释放它们。使用完 Office 对象后,使用 System.Runtime.InteropServices.Marshal.ReleaseComObject 释放它。然后在 Visual Basic 中将变量设置为 Nothing(在 C# 中为 null)以释放对该对象的引用。

您可以在 Systematically Releasing Objects 文章中阅读更多相关信息。它与 Outlook 相关,但同样的原则可以应用于所有 Office 应用程序。

顺便说一句:它不依赖于 VSTO。它来自 COM 世界...

"two dot rule" 愚蠢背后有非常有害的货物崇拜,它完全不能让 C# 程序员从第 4 版开始摆脱困境。而且它比 simple way 制作 Office 程序要痛苦得多按需退出。

但这根本不是你遇到的问题,cargo cult 仅适用于使用 Automation 激活 Office 程序的进程外程序。您的代码实际上在 inside Office 程序中运行,您当然不关心程序何时终止。因为这也会终止您的代码。

只需按照编写常规 C# 代码的方式编写代码,GC 不需要任何帮助。

正如小肯定经常发生的那样,双点规则需要进一步解释。可以当助记词使用。

原因是您在 .NET 中获得的每个新 RCW 都将包含对相应 COM 对象的引用。所以,如果你有(假设 obj 是一个 RCW,这是你第一次获得其他对象):

obj.Property[0].MethodThatReturnsAnotherObject()
//     1     2                  3

您将获得 3 个额外的 RCW。如您所见,只有 1 个额外的点。尽管属性可能是获取其他 COM 对象的最常见方式,但这并不是唯一的方式。

通常,每个 RCW 只会在垃圾回收时释放底层 COM 对象,除非您使用 Marshal.ReleaseComObject。仅当您完全确定您是唯一使用您要发布的 RCW 的人时才使用此方法。


要完全清楚这个主题:

仅在 确实 时使用 ReleaseComObjectFinalReleaseComObject,并且您完全、完全确定您的代码段是唯一的一个指的是 RCW。


<type> propObj;
try
{
    propObj = obj.Property;
    <type> propArrayObj;
    try
    {
        propArrayObj = propObj[0];
        <type> propArrayObjReturn;
        try
        {
            propArrayObjReturn = propArrayObj.MethodThatReturnsAnotherObject();
        }
        finally
        {
            if (propArrayObjReturn != null) Marshal.ReleaseComObject(propArrayObjReturn);
        }
    }
    finally
    {
        if (propArrayObj != null) Marshal.ReleaseComObject(propArrayObj);
    }
}
finally
{
    if (propObj != null) Marshal.ReleaseComObject(propObj);
}

这很乏味,包装器在这里可能会有所帮助:

using System;
using System.Runtime.InteropServices;
using System.Threading;

public class ComPtr<T> : IDisposable where T : class
{
    public ComPtr(T comObj)
    {
        if (comObj == null) throw new ArgumentNullException("comObj");

        if (!typeof(T).IsInterface)
        {
            throw new ArgumentException("COM type must be an interface.", "T");
        }
        // TODO: check interface attributes: ComImport or ComVisible, and Guid

        this.comObj = comObj;
    }

    private T comObj;

    public T ComObj
    {
        get
        {
            // It's not best practice to throw exceptions in getters
            // But the alternative might lead to a latent NullReferenceException
            if (comObj == null)
            {
                throw new ObjectDisposedException("ComObj");
            }

            return comObj;
        }
    }

    ~ComPtr()
    {
        Dispose(false);
    }

    // IDisposable
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected void Dispose(bool disposing)
    {
        #if !RELEASECOMPTR

        // Option 1: Safe.  It might force the GC too often.
        // You can probably use a global limiter, e.g. don't force GC
        // for less than 5 seconds apart.
        if (Interlocked.Exchange(ref comObj, null) != null)
        {
             // Note: GC all generations
            GC.Collect();
            // WARNING: Wait for ALL pending finalizers
            // COM objects in other STA threads will require those threads
            // to process messages in a timely manner.
            // However, this is the only way to be sure GCed RCWs
            // actually invoked the COM object's Release.
            GC.WaitForPendingFinalizers();
        }

        #else

        // Option 2: Dangerous!  You must be sure you have no other
        // reference to the RCW (Runtime Callable Wrapper).
        T currentComObj = Interlocked.Exchange(ref comObj, null);
        if (currentComObj != null)
        {
            // Note: This might (and usually does) invalidate the RCW
            Marshal.ReleaseComObject(currentComObj);
            // WARNING: This WILL invalidate the RCW, no matter how many
            // times the object reentered the managed world.
            // However, this is the only way to be sure the RCW's
            // COM object is not referenced by our .NET instance.
            //Marshal.FinalReleaseComObject(currentComObj);
        }

        #endif
    }
}

这会使前面的示例更友好一些:

using (var prop = new ComObj<type>(obj.Property))
{
    using (var propArray = new ComObj<type>(prop.ComObj[0]))
    {
        using (var propArrayReturn = new ComPtr<type>(propArray.ComObj.MethodThatReturnsAnotherObject()))
        {
        }
    }
}

为了避免 ComObj 属性,您可以实施代理,但我将把它留作练习。具体来说,做一个高效的代理生成,而不是通过反射转发。