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 的人时才使用此方法。
要完全清楚这个主题:
仅在 确实 时使用 ReleaseComObject
或 FinalReleaseComObject
,并且您完全、完全确定您的代码段是唯一的一个指的是 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
属性,您可以实施代理,但我将把它留作练习。具体来说,做一个高效的代理生成,而不是通过反射转发。
来自 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 的人时才使用此方法。
要完全清楚这个主题:
仅在 确实 时使用 ReleaseComObject
或 FinalReleaseComObject
,并且您完全、完全确定您的代码段是唯一的一个指的是 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
属性,您可以实施代理,但我将把它留作练习。具体来说,做一个高效的代理生成,而不是通过反射转发。