RegexWrap
为祝贺 MSDN 杂志二十周年纪念,现在我解释了 ManWrap,接下来是做一些包装的时候了!我用 ManWrap 将 .NET 的 Regex
类包装在一个叫做 RegexWrap.dll 的 DLL 中。如 Figure 7
所示,一个经过删节的头文件。因为细节很琐碎,我就不作全面解释了,以下是一个典型的包装器:
CString CMRegex::Replace(LPCTSTR input, LPCTSTR replace)
{
return (*this)->Replace(input, replace);
}
实际上在每一个案例中,实现就一行:调用底层的托管方法并让编译器转换参数。interop(互用性)不是很好玩吗?即便参数为另一个包装类它也照样工作,就象我在
CMRegex::Match 中已经解释的那样。
当然,并不是所有的东西都琐碎。我在创建 RegexWrap
的过程中确实也碰到过一些不顺和阻碍:集合(collections)、委托(delegates)、异常(exceptions)、数组(arrays)和枚举(enums)。下面我将一一描述是如何处理它们的。
集合处理
框架中集合无处不在。例如,Regex::Matches 将所有匹配作为 MatchCollection 返回,Match::Groups 返回的所有
Groups 是 GroupCollection。我处理集合的第一个想法是将它们转换为包装对象的 STL
容器。接着我认识到这是个坏主意。为什么要创建一组已经在集合里的指向对象的新句柄呢?虽然 .NET 的 Collections 在某些方面类似 STL
容器,但它们并不完全相同。例如,你可以通过整数索引或字符串名来存取某个 GroupCollection。
与其使用 STL vector 或 map,还不如简单一点,使用我已经建立的系统,即 ManWrap。如 Figure 8 所示,我展示了如何包装
GroupCollection。它正是你所期望的,只是新加了一个宏,DECLARE_COLLECTION,它与
DECLARE_WRAPPER 所做的事情一样,此外还添加了三个所有集合都固有的方法:Count、IsReadOnly 和 IsSynchronized。自然少不了
IMPLEMENT_COLLECTION 来实现这些方法。既然 GroupCollection 让你用整数或字符串来索引,那么包装器有两个 operator[]
重载。
一旦我包装了 Match、Group 和 CaptureCollections,我便可以包装使用它们的方法。Regex::Matches 返回
MatchCollection,所以包装器如下:
CMMatchCollection CMRegex::Matches(LPCTSTR input)
{
return (*this)->Matches(input);
}
CMMatch::Groups 和 CMGroup::Captures 完全相同,再次重申,编译器默默地完成所有类型转换。我爱C++ 和 interop!
处理委托
在编程历史上最重要的革新之一是回调概念。这种调用机制使你调用的某个函数直到运行时才知道。回调为虚拟函数以及所有形式的事件编程提供了基础。但在托管世界,人们不说“回调”,而是说“委托”。例如,Regex::Replace
的形式之一允许传递 MatchEvaluator:
MatchEvaluator* delg =
// create one
String *s = Regex::Replace("\\b\\w+\\b",
"Modify me.", delg);
Regex::Replace 针对每个成功的 Match 调用你的 MatchEvaluator 委托。你的委托返回替代文本。稍后,我会展示一个使用
MatchEvaluator 小例子。现在,我们集中精力来对它进行包装。框架中是委托,而C++中称回调。为了使其交流,我先得需要一个 typedef:
class CMMatch ... {
public:
typedef CString (CALLBACK* evaluator)(const
CMMatch&, void* param);
};
CMMatch::evaluator 是一指向函数的指针,它有两个参数:CMMatch 和 void* param,并返回 CString。将
typedef 放在 CMMatch 完全是风格使然,没有其它意图,但这样做确实避免了全局名字空间的混乱。void* param
为本机调用者提供了一种传递其状态的途径。委托总是要与某个对象关联(如果该方法为静态,则对象可为空),但在 C/C++
中则始终都是一个函数指针,所以回调接口通常都加一个 void* 以便能传递状态信息。完全是低级C的风格。有了新的 typedef
以及将这些评论了然于心,我可以象这样声明 CMRegex::Replace:
class CMRegex ... {
public:
static CString Replace(LPCTSTR input,
LPCTSTR pattern,
CMMatch::evaluator me,
void* param);
};
我的包装器类似实际的 Replace 方法(都是静态的),带额外参数 void* param。那么我如何实现它呢?
CString CMRegex::Replace(...)
{
MatchEvaluator delg =
//
how to create?
return Regex::Replace(..., delg);
}
为了创建 MatchEvaluator 我需要一个 __gc 类,这个类要具备一个方法,该方法调用调用者的本机回调函数,而回调函数带有调用者的 void*
参数。我写了一个小托管类:WrapMatchEvaluator,专做此事(详情请参考代码)。为了节省键盘输入,WrapMatchEvaluator 有一静态
Create 函数,返回一新的 MatchEvaluator,所以 CMRegex::Replace 仍然只有一行:
CString CMRegex::Replace(LPCTSTR input,
LPCTSTR pattern,
CMMatch::evaluator me,
void* lp)
{
return Regex::Replace(input, pattern,
WrapMatchEvaluator::Create(me, lp));
}
好了,源文件中只有一行,这里是为了便于美观和印刷的原因而将其分行了。既然本机代码用不着 WrapMatchEvaluator(它是一个 __gc 类),在
RegexWrap.cpp 内实现,而非头文件。
处理异常
.NET 框架迟早会抱怨你的所为粗鲁,我知道,如果你传给 Regex 一个糟糕的表达式,你有何指望?本机代码无法处理托管异常,所以我还得做一些事情。在
CLR 调试器中 Dump 用户信息当然不会让我觉得光彩,所以我也得包装
Exceptions。我会在边界捕获它们并在它们流窜到本机区域之前让它们裹上其包装。捕获并包装是个单调乏味的活,但又不得不做。Regex
的构造函数可以丢出异常,所以我需要修订我的包装器:
Regex* NewRegex(LPCTSTR s)
{
try {
return new Regex(s);
} catch (ArgumentException*
e) {
throw CMArgumentException(e);
} catch (Exception* e) {
throw CMException(e);
}
}
CMRegex::CMRegex(LPCTSTR s) : CMObject(NewRegex(s))
{
}
基本套路是在包装器内捕获异常,然后用包装好的形式再重新丢出它。之所以引入 NewRegex 是因为这样做我能使用初始化语法,而不用构造函数中对
m_handle 赋值(那样效率不高,因为要赋值 m_handle 两次)。一旦我捕获并包装好
Exceptions,本机代码便能以本机方式处理它们.下面示范了当用户敲入坏表达式时 RegexTest 是如何应对的:
// in FormatResults
try {
// create CMRegex, get matches,
build string
} catch (const CMException& e) {
result.Format(_T("OOPS! %s\n"),
e.ToString());
MessageBeep(0);
return result;
}
在包装异常时有一点要考虑,即是否需要包装每一种异常丢出。对于 Regex 而言,只有 ArgumentException,但 .NET
有一大堆异常类型。包装哪一个以及要添加多少 catch 块依赖于你的应用程序需要多少信息。无论你做什么,都要保证在最后的 catch
块中捕获基本异常类,这样才不至于有疏漏而导致你的应用程序崩溃。
包装数组
包装完集合、委托和异常。现在该轮到数组了。Regex::GetGroupNumbers 返回整型数组,而 Regex::GetGroupNames
返回字符串数组(String)。将它们传递到本机区域之前,我必须将托管数组转换为本地类型。C-风格数组是一种选择,但有 STL 存在,便没有理由使用
C-风格的数组。ManWrap 有一个模板函数,用来将 Foo 托管对象数组转换成 CMFoo 类型的 STL
vector。CMRegex::GetGroupNames 使用它,正像你下面所看到的:
vector<CString> CMRegex::GetGroupNames()
{
return wrap_array<CString,String>((*this)->GetGroupNames());
}
又是只有一行代码。另一个 wrap_array 转换整型数组,因为编译器需要 __gc 说明符来断定本机和托管整型数组之间的差别,具体细节你去琢磨源代码吧。
封装枚举
终于轮到封装枚举了,这是 RegexWrap 一系列要解决的问题中最后一个。其实也不是什么问题,只是解决令人头疼的键盘敲入。某些 Regex 方法允许用
RegexOptions 来进行行为控制。例如,如果你想忽略大小写,可以用 RegexOptions::IgnoreCase 调用 Regex::Match。为了让本机应用存取这些选项,我用相同的名称和值定义了自己的本地枚举,如
Figure 7 所示。为了节省键盘输入和消除错误,我写了一个小实用工具 DumpEnum,它为任何.NET框架枚举类生成 C 代码。
建立混合模式的 DLLs
解决了所有的编程问题,最后一步是将 RegexWrap 打包成一个DLL。此时你的所有类通常都得用__declspec(dllexport) 或 __declspec(dllimport)处理(而我是宏来简化的),同时在生成托管DLL时,你还得玩点技巧。托管DLLs需要专门的初始化,因为它们不能用常规的
DllMain 启动代码,它们需要 /NOENTRY 以及手动初始化。详情参见 2005 年二月的《C++ At Work》专栏。RegexWrap
的底线是使用 RegexWrap.dll,你必须实例化一个专门的 DLL----在全局范围的某个地方初始化类,就像如下的代码行这样:
// 在应用程序的某个地方
CRegexWrapInit libinit;
调试期间我还遇到一个小问题。为了在你的本机应用程序中调试包装器DLLs,你需要在项目的调试(Debug)设置中将“调试器类型(Debugger
Type)”设置为“混合模式(Mixed)”。默认(自动)加载哪个调试器要依赖 EXE。对于 ManWrap 来说,EXE
是本机代码,所以IDE使用本机调试器,那么你就无法跟踪到托管代码。如果你选择“调试类型”为“混合模式”,那么IDE两个调试器都加载。
一旦你摆平了这些小麻烦,RegexWrap 便会像任何其它 C++ DLL 工作。客户端包含头文件并链接导入库。自然,你需要在PATH中加入
RegexWrap.dll 的路径,并且 .NET 框架要运行起来。典型的客户端应用(如 RegexTest)其文件及模块之间的关系如图 Figure 9
所示。
Figure 9 文件和模块的关系
RegexWrap 趣事
随着 Regex 的最后包装,现在该消遣一下了!我写 RegexWrap 的缘由是为了将正则表达式的威力带给本机 MFC 程序。
我做的第一件事情是用 RegexWrap 将我原来所写的混合模式的 RegexTest 程序及其托管函数 FormatResults
移植为纯粹本机版本。每个 Regex、Match、Group 和 Capture 指针现在都成了 CMRegex、CMMatch、CMGroup 或
CMCapture 对象。集合的情况可入法炮制(详情请下载源代码)。重要的是现在 RegexTest 完全本地化了,在其项目文件或make文件里你找不到 /clr。如果你是正则表达式新手,那么
RegexTest 是你开始探究它们的最好途径。
接下来的例子是一个有趣的程序,这个程序将英语变成难以理解的乱语。语言学家长期以来都在观察下面这这种古怪的现象:如果你打乱某个句子中每个单词中间的字母,而保留开始和结尾处的字母,那么结果比你想象的更可读。显然,我们的脑子是通过扫描单词开始和结尾处的字母并填充其余部分来阅读的。我用
RegexWrap 实现了一个 WordMess 程序,它演示了这种现象。敲入一个句子后,WordMess 向所描述的那样打乱它,程序运行如 Figure 10
所示。这里是 WordMess 以本自然段的第一句为例:“my nxet sapmle is a fun prgaorm taht tnurs Ensiglh
itno smei-reabldae gibbiserh.”
Figure 10 WordMess
WordMess 使用 MatchEvaluator 委托形式的 Regex::Replace(当然是通过其包装器):
// in CMainDlg::OnOK
static CMRegex MyRegex(_T("\\b[a-zA-Z]+\\b"));
CString report;
m_sResult = MyRegex.Replace(m_sInput, &Scrambler, &report);
MyRegex 为匹配单词的静态 CMRegex
对象,也就是说,打乱环绕单词的一个或多个字母的顺序。(用C++编写正则表达式最难的部分是每次都要记住两个你想得到的反斜线符号的类型。)所以
CMRegex::Replace 针对输入句子中每个单词调用我的 Scrambler 函数一次。Scrambler 函数如 Figure 11 所示。看看用
STL 字符串和 swap 以及 random_shuffle 算法使问题的解决变得多么容易。如果没有 STL,那么将面临编写大量的代码。Scrambler 将
CString 作为其 void* param 参数,所做的每次替换都添加到这个 CString。WordMess 将报告添加到其结果显示区域,如 Figure
10 所示。多神奇啊!
我的最后一个应用,我选择更认真和实用的东西,这个程序叫做 RegexForm,验证不同类型的输入:邮编、社会保险号、电话号码以及 C
符号(tokens)。有关 RegexForm 的讨论参见本月的 C++ At Work 专栏。
结论
好了,包装就讲到这里!希望你已经和我一起分享了包装 Regex 的乐趣,同时我希望你能找到用 ManWrap
包装其它框架类的方法,从而你能从本机代码中调用。ManWrap 并不适合每一个人:只有当你想保持本机代码而又想调用框架时才需要它,否则用 /clr
并直接调用框架即可。
标签:
本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@evget.com