动态方法分派器






4.98/5 (69投票s)
不再有冗长的 switch 语句!
目录
- 1. 动机
- 2. 动态方法调度器:先看完整实现
- 3. DynamicMethodDispatcher 类使用方法
- 4. MultipleInvocationDynamicMethodDispatcher
- 5. 如果字典中找不到键怎么办?
- 6. 补充 OOP 还是滥用它?
- 7. 构建代码和兼容性
- 8. 结论
- 9. 致谢
1. 动机
冗长的 switch 语句是件丑陋的事情。不仅树状的缩进级别和无意义的 break 语句看起来丑陋且不利于可读性,而且使用它们还会导致相当大的性能损失,因为 case 条件必须从上到下逐一检查。将 "switch
" 更改为 "if
" 语句也无济于事。当需要一个冗长的 if-then-else
块时,问题是相同的。不幸的是,在许多情况下,冗长的 switch 语句似乎不可避免。一个典型的例子是从头开始编写 Windows 消息函数。case 的数量可能达到数百个;其中许多需要进行其各自的特定处理。
同时,问题可以很容易解决。这是个想法。对 case 列表进行线性暴力运行可以被一些先进而有效的搜索方法取代。这种方法以 Dictionary
的形式随时可用。switch 的值应该用(唯一的)字典键替换,而代码的分支则用委托实例替换。唯一的问题是以一种通用的方式使用这种机制。
这种方法同时解决了可读性和性能问题。该方法将把委托调用列表的填充代码与调用代码分开。字典中委托实例及其调用列表的填充应该只进行一次(理想情况下——每个应用程序域生命周期只进行一次),但调用可以多次进行,仅用一行代码表示,并且在运行时会非常有效,特别是对于一组冗长的 case,每个 case 都由一个唯一的键表示。让我们看看它是什么样子。
在这项工作中,我提供了两个泛型类,它们以两种不同形式实现该想法:单播和多播。
在我解释它是如何工作和如何使用之前,让我们先看看完整的实现。
2. 动态方法调度器:先看完整实现
让我们看看用于根据键调度委托的 DynamicMethodDispatcher
类。这是一个完全实现的泛型类。
using System.Collections.Generic;
public class DynamicMethodNotFoundException<KEY, MESSAGE> : System.Exception {
internal DynamicMethodNotFoundException(KEY key, MESSAGE message)
{ this.FKey = key; this.FMessage = message; }
public KEY KeyValue { get { return this.FKey; } }
public MESSAGE MessageValue { get { return this.FMessage; } }
KEY FKey; MESSAGE FMessage;
} //class DynamicMethodNotFoundException
public abstract class DynamicMethodDispatcherBase<KEY, MESSAGE, RETURN> {
public delegate RETURN DynamicMethod(KEY key, MESSAGE message);
public int this[KEY key] {
get {
DynamicMethod method;
if (Dictionary.TryGetValue(key, out method))
return method.GetInvocationList().Length;
return 0;
} //get this
} //this
KEY[] Keys {
get {
KEY[] result = new KEY[Dictionary.Keys.Count];
int current = 0;
foreach (KEY key in Dictionary.Keys) {
result[current] = key;
++current;
} //loop
return result;
} //get Keys
} //Keys
#region implementation
internal protected RETURN SingleCastInvoke(KEY key, MESSAGE message) {
DynamicMethod method;
if (Dictionary.TryGetValue(key, out method))
return method.Invoke(key, message);
throw new DynamicMethodNotFoundException<KEY, MESSAGE>(key, message);
} //SingleCastInvoke
internal protected bool SingleCastTryInvoke(KEY key,
MESSAGE message, out RETURN returnValue) {
DynamicMethod method;
bool success = Dictionary.TryGetValue(key, out method);
if (success)
returnValue = method.Invoke(key, message);
else
returnValue = default(RETURN);
return success;
} //SingleCastTryInvoke
internal protected Dictionary<KEY, DynamicMethod> Dictionary =
new Dictionary<KEY, DynamicMethod>();
#endregion implementation
} //DynamicMethodDispatcherBase
public class DynamicMethodDispatcher<KEY, MESSAGE, RETURN> :
DynamicMethodDispatcherBase<KEY, MESSAGE, RETURN> {
public bool Add(KEY key, DynamicMethod method) {
if (Dictionary.ContainsKey(key)) return false;
Dictionary.Add(key, method);
return true;
} //AddReplace
public RETURN Invoke(KEY key, MESSAGE message)
{ return SingleCastInvoke(key, message); }
public bool TryInvoke(KEY key, MESSAGE message, out RETURN returnValue)
{ return SingleCastTryInvoke(key, message, out returnValue); }
} //class DynamicMethodDispatcher
MultipleInvocationDynamicMethodDispatcher
类是相同事物的多播变体。与它的单播对应物不同,它支持绑定到相同键的委托队列,并返回一个结果数组,数组中的每个元素对应一个委托实例。
public class MuticastDynamicMethodDispatcher<KEY, MESSAGE, RETURN> :
DynamicMethodDispatcherBase<KEY, MESSAGE, RETURN> {
public delegate RETURN AccumulationMethod(
int index,
KEY key, MESSAGE message,
bool lastResultAvailable, RETURN lastResult, RETURN currentResult);
public void Add(KEY key, DynamicMethod method) {
DynamicMethod delegateInstance;
if (Dictionary.TryGetValue(key, out delegateInstance)) {
//adding method to invokation list will
//change delegate's identity (wow!):
Dictionary.Remove(key);
delegateInstance += method;
} else
delegateInstance = method;
Dictionary.Add(key, delegateInstance);
} //AddReplace
public RETURN[] Invoke(KEY key, MESSAGE message) {
DynamicMethod delegateInstance;
if (Dictionary.TryGetValue(key, out delegateInstance)) {
System.Delegate[] invocationList = delegateInstance.GetInvocationList();
int index = 0;
RETURN[] returnValue = new RETURN[invocationList.Length];
foreach (System.Delegate method in invocationList) {
returnValue[index] = ((DynamicMethod)method)(key, message);
index++;
} //loop invocationList
return returnValue;
} else
return null;
} //Invoke
public RETURN Invoke(KEY key, MESSAGE message, AccumulationMethod accumulation) {
bool dummyFound;
return AccumulatedInvoke(key, message, accumulation, true, out dummyFound);
} //Invoke
public bool TryInvoke(KEY key, MESSAGE message,
AccumulationMethod accumulation, out RETURN returnValue) {
bool found;
returnValue = AccumulatedInvoke(key, message, accumulation, false, out found);
return found;
} //Invoke
void InvokeNoReturn(KEY key, MESSAGE message) {
SingleCastInvoke(key, message);
} //InvokeNoReturn
public bool TryInvokeNoReturn(KEY key, MESSAGE message) {
RETURN dummyReturnValue;
return SingleCastTryInvoke(key, message, out dummyReturnValue);
} //TryInvokeNoReturn
#region implementation
public RETURN AccumulatedInvoke(KEY key, MESSAGE message,
AccumulationMethod accumulation, bool throwException, out bool found) {
if (accumulation == null) { //fallback:
if (throwException) {
found = true;
InvokeNoReturn(key, message);
} else
found = TryInvokeNoReturn(key, message);
return default(RETURN);
} //if no accumulation method
DynamicMethod delegateInstance;
found = Dictionary.TryGetValue(key, out delegateInstance);
if (found) {
System.Delegate[] invocationList = delegateInstance.GetInvocationList();
int index = 0;
RETURN lastResult = default(RETURN);
foreach (System.Delegate method in invocationList) {
bool lastResultAvailable = index > 0;
RETURN currentResult = ((DynamicMethod)method)(key, message);
lastResult = accumulation(index, key, message,
lastResultAvailable, lastResult, currentResult);
index++;
} //loop invocationList
return lastResult;
} else
if (throwException)
throw new DynamicMethodNotFoundException<KEY, MESSAGE>(key, message);
else
return default(RETURN);
} //AccumulatedInvoke
#endregion implementation
} //class MuticastDynamicMethodDispatcher
首先,我想讨论我设计的被调度的泛型方法:DynamicMethod
。为什么正好是两个输入参数和一个返回值?为什么不只有一个参数?为什么不是更多?嗯,这是因为它实现了最通用的方法。键参数应该与任何其他输入数据隔离,因为它是独特的特殊参数:这是一个用于在字典中存储委托实例的唯一键。仅仅这个参数是不够的,因为应用程序可能需要一些不一定与 key
相关的数据输入。这个被称为 message
的“自由”参数可以携带任意数量的数据,因为它的类型可以是其他数据元素的数组或任何其他数据集合。这种技术的一个有趣应用是当类型 MESSAGE
是 System.Type
时,参见6。
3. DynamicMethodDispatcher 类使用方法
现在我想从使用开始解释两个调度器类中较简单的一个是如何工作的。请参阅演示项目 TestMessageDispatching.exe 的源代码。这是一个使用 System.Windows.Forms
的 Windows .NET 应用程序,演示了原始的 Windows 消息处理。
为了演示动态调度器如何取代冗长的(在这种情况下确实很长)switch
语句,许多不同的 Windows 消息以不同的方式处理:一些以不同的消息特定形式将消息数据发送到 UI,以便记录在不同的列表视图中(取决于消息 ID)并附带不同的注释,一些发出声音,一些两者兼而有之。对于某些状态消息,事件被计数并显示在列表视图中:计算应用程序激活和停用的次数(例如通过 Alt-TAB),以及从消息处理窗体中失去键盘焦点所获得的事件;事件计数器是此窗体的字段。
首先,这是调度器的定义和初始化方式
using MessageDispatcher =
SA.Universal.Technology.DynamicMethodDispatcher<WindowsMessage, MessageInfo, bool>;
public partial class FormMessageHandler {
//...
static int activateCount = 0;
static int focusCount = 0;
static int nonClientActivateCount = 0;
MessageDispatcher MessageDispatcher = new MessageDispatcher();
} // class FormMessageHandler
当 `using` 子句实例化泛型类型参数时,它定义了动态方法的签名。`WindowsMessage` 类型是包含 Windows 消息 ID 的枚举类型,而 `MessageInfo` 是一个类,它以 .NET 方式表示 Windows 消息的结构,通过使用 `System.Runtime.InteropServices.StructLayoutAttribute` 和 `System.Runtime.InteropServices.FieldOffsetAttribute` 属性实现不同语义参数的联合。下载并查看完整源代码以获取更多信息。
消息的捕获和调度器的调用方式如下
public partial class FormMessageHandler {
//...
protected override void DefWndProc(ref Message m) {
bool ignore;
if (MessageDispatcher.TryInvoke((WindowsMessage)m.Msg,
MessageInfo.FromMessage(m), out ignore)) {
if (!ignore)
base.DefWndProc(ref m);
} else
base.DefWndProc(ref m);
} //DefWndProc
} //class FormMessageHandler
布尔类型的返回值用于指示是否应调用默认的 Windows 过程(从基类继承),或者忽略它,从而有效地取消消息的处理。DynamicMethodDispatcher
类有两种调用方法形式,取决于当字典中找不到键(本例中为 WindowsMessage
)时的处理方式。Invoke
方法如果字典中找到动态方法,则返回其结果,否则抛出泛型类型 DynamicMethodNotFoundException
的异常。
消息处理在一行中被调用,并完全取代了冗长的 switch
语句。现在,冗长的序列仍然需要存在于其他地方。是的,动态方法应该被创建并添加到字典中。最重要的是什么?它在应用程序的生命周期中只完成一次。调用会完成多次,并且比 switch
语句更有效。如果 case 的数量很大,它可能会更有效:switch
语句线性工作,而字典搜索时间是 O(0) 级别(参见 "大 O 符号"),也就是说,计算时间不取决于字典的大小(渐近地,对于大量元素)。
这是这段长代码的简短片段
MessageDispatcher.Add(WindowsMessage.NCACTIVATE, (key, msg) => {
ShowStatusMessage(Time.Now, msg,
GetActivationMessage(msg, ref nonClientActivateCount),
"Non-client activation");
return false;
});
MessageDispatcher.Add(WindowsMessage.ACTIVATEAPP, (key, msg) => {
ShowStatusMessage(Time.Now, msg, GetActivationMessage(msg, ref activateCount),
"Application activation");
return false;
});
MessageDispatcher.Add(WindowsMessage.SETFOCUS, (key, msg) => {
focusCount++;
ShowStatusMessage(Time.Now, msg, string.Empty,
string.Format("Focused: {0}", focusCount));
return false;
});
MessageDispatcher.Add(WindowsMessage.KILLFOCUS, (key, msg) => {
ShowStatusMessage(Time.Now, msg, string.Empty, "Focus lost");
return false;
});
MessageDispatcher.Add(WindowsMessage.KEYUP, (key, msg) => {
ShowKeyboardMessage(Time.Now, msg, "Key up");
Console.Beep(360, 30);
return false;
});
MessageDispatcher.Add(WindowsMessage.KEYDOWN, (key, msg) => {
Console.Beep(400, 30);
return false;
});
MessageDispatcher.Add(WindowsMessage.CHAR, (key, msg) => {
ShowKeyboardMessage(Time.Now, msg, "Character");
return false;
});
MessageDispatcher.Add(WindowsMessage.SYSKEYUP, (key, msg) => {
ShowKeyboardMessage(Time.Now, msg, "System key up");
Console.Beep(200, 20);
return false;
});
重要的是这里使用了匿名方法。此外,还使用了它的 Lambda 形式。我对此有两点说明。首先,如果你需要在将匿名委托实例传递给 MessageDispatcher.Add
之前创建它,你将需要参数的类型。
SA.Universal.Technology.DynamicMethodDispatcher<WindowsMessage,
MessageInfo, bool>.DynamicMethod method =
(key, msg) => {
ShowStatusMessage(Time.Now, msg,
GetActivationMessage(msg, ref nonClientActivateCount),
"Non-client activation");
return false;
};
委托的 lambda 形式在此处使用类型推断,但目标委托的类型仍需定义。在 MessageDispatcher.Add
的情况下,这并非必需,因为委托类型是从此方法的参数类型推断出来的,因此委托方法的参数类型(WindowsMessage
和 MessageInfo
)也一并推断出来。
我提供的源代码中找不到 Lambda 表达式形式,因为我也支持 C# v.2.0 的兼容性(参见 7),该版本无法使用 Lambda 表达式。在这种情况下,添加动态方法看起来稍微冗长一些。
MessageDispatcher.Add(WindowsMessage.NCACTIVATE,
delegate(WindowsMessage key, MessageInfo msg) {
ShowStatusMessage(Time.Now, msg,
GetActivationMessage(msg, ref nonClientActivateCount),
"Non-client activation");
return false;
});
即使相同的委托方法应该用于多条消息,匿名委托仍然可以提供重用
WindowsMessage[] mouseMessages = new WindowsMessage[] {
WindowsMessage.LBUTTONDBLCLK,
//...
//all other mouse-related messages...
WindowsMessage.NCRBUTTONDOWN,
WindowsMessage.NCRBUTTONUP,
};
foreach (WindowsMessage wm in mouseMessages)
MessageDispatcher.Add(wm, , (key, msg) => {
ShowMouseMessage(Time.Now, msg, string.Empty);
return false;
});
4. MultipleInvocationDynamicMethodDispatcher
DynamicMethodDispatcher
的多重调用版本的完整源代码已在上面显示(参见 2)。
DynamicMethodDispatcher
及其多重调用版本之间的区别在于能够将多个处理程序添加到由相同键找到的委托实例。如果通过现有键添加新的处理程序,则操作始终成功。所有处理程序都累积在由键找到的相同委托实例的调用列表中。
多重调用版本的一个特殊关注点是处理由相同调用列表的不同句柄调用的方法结果。它们可能不同,因此我们可能需要做一些事情来避免丢失它们。
在研究它如何工作之前,我们需要一些关于委托本质的高级知识。
4.1 关于委托实例的本质
让我们来看一个委托的实例。当你通过 GetType
获取它的类型并使用反射检查它时,它会显示实例的类型是……一个类——它的 Type
返回的 IsClass
属性值为 true
。它支持很多功能,特别是,它通过 GetInvocationList
方法返回处理程序的调用列表。当我们使用 "=" 或 "+=" 运算符向调用列表添加处理程序时会发生什么?
如果我们将一个委托实例赋值给另一个委托实例,那么它们就变成了同一个对象,我们可以测试它(参见下面的代码)。但是,如果我们使用“=”将一个处理程序“赋值”给一个尚未初始化(`null`)的委托实例会发生什么?毫不奇怪,它被创建并初始化,其调用列表长度变为 1。我们可以使用“+=”向同一个委托实例添加另一个处理程序吗?令人惊讶的是,不行!
让我们使用“==”运算符和 object.ReferenceEquals
检查委托实例的标识和引用标识。
delegate int TestDelegate(string name);
delegate string FormatEqual(bool value);
static void ReportDelegateEquivalence(Delegate left, Delegate right) {
FormatEqual formatEqual =
delegate(bool value) { if (value) return string.Empty; else return "NOT"; };
System.Console.WriteLine(
"Delegates are {0} equal, they are referentually {1} equal",
formatEqual(left == right),
formatEqual(object.ReferenceEquals(left, right)));
} //ReportDelegateEquivalence
static void TestDelegateEquivalence() {
TestDelegate delegate1 = delegate(string name) { return 1; };
TestDelegate delegate2 = delegate(string name) { return 1; };
//output: "Delegates are NOT equal, they are referentually NOT equal":
ReportDelegateEquivalence(delegate1, delegate2);
delegate1 = delegate2;
//output: "Delegates are equal, they are referentually equal":
ReportDelegateEquivalence(delegate1, delegate2);
delegate2 += delegate(string name) { return 2; };
//output: "Delegates are NOT equal, they are referentually NOT equal":
ReportDelegateEquivalence(delegate1, delegate2);
} //TestDelegateEquivalence
我们可以看到,委托实例会因 "+=" 运算符而改变其引用标识!这是因为委托实例是不可变的,很像字符串。你不能向委托实例添加处理程序。相反,会创建一个全新的委托实例,其调用列表是从存储在同一变量中的委托实例复制而来的,并添加(或删除)了处理程序;然后将结果委托实例赋值给同一变量。这种行为的目的是为了线程。当一个线程向委托实例添加处理程序时,另一个线程正在执行同一个委托的调用会发生什么?为了避免潜在的致命冲突,两个过程都必须互锁,直到其中一个过程完成。委托实例不可变提高了并行性。
我非常感谢 Nishant Sivakumar 向我解释了委托实例不可变本质的目的。
现在,如果我们试图“更新”注册表中存储的委托实例,这个特性将破坏与注册表一起工作的明显策略。它实际上不会改变任何东西,因为更改将在另一个新的委托实例上完成。相反,`MultipleInvocationDynamicMethodDispatcher` 的代码从字典中删除一个委托实例,“添加”一个新的处理程序(实际上是创建一个全新的委托实例),然后再次将这个新实例添加到字典中。请参阅 `MultipleInvocationDynamicMethodDispatcher.Add` 的实现。
有趣(也很自然)的是,委托实例的类型是即时创建的,你可以从其全名 `delegate1.GetType().FullName` 中看出。同样有趣的是,委托实例运行时类型的基类是 `System.MulticastDelegate`。一旦使用运算符“=”添加了第一个处理程序并初始化后,委托实例就变成了多播委托。在此之前,委托实例未初始化;其值为 `null`。C# 语法不允许在实例初始化之前应用运算符“+=”;初始化它的唯一方法是运算符“=”。所有处理程序都可以通过实例方法 `GetInvocationList` 访问。很容易检查出调用列表元素的运行时类型也基于 `System.MulticastDelegate` 类型。
这引出了一个有趣的问题。这意味着我们可以使用运算符“+=”将一个多播委托实例添加到另一个多播委托实例的调用列表中。我们会得到一个委托实例的树吗?不会!
这很容易检查。请注意,在方法体 `TestDelegateEquivalence` 结束时,`delegate1` 的调用列表长度为 1,`delegate2` 的调用列表长度为 2。让我们在末尾添加另一行:
delegate1 += delegate2;
当我们在此操作后检查 `delegate1` 的委托列表时,我们会发现它仍然有一个由 3 个长度为 1 的元素组成的扁平调用列表。重新创建新(不可变)委托实例的过程会以扁平形式重新创建调用列表;你无法创建树或其他“病态”结构。例如,不可能创建长度为 0 的调用列表。也不可能拥有调用列表中为 `null` 或长度不为 1 的成员。
看来委托实例上的操作使用的算法比人们根据通常文档想象的要狡猾得多!
4.2 返回值
让我们考虑如何将泛型类型 RETURN
的值作为调用结果返回。最简单的方法是使用 InvokeNoReturn
或 TryInvokeNoReturn
方法丢弃所有处理程序的返回值。这两种方法之间的区别在于,当字典中找不到键时,第二种方法不会抛出异常,参见 5。请注意,此方法使用与 DynamicMethodDispatcher.Invoke
方法完全相同的实现。
相反的方法是 Invoke(KEY key, MESSAGE)
,它将每个处理程序返回的所有值作为 RETURN[]
类型的数组返回。这种方法的便利之处在于不需要特殊的 TryInvoke
方法来不抛出异常。当字典中找不到键时,该方法返回 null
。此方法的实现不能使用预定义的委托调用方法来丢弃每个处理程序返回的所有返回值。相反,为了获取每个处理程序返回的每个值,实现会调用委托实例方法 GetInvocationList
返回的调用列表的每个单独元素。有关更多详细信息,请参阅源代码。
返回单个返回值的最复杂方法是累积调用列表中所有元素的结果。请参见委托类型 AccumulationMethod
和方法 Invoke(KEY, MESSAGE, AccumulationMethod)
。例如,让我们看看如何返回调用列表元素返回的所有 double
类型返回值的乘积:
var dispatcher = new MuticastDynamicMethodDispatcher<int, string, double>();
dispatcher.Add(3, (key, value) => { return key; });
dispatcher.Add(3, (key, value) => { return System.Math.Pow(key, System.Math.PI); });
dispatcher.Add(3, (key, value) => { return double.Parse(value); });
dispatcher.Add(5, (key, value) => { return 1d; });
dispatcher.Add(5, (key, value) => { System.Console.WriteLine(key); return key; });
dispatcher.Invoke(
3, "11.2",
delegate(
int index,
int key, string message,
bool lastResultAvailable, double lastResult, double currentResult) {
if (lastResultAvailable) return currentResult;
else return lastResult * currentResult;
});
同样,对于 C# v.2.0,它会更冗长一些
MuticastDynamicMethodDispatcher<int, string, double> dispatcher =
new MuticastDynamicMethodDispatcher<int, string, double>();
dispatcher.Add(3, delegate (int key, string value) { return key; });
dispatcher.Add(3, delegate(int key, string value)
{ return System.Math.Pow(key, System.Math.PI); });
//...
dispatcher.Invoke(
3, "11.2",
delegate(
int index,
int key, string message,
bool lastResultAvailable, double lastResult, double currentResult) {
if (lastResultAvailable) return currentResult;
else return lastResult * currentResult;
});
5. 如果字典中找不到键怎么办?
让我们根据两种类的所有方法在动态方法字典中是否存在某个键来对其行为进行分类。
首先,DynamicMethodDispatcher.Add(KEY, DynamicMethod)
和 MuticastDynamicMethodDispatcher.Add(KEY, DynamicMethod)
方法的实现是不同的。如果字典中已经存在一个键,第一个方法没有效果并返回 false。第二个方法总是成功的。请参阅两者的实现。
现在,让我们对这两个类的所有调用方法进行分类。
当在动态方法字典中找不到键时,这些方法将抛出 DynamicMethodNotFoundException
异常。
DynamicMethodDispatcher.Invoke(KEY, MESSAGE)
;MuticastDynamicMethodDispatcher.Invoke(KEY, MESSAGE, AccumulationMethod)
;MuticastDynamicMethodDispatcher.InvokeNoReturn(KEY, MESSAGE)
.
当在动态方法字典中找不到键时,这些方法不会抛出异常;请阅读关于调用者如何识别这种情况的注释。
DynamicMethodDispatcher.TryInvoke(KEY, MESSAGE, out RETURN)
;如果找不到键,此方法将返回false
,我们的RETURN
值应被忽略。MuticastDynamicMethodDispatcher.Invoke(KEY, MESSAGE)
;如果找不到键,此方法将返回null
,否则将返回包含调用列表中所有元素的返回值的数组。MuticastDynamicMethodDispatcher.TryInvoke(KEY, MESSAGE, AccumulationMethod, out RETURN)
;如果找不到键,此方法将返回false
,我们的RETURN
值应被忽略。MuticastDynamicMethodDispatcher.TryInvokeNoReturn(KEY, MESSAGE)
;如果找不到键,此方法将返回false
。
如果您查看可下载的源代码,您会发现每个公共方法都有全面的 XML 注释。
6. 补充 OOP 还是滥用它?
动态方法调度器的键可以是类型吗?当然可以,但是……任何对面向对象技术有深入理解的人都会立即识别出一种糟糕的反模式,它会违背 OOP 的目的。在这种反模式中,类型的标记取代了基于共同抽象接口的类层次结构的继承,该接口使用后期绑定实现。在 .NET 的情况下,System.Type
类型的实例可以扮演这种标签的角色。
然而,这个问题并不那么简单。首先,面向对象编程技术并非没有局限性和方法论问题。例如,考虑圆-椭圆问题。这只是面向对象开发可能陷入僵局的一个案例,而这实际上从一开始就可以预见。
我想通过一些真实的例子来阐述导致这种标记方法的技巧。传统 OOP 无法提供自然解决方案的问题之一是并行层次结构。让我们考虑一些实际情况。
6.1. 用例:并行层次结构问题
当我们创建一个用于 CAM 和 SCADA 应用程序的 CAD 模型时,我们试图创建一个综合模型,以便其实例能够涵盖应用程序领域中所有可想象的设计范围。它还应该用作我们可以从许多第三方模型导入的所有设计的内部数据表示。对此类数据模式进行建模并不是一个大问题:所有遗留系统都有其局限性,我们可以克服这些局限性,因为我们的新模型可以设计成所有已知功能的超集。我们只在进行导入插件功能所需的遗留或第三方模型翻译时遇到问题。问题在于遗留图形基元的集合是作为单独的对象层次结构构建的。我们不想对遗留数据模型创建永久依赖。问题的根源在于经典的单父继承具有树的结构。在并行层次结构的情况下,我们需要在两者之间建立映射。从经典 OOP 的角度来看,这可以被视为一些临时映射代码。然而,使用动态方法调度器,它不必是临时代码。
让我们考虑两个并行层次结构:一个代表我们综合的 CAD 模型(部分显示),另一个是 AutoDesk DWG 文件所呈现的图形基元层次结构。
//our CAD model:
public abstract class Shape { /* ... */ }
public class PolyLine2D : Shape { /* ... */ }
public class PolyLine3D : Shape { /* ... */ }
public class Ellipse : Shape { /* ... */ }
public class EllipticArc : Shape { /* ... */ }
public class Text : Shape { /* ... */ }
//...
//hierarchy of the AutoCAD DWG file:
public class AcadPrimitive { /* ... */ }
public class AcadPolyLine2D : AcadPrimitive { /* ... */ }
public class AcadPolyLine3D : AcadPrimitive { /* ... */ }
public class AcadCircle : AcadPrimitive { /* ... */ }
public class AcadArc : AcadPrimitive { /* ... */ }
public class AcadText : AcadPrimitive { /* ... */ }
public class AcadMultilineText : AcadPrimitive { /* ... */ }
//...
现在,映射代码应该实现某个接口,用于将任何第三方模型转换为我们的内部表示。
public interface IModelTranslator { /* ... */ }
public class DwgModelTranslator :
DynamicMethodDispatcher<System.Type>,
IModelTranslator {
public DwgModelTranslator() {
Add(typeof(AcadPolyLine2D), PolyLine2D);
Add(typeof(AcadPolyLine3D), PolyLine3D);
Add(typeof(AcadCircle), PolyLine3D);
Add(typeof(AcadArc), PolyLine3D);
Add(typeof(AcadText), Text);
Add(typeof(AcadMultilineText), Text);
//...
} //DwgModelTranslator
Shape PolyLine2D(System.Type acadType, AcadPrimitive acadPrimitive) {
return new PolyLine2D(/* ... */);
} //PolyLine2D
//demonstrates covariance:
PolyLine3D PolyLine3D(System.Type acadType, AcadPrimitive acadPrimitive) {
return new PolyLine3D(/* ... */);
} //PolyLine3D
Shape Circle(System.Type acadType, AcadPrimitive acadPrimitive) {
return new Ellipse(/* ... */);
} //Circle
EllipticArc Arc(System.Type acadType, AcadPrimitive acadPrimitive) {
return new EllipticArc(/* ... */);
} //Arc
Shape Text(System.Type acadType, AcadPrimitive acadPrimitive) {
return new Text(/* ... */);
} //Text
//...
//to be used to implement IModelTranslator.Translate (not shown)
Shape TranslatePrimitive(AcadPrimitive primitive) {
return this.Invoke(primitive.GetType(), primitive);
} //TranslatePrimitive
//...
} //class DwgModelTranslator
请注意,我们处理了层次结构之间不存在一对一对应关系的情况。此外,相同的方法可以重复使用,将不同的 AutoDesk 基元转换为一个更通用的 CAD 形状。
使用遗留系统和导入并不是并行层次结构会给经典 OOP 设计带来问题的唯一领域。在需要层次结构之间映射时,存在不同类别的问题。当同一应用领域的不同方面需要分离时就会发生这种情况。例如,纯数据模型只处理数据转换和持久化;同一模型的图形表示处理诸如图形表示系统映射(例如:WPF)、缩放、平移、显示独立图层等方面。当上述所有内容都表示 CAD 模型时,它应该与同一模型的 CAM 方面等分开。
基于动态方法调度器的结构提供了并行模型之间的映射,从而补充了基于纯树继承关系的结构。
6.2. 动态方法调度器作为虚方法表
让我们回到使用标记方法的顾虑,一些人认为这是对 OOP 的滥用。
动态方法调度器功能的通用性使得这种基于标记的技术并非完全是临时的。如果你查看动态方法字典在动态方法调度器中是如何工作的,你会注意到这种机制类似于传统 OOP 的虚方法表(VMT)。更确切地说,它可以被认为是多态性理论中广义的动态调度技术的一种变体。与基于虚消息表的经典 OOP 中一样,委托调用是间接进行的。在经典的 VMT 中,调用是使用表中的地址偏移量进行调度的。使用动态方法调度器的字典,委托通过字典键访问,这会消耗更多的 CPU 时间,但这两种机制仍然提供 O(0)
的速度(参见“大 O 符号”)。在这两种机制中,调度在运行时选择方法实现;要选择的方法集包括相同签名的方法,但动态方法调度器还允许此处解释的协变和逆变。
数据结构的使用(VMT 的表,动态方法调度器的字典)非常不同。对于 VMT,每个类都有一个表。在调用时,从相同签名和相同类层次结构的相同方法集中进行选择。所有表的内容在编译时完全定义。对于动态方法调度器,相同签名的方法集是完全任意的。它在运行时显式填充,在运行时可能完全未知。(例如,想象它是使用随机数生成器完成的。)
动态方法调度器机制可以被认为是另一种动态调度机制,它泛化并补充了基于 VMT 的经典 OOP 机制。
这些差异的主要原因是基于 VMT 的经典 OOP 机制总是嵌入在面向对象语言中。这种嵌入式实现确实存在。我想专门用一个单独的部分来解释我设计动态方法调度器的文化和技术根源。事实上,我并非完全从零开始“发明”了它。
6.3. 关于这个想法的历史
一个非常接近动态方法调度器机制的机制在 Borland Delphi 和 VCL 库中创建。目前,它在 Embarcadero Delphi 中使用。请参阅这份手册。
让我们看一个这份文档中的简短例子:
TFoo = class
procedure IAmAStatic;
procedure IAmAVirtual; virtual;
procedure IAmADynamic; dynamic;
procedure IAmAMessage(var M: TMessage); message wm_SomeMessage;
end;
在这个 Delphi Pascal 类中,两个方法使用不同的机制提供了完全相同的功能:IAmAVirtual
使用基于 VMT 的虚拟机制,而 IAmADynamic
使用基于动态方法表(DMT)的替代机制,其中方法使用自动生成的 32 位整数类型唯一标识符作为键进行调度。当派生类中重写动态方法时,新类使用不同的 DMT 实例,其中重写的方法与相同的键关联。根据 Borland 和 Embarcadero 的文档,DMT 内存消耗较少,但调度较慢。我认为这可以基于与动态方法调度器非常相似的操作原理来理解。
第三种方法 IAmAMessage
则大不相同。它也使用 DMT 进行调度,但键值由类的开发人员硬编码。在此示例中,它可以是 Windows 消息 ID 之一。TMessage
类型并非唯一能与此机制配合使用的类型。这种形式的方法仅需要一个以 32 位整数类型键开头的任何结构的引用参数,该参数将作为调度结果传递给被调用的方法。此外,这个类不必调度 Windows 消息。您可以使用预定义的特殊方法 Dispatch
将其用于任何目的。专门将此方法用于 Windows 消息只是某些 UI 相关类(如 Form
)中使用的调度机制的一种应用。
我想指出,Delphi DMT 机制对于通过键进行调度来说不够灵活,因为只有一个预定义的键类型。此外,使用消息指令并不完全安全,因为方法参数的正确性基于用户提供正确参数类型的假设。我提供的 .NET 动态方法调度器的实现是完全安全的,并且更加灵活和健壮。
7. 构建代码和兼容性
代码适用于 Microsoft .NET 2.0 到 4.0 版本。解决方案 DynamicMethodDispatcher.2005.sln、DynamicMethodDispatcher.2008.sln 和 DynamicMethodDispatcher.2010.sln 允许使用相应版本的 Microsoft Visual Studio 构建代码。
代码可以使用批处理文件 build.2005.bat、build.2008.bat 和 build.2010.bat 在批处理模式下构建(使用相应 Visual Studio 解决方案文件格式的 MSBuild.exe 构建)。批处理构建不需要安装 Visual Studio 或任何其他开发环境——它只使用 .NET 可再发行组件包。
构建为两种配置(Debug 和 Release 是独立的输出目录:.\bin.Debug 和 .\bin.Release) 创建了两个可执行文件。
- SA.Universal.Technology.dll:包含类
MuticastDynamicMethod
和MuticastDynamicMethodDispatcher
。 - TestMessageDispatching.exe:3 中描述的演示/测试应用程序。此应用程序只能在 Windows 上运行,因为它演示了原始 Windows 消息的处理。
7.1. 兼容性:Microsoft .NET
库和演示/测试应用程序代码都与 Windows 2000 及更高版本的 Windows(带 .NET Framework v.2.0)以及 .NET Framework v.2.0 及更高版本向后兼容。
7.2. 兼容性:Mono
该库应该与所有平台上的 Mono 兼容(在 Linux 上测试了 Mono v. 1.2.6 和 v. 2.4.4),但我前面解释过,演示/测试应用程序只能在 Windows 上运行。
8. 结论
本文介绍的动态方法调度器概念非常强大,可以在多种情况下显著提高代码性能和可维护性。在 OOP 限制被突破的情况下,它可以用作对传统 OOP 设计的补充。
然而,这种方法绝不应被视为优于传统技术的方法。相反,它应被视为开发者表达能力调色板中的一个有用补充。
9. 致谢
我几年前创建了第一个动态方法调度器变体的代码,并将其初步的单次调用版本用于不同的应用程序。撰写本文的想法源于 CodeProject 问答论坛上的一个问题。我于 2010 年 1 月 18 日回复 Dalek Dave 的问题时提出了这个想法。Dalek 本身就是 CodeProject 的知名专家。后来,其他几位专家鼓励我根据我的想法写一篇文章。第一个给我写这篇文章想法的人是 Marcus Kramer。
在撰写本文期间,我添加了许多重要功能,包括全新的 MuticastDynamicMethodDispatcher
类,并大大加深了对 .NET 委托内部机制的理解。当我发现委托实例的只读性质时,是 Nishant Sivakumar 向我解释了它的重要性(参见 4.1)。非常感谢你,Nish!
我感谢所有对这项工作表示兴趣的人。希望我的工作能不负众望。:-)