.NET MATTERS...
XML注释、迟绑定 COM 及其它
原著:Stephen Toub
翻译:Abbey
下载源代码:NETMatters0406.exe(173KB)
原文出处:.NET Matters:XML Comments, Late-bound COM, and More(MSDN Magazine June 2004)
注:此文在 MSDN 杂志上有更新。本文也一并给出。参见:编辑更新。有关更新的详细信息请参照原文。
我想从某个方法中使用该方法的 XML 说明文档,这可能吗?我希望能以下面这样的方式使用它们:
///<summary>用法: myApp.exe file1.dat file2.dat … fileN.dat</summary>
public static void Main(string [] args)
{
if (args.Length == 0)
{
Console.WriteLine(XmlComments.Current.Summary.InnerText);
return;
}
...
}
在 .NET 框架内部有这样的类吗?
C#的编译器可以直接从C#的源代码文件中提取其XML注释。如果在编译时,/doc 命令行选项(或Visual Studio .NET IDE 中同等选项),被用于指定一个文档目标文件,那么源代码中的所有注释将会被提取出来并按 XML 格式写入该目标文件中。 当这个 XML 文件与编译过的程序集在同一目录时(注意:这个 XML 文档还必须与程序集同名,但扩展名为 .XML),Visual Studio .NET 便能用这个文件显示(IntelliSense:“智能感应”)。到 Visual Studio 2005 推出时,Visual C++ 与 Visual Basic 的编译器都能支持这个特性。
.NET 框架没有提供专门用于采集和处理 XML 注释的类,但 System.Xml 命名空间所提供的 功能可被直接用于完成这样的任务。实际上,在本文附带的源代码中有一个类,借助这个类,你便可以使用你前面描述的语法。
为了获取描述特定类型或类成员的 XML 文档,你首先得找到包含那个文档的 XML 文件。为此,你必须知道该类型或类成员是在哪个程序集里声明的(译注:XML文档里的元素与程序集里的这些元素紧密关联,看看源文件,再看看生成的XML文件就明白了 )。对于给定的 MemberInfo 对象,你可以调用 MemberInfo.DeclaringType() 来获得声明该成员的 Type 对象 (如果你想要描述某个 Type 的文档,你不必获得其 DeclaringType,因为你已经具备一个有效的 Type 对象)。使用该 Type 对象的 Assembly属性 便能找到声明这个类型的程序集,然后,从属性返回的 Assembly 实例通过 Location 属性揭示文件在磁盘上的位置。然后,你可以用 .xml 替换程序集文件的扩展名,并将此 XML 文档从其所在位置加载到 XmlDocument 对象中。需要注意的是,.NET 框架中大多数程序集都是从 GAC(全局程序集缓存——Global Assembly Cache)加载的,而其 XML 文档文件都在运行时目录。如果系统从程序集所在文件夹目录加载 XML 文档失败的话,就再试试从 RuntimeEnvironment.GetRuntimeDirectory() 方法 返回的位置(详情参见上一期的专栏)。
Figure 1 编程实现对 XML 注释的访问
一旦你将程序集的 XML 注释加载到一个 XmlDocument 对象,就可以利用 XPath 表达式找到描述特定 MemberInfo 对象的注释了。其原理如 Figure 1,获取 XmlNode 的代码示例参见 Figure 2。关于如何根据 XML 文档创建 XPath 表达式,请见 Processing the XML File。
我已经创建了一个 XmlComments 类,其构造用 MemberInfo 对象作为参数,此 MemberInfo 对象正是你希望的 XML 注释。 该构造函数获取 XmlNode 以得到成员的注释(如果有的话),并用 XPath 查询解析结果查找节点以及描述概要、方法参数、能被丢出的异常等的节点清单。随后通过 XmlComments 的公共属性,如:Summary 和 Exceptions 来获得这些节点和节点清单。为了解决你所提出的问题,我添加了一个静态 Current 属性,它使用 StackTrace 类来获取方法调用框架,然后用关联的 MemberInfo 以构造一个 XmlComments 实例。
public static XmlComments Current
{
get { return new XmlComments(new StackTrace().GetFrame(1).GetMethod()); }
}
在我写这个东西时遇到了一些难题,其中最重要的一个是属性与事件的存取器(Accessor)方法。存取器没有自己的 XML 注释,所以对于 某个给定的存取器 MethodInfo,我需要存取其父辈 MethodInfo。一种方法是使用简单的串处理从存取器名字解析出属性和事件名(例如,修剪存取器名字的开始部分“get_” 或者“set_”)。我选择了一种简单实用的方法。对于一给定的类型,我利用一张哈希表来存储存取器 MethodInfo 对象到其父辈 MemberInfo 的映射(通过遍历某个 Type 对象中的所有属性和事件来创建,并将所有找到的存取器添加到这张表中)。当需要检查某个方法是否为存取器时,我可以查询这张表;如果 该 MemberInfo 是一个表中键值,那么我不仅可以肯定它是一个存取器,而且还可以立即访问其父类的 MemberInfo,并将其作为参数传递给 GetComments() 方法,如 Figure 2 所示,从而替代该存取器原来的 MethodInfo )。
当然,上述解决方案只适用于为你自己的应用程序生成 XML 注释。并且需谨记 XML 注释并未被编译进入程序集,而是以独立的 .xml 文件形式存在的 ,也就是说,使用该技术的任何方案都应该测试.xml文件是否存在,是否在正确的位置,是否已正确地命名。
我想在运行时访问一个 COM 组件,但我没有对应的 interop 程序集。我还能使用它吗?我曾考虑在需要时动态生成一个程序集,但这 样做似乎有些过度。问题是我正在编写一个.NET的通用工具类,在编译时不知道该 COM 组件的 ProgID。而这个组件实现了 IDispatch 接口。
Type 类提供了一个方法 GetTypeFromProgID(),正好适用于你的这种情形 (如果你只知道 CLSID,而不是 ProgID,可以用 Type.GetTypeFromCLSID())。将 COM 组件的 ProgID 作为参数调用该方法,它将返回描述该组件的一个 Type 对象。接着利用 Activator 类创建该组件的一个实例,再通过 Type 类的各种方法 (比如 InvokeMember、SetProperty 与 GetProperty)访问这个实例。
为了让上述工作变得更简单些,我创建了一个辅助类,如 Figure 3 所示,简化了常用的操作。看看下面这个例子,这段代码创建了一个 SharePoint.StssyncHandler COM 组件的实例,并 查询其 GetStssyncAppName 方法以确定机器上的哪个程序处理 stssync 协议:
using(LateBoundComHelper lb =
new LateBoundComHelper("SharePoint.StssyncHandler"))
{
string stsSyncHandler = (string)lb.Invoke("GetStssyncAppName");
}
如果你的系统上安装了Microsoft Outlook 2003,那么在 stsSyncHandler 最可能返回的应该是“Outlook”。
另一方面,.NET 框架的 System.Web 命名空间里也深藏了一个相似的辅助类。使用你最熟悉的反编译或者反汇编工具,可以在其中找到 System.Web.Mail.SmtpMail 类。这个 SmtpMail 类内嵌了一个LateBoundAccessHelper 类,后者提供了针对 Windows NT中的 CDONT.NewMail、Windows 2000 以上版本中的 CDO.Message 的迟绑定访问。
如果基于某些原因你仍然需要利用一个 COM 组件的类型库动态地创建一个程序集,你可以通过调用 System.Runtime.InteropServices.TypeLibConverter 类的 ConvertTypeLibToAssembly() 方法实现之。实际上,.NET 框架 SDK 中的工具 tlbimp.exe 与 tlbexp.exe 就是封装了这个类而实现的(tlbimp.exe 使用了 ConvertTypeLibToAssembly()方法,tlbexp.exe 则使用了ConvertAssemblyToTypeLib()方法 )。该方法的详细使用说明参见 TypeLibConverter.ConvertTypeLibToAssembly Method。
我们的开发团队在整个 Windows Forms 程序中使用了 Debug.Assert。当有一个断言失败时,就会 显示一个断言对话框。我想根据某种环境条件来控制该对话框的显示,让它只在某些特定的环境条件下才被显示,但又不想为此去修改每次对 Assert 的调用。这能做到吗?
如同任何一个编程挑战一样,对这个问题也有多种解决办法。一个可行的方案是从 DefaultTraceListener 派生 来编写自己的跟踪侦听器(Trace Listener)。重写其 Fail()方法,并且只在需要显示断言对话框的地方才调用基类的 Fail()方法。记住,只有在调用代码有UIPermissionWindow.SafeSubWindows 权限,并且在程序的诊断配置中启用了 AssertUIEnabled 设置,DefaultTraceListener 才会显示断言对话框。如果你自定义了跟踪侦听器,你可以 象下面这样编程实现替换 DefaultTraceListener:
System.Diagnostics.Debug.Listeners.Clear(); System.Diagnostics.Debug.Listeners.Add(new MyCustomTraceListener());或者使用一个象这样的配置文件:
<system.diagnostics> <trace autoflush="true" indentsize="0"> <listeners> <clear/> <add name="MyTraceListener" type="NetMatters.MyCustomTraceListener, MyAssembly" /> </listeners> </trace> </system.diagnostics> 如果你决定用另外的方法并且想在显示断言对话框时不依赖 Debug 或者 TraceListeners 集合,那么一个简便易行的方法就是再次使用 DefaultTraceListener 内建的功能:
new DefaultTraceListener().Fail();
不管在侦听器集合中当前配置了哪些侦听器都会显示这个断言。也请记住:将 Debug 与 Trace 的侦听器集合当做是单独的实体想法是不恰当的。其实,Debug.Listeners 与 Trace.Listeners 两者在内部都封装了TraceInternal.Listeners,因此侦听器被添加到其中某个集合实际上同时也被添加到另一个集合。事实上,Trace 和 Debug 类之间的真正差别是作为参数传递给 ConditionalAttribute 的值被附加给类的每一个方法。这个特性控制着调用现场被编译成目标 MS 中间语言( MSIL )的环境。
我的程序需要在某个缓冲区里保存最后加入的N个对象。我在整个 System.Collections 命名空间中也没能找到合适的类。 请问有没有这种现成的类?.NET 框架中有没有办法复用现成的功能使我的实现变得简单些?
据我所知,System.Collections.Queue 是.NET 框架中最接近于你的要求的类。实际上,仅需增加几行代码就能让它完全符合你的要求,完成你所需的操作。
Queue 类实现了经典的与 FIFO (First In First Out,先进先出)同名的数据结构。在其内部,它维护着一个对象数组和两个用于指示数组头尾的整数。当调用 EnQueue() 方法向队列中增加元素时,它首先会 进行内部检查以确定数组是否有足够的空间容纳新元素。若不能,便会增加数组的容量。当确认数组有足够的空间后,新元素会被添加到由整数指定的尾部位置。随着 表示尾部整数的增长,可能会导致它包合数组的起始位置,这是因为元素在出队后,头指针会增加,从而在数组的起始位置留下一个缺口。(译注:System.Collections.Queue 类使用了环形数组,应该不会出现这样的"假溢出"现象吧?)。
为了实现环形缓冲,你必须要保证队列具备最大的容量,并且决不能超过这个最大值上限。(译注:关于环形缓冲,CodeProject 中有一文,我花了一个小时也没打开。:-P)。一种方案是 改写Enqueue()方法,首先检测是否已达队列容量上限。如果是,只需简单地调用 Dequeue() 方法删除一个元素,然后委托基类继续执行。Figure 4 中的代码展示了基本的实现。注意为了让上述代码更健壮,你还需要改写并实现的其它的一些方法,如 Queue.Clone,它返回一个 Queue 实例 ,我把它作为练习留给你来完成。
我想写一个正则表达式,它能扫描只与“{”和“}”字符组合匹配的字符串(“{”后某处跟随匹配的“}”),但我无法确定表达方式。请问有可能实现吗?比如字符串“abcd{1234{5678}ef}g”是有效的, 因为有“{}”,而“}{”和“abcd{1234{5678{ef}g}”则是无效的。寻求帮助!
用数量相等的“{”和“}”匹配整个字符串,而不管它们出现的顺序, 需要一种比正则语法(那些能被正则表达式处理的)更强大的语法,用 CFG(context-free grammars)可以轻松解决。但是.NET 框架中没有提供 相应的解析类。
如果你只是想确定是否有与“{”和“}”字符的合法匹配,那可以写一些简单的代码来遍历串中的没一个字符并手工强制此规则。计数以 0 开始,每次遇到“{”时计数器就加 1,每遇到一个“}”则减1。这样当遍历完整个字符串后,如果计数器为负值,那这个字符串就是未匹配的。若计数器为0,则字符串就是匹配了的。
private bool IsMatch(string str)
{
int count=0;
for(int i=0; i<str.Length; i++)
{
if (str[i] == ''{'') count++;
else if (str[i] == ''}'') count--;
if (count < 0) return false;
}
return count == 0;
}
即便具备支持.NET 框架的 CFG 类,我想其性能也不会比我上面示范的代码好多少, 况且对任何维护该代码的人来说它也很清晰(尤其是加上注释的话)。
[编辑更新-7/20/2004:System.Text.RegularExpressions.Regex 支持用 lookahead 和 lookbehind 修饰符分组构造,这样便支持比正则语法更强大的某些语法(有关这方面的详细信息参见:“Grouping Constructs”)。因此,用数量相等的按适当规则排列的左右大括弧匹配字符串可能的模式如下:
string pattern =
@"^((?<openBracket>\{) | [^\{\}] |" +
@"(?<closeBracket-openBracket>\}))*" +
@"(?(openBracket)(?!))$";
Regex r = new Regex(pattern, RegexOptions.IgnorePatternWhitespace);
不管怎样,前面展示的 IsMatch 方法仍然比等价的 Regex 表现要好。]
我正在编写一个应用程序,它使用 HTTP 请求用户提供的不同 Web 站点的 URLs 地址。但我只想请求本地 Intranet 内的 URL。有没有什么简单易行的方法来实现这种检查?
当强制执行 CAS (Code Access Security)机制时,System.Security.Policy 命名空间提供了一个 由公共语言运行时(CLR)使用的 Zone 类。你可以显式地使用该 Zone 类来实现你所希望的那种检查。
private static bool IsIntranet(Uri url)
{
System.Security.Policy.Zone zone =
System.Security.Policy.Zone.CreateFromUrl(url.ToString());
return zone.SecurityZone == System.Security.SecurityZone.Intranet;
}
某个区域中的 URL 成员关系是基于你的 IE 配置的。为了对区域进行配置,使用 IE 选项面板中的“安全”标签页。
一个更安全的办法是在一个只允许请求 Intranet URLs 的 CAS 沙盒环境中运行这些 HTTP 请求代码。关于沙盒的详细内容,请参见 Sandboxing code dynamically(译注:我无法访问该Web页)。
有什么疑问或者意见,请给我发Email:[email protected].
Stephen Toub 是 MSDN 杂志的技术编辑。
摘自 MSDN Magazine 的 2004 年 6 月刊。此杂志可通过各地的报摊购买,也可以订阅。
本文地址:http://com.8s8s.com/it/it27809.htm