委托的不同处理方式 - 第 1 部分






4.98/5 (18投票s)
使用委托来分组、处理横切关注点并创建动态验证器
引言
这篇文章是关于如何以一种您可能未曾想过的方式使用委托。当我开始撰写本文时,显而易见这将是一篇很长的文章,所以我将其分为两部分。第一部分概述了委托以及如何使用它们来分组功能和处理横切关注点。 第二部分将深入探讨如何使用委托来创建一个可以动态验证任何类型对象的验证器。
背景
委托被定义为一种类型安全函数,它可以像引用变量(内存空间的指针)一样传递,并可用于将功能注入对象。
如果您需要更多关于委托机制的介绍,您可以在 CodeProject 或 MSDN 上找到许多关于委托和多播委托的文章;请搜索这些内容。
如果您进行过 Winforms 开发,您已经以事件处理程序的形式使用过委托。
假设您有一个名为 btnExample_Click
的按钮。
(代码中的区域 1)
在设计器创建的局部类中,您会发现以下内容,而在 CS 文件中,您会找到引用的代码。
//in Part1.Designer.cs
this.btnExample.Click += new System.EventHandler(this.btnExample_Click);
//in Part1.cs
private void btnExample_Click(object sender, EventArgs e)
{
}
System.EventHandler()
被定义为接受一个预定义的委托,名为 void EventHandler(object sender, EventArgs e)
,其中 object
是生成事件的 object
(在本例中为 button
),而 EventArgs
被定义为一个类,允许您传递特定于生成事件的 object
的数据。
如果您使用过 Linq 扩展,您也使用过委托,其形式是将函数传递给扩展以代表扩展执行某些操作。
(代码中的区域 2)
private void btnExample2_Click(object sender, EventArgs e)
{
List<string> lst = new List<string>();
lst.Add("Steve");
lst.Add("Sam");
lst.Add("Mark");
string result = lst.Where( w => { return w == "Steve"; }).FirstOrDefault();
//result = Steve
result = lst.Where(SamFunction).FirstOrDefault();
//result Sam
result = lst.Where(MarkFunction).FirstOrDefault();
//result Mark
}
您可能见过一个签名看起来像
Lst.Where(Func<TSource, bool> predicate)
Where
扩展要求一个函数,该函数接受列表中的一个项,并根据传入的对象返回 true
或 false
。参数可以通过多种方式填充:
作为 Lambda 表达式
string result = lst.Where( w => { return w == "Steve"; }).FirstOrDefault(); - //result = Steve
作为传入的函数
result = lst.Where(SamFunction).FirstOrDefault(); - //result Sam
其中 Sam
函数定义为
private bool SamFunction(string inval )
{
return inval == "Sam";
}
作为分配给 Func(string,bool)
变量的 Lambda 表达式(或函数)
result = lst.Where(MarkFunction).FirstOrDefault(); - //result Mark
其中 MarkFunction
定义为
Func<string, bool> MarkFunction = (w) => { return w == "Mark"; };
在所有情况下,该函数都根据传入的项目返回 true
或 false
。
展望未来
那么问题来了,我们能否创建自己的函数并在其中使用其他函数?
有两个特殊的关键字:
Action
和 Func
它们是委托,可以无需显式声明自定义委托即可使用。
Action
用于传递 void
函数(VB.NET 中的 sub
例程),而 Func
允许您传递返回值的函数(VB.NET 中的 function
)。
每个委托类型都重载了以接受不同数量(最多 16 个)的输入参数,对于 Func
,TResult
仅返回一个参数。
示例
Action(T) accepts one parameter of generic T type.
Action(T1, T2) accepts two parameters of generic T type.
Func(T, TResult) accepts one parameter of generic T type and returns a generic TResult.
Func(T1, T2, TResult) accepts two parameters of generic T type and returns a generic TResult.
Func
委托中的最后一个参数是返回类型。
(代码中的区域 3)
在以下示例中,我创建了三个函数,它们接受 Action
或 Func
委托作为参数。
private void MyMethod(Action a)
{
//This is what invoke the delegate passed in
a.Invoke();
}
private void MyMethod(Action<string> a, string msg)
{
//This is what invoke the delegate passed in
a.Invoke(msg);
}
private string MyMethodString(Func<string> f)
{
//This is what invoke the delegate passed in
return f.Invoke();
}
//Example use:
private void btnExample3_Click(object sender, EventArgs e)
{
//Action call no parameters
MyMethod(() => { MessageBox.Show("Show Me"); });
//Action call with a string parameter
string s = "SHOW ME";
//Notice assignment to a Action(T) delegate
Action<string> ai = (val) => { MessageBox.Show(val); };
MyMethod(ai,s);
//Func call
string retval = MyMethodString(() => { return "the Money";});
MessageBox.Show(retval);
}
(代码中的区域 4)
那么问题就来了,我们还能用它们做什么?
一个想法是使用它们将横切关注点集中到一个地方
您写过多少次看起来像下面的函数?
private void WriteHeader()
{
lg.Log("Start Header", "Program", Logger.LogLevel.Info);
try
{
var fs = new StreamWriter("test.txt", true);
fs.WriteLine("Header");
fs.Close();
}
catch (Exception ex)
{
lg.Log(ex.Message,"Header",Logger.LogLevel.Error); }
}
private void WriteBody()
{
lg.Log("Start Body", "Program", Logger.LogLevel.Info);
try
{
var fs = new StreamWriter("test.txt", true);
fs.WriteLine("Body");
fs.Close();
}
catch (Exception ex)
{
lg.Log(ex.Message,"Body",Logger.LogLevel.Error);
}
}
private void WriteFooter()
{
lg.Log("Start Footer", "Program", Logger.LogLevel.Info);
try
{
var fs = new StreamWriter("test.txt", true);
fs.WriteLine("Footer");
fs.Close();
}
catch (Exception ex)
{
lg.Log(ex.Message,"Footer",Logger.LogLevel.Error);
}
}
或许还有更多…
一切正常,然后您的老板说:“我刚查看了日志,发现我忘了告诉你,您还应该记录函数退出时的情况。另外,您应该将您的代码包装在 using 语句中。”
现在您必须去 X 个函数中,在底部添加 x.log(“end”)
并将其包装在 using
语句中。
现在老板又说:“我最近参加了同学聚会,遇到了一位老朋友,他告诉我关于处理错误和重新抛出友好消息的所有事情。”
随着时间的推移,您的每个函数都会变得臃肿,维护起来也越来越困难。下一个支持代码的开发人员不知道为了满足下一个请求需要修改哪些方法。问题不断累积。
如果能在一个地方执行所有这些请求,并在准备好时延迟执行,那该多好?这就是我们将要使用委托做到的。
(代码中的区域 5 和示例辅助类)
我创建了一个类,我可以向其中添加委托,并在一个地方执行和调用它们。执行会延迟,直到我调用写入方法。
(示例辅助类区域)
class WriteDocument
{
ILogger lg;
string fname;
//Pass in reference to logger and filename
public WriteDocument(ILogger log,string filename)
{
lg = log;
fname = filename;
}
//Create a list of action definitions which will be invoked
private List<ActionDefinition> la = new List<ActionDefinition>();
//Used to add actions to the list
internal void AddAction(ActionDefinition a)
{
la.Add(a);
}
/// <summary>
/// Write out the data using delegates, in ascending order
/// </summary>
internal void Write()
{
Write(true);
}
/// <summary>
/// Write out the data using delegates
/// </summary>
/// <param name="orderdirectionasc">True = Acending, False Descending</param>
internal void Write(bool orderdirectionasc)
{
lg.Log("WriteDocument Start", "WriteDocument", Logger.LogLevel.Info);
try
{
//notice that now I don't need to keep appending file, doing all my work at one time,
//so we can overwrite each time.
using (var fs = new StreamWriter(fname, false))
{
//Notice we can change order of processing by specifying.
List<ActionDefinition> lordered;
if (orderdirectionasc)
lordered = la.OrderBy( o => o.dOrderRun).ToList();
else
lordered = la.OrderByDescending( o => o.dOrderRun).ToList();
foreach (var action in lordered)
{
//Notice we invoke the dAction set in the class.
action.dAction.Invoke(fs);
//We can log the name of the method (sure I could have used reflection as well)
lg.Log(action.dDescription, "WriteDocument", Logger.LogLevel.Info);
}
}
}
catch (Exception ex)
{
//We can log and change the Exception thrown
lg.Log(ex.Message, "WriteDocument", Logger.LogLevel.Error);
throw new Exception("An Error occurred please check your logs");
}
finally
{
lg.Log("WriteDocument End", "WriteDocument", Logger.LogLevel.Info);
}
}
请注意,它看起来很像每个函数中的内容。此外,老板还问我们是否可以正向和反向打印。这之前我们很难做到。:)
它使用一个支持类来保存有关被添加和调用的函数的信息。
/// <summary>
/// This class allows us to group more information about the delegate together.
/// You could add more properties like Classification and filter in the WriteDocument routine.
/// </summary>
class ActionDefinition
{
/// <summary>
/// The function to pass which accepts a StreamWriter as an argument
/// </summary>
public Action<StreamWriter> dAction { get; set; }
/// <summary>
///The description, I use for logging the function invoked
/// </summary>
public string dDescription { get; set; }
/// <summary>
/// The order number, can be used to re-order the execution.
/// </summary>
public int dOrderRun { get; set; }
}
(代码中的区域 5)
老板提醒!“我想添加一个打印部分,但前提是测试结果为阳性。”
它是这样设置的:
private void WriteHeader(StreamWriter fs)
{
fs.WriteLine("Header");
}
private void WriteBody(StreamWriter fs)
{
fs.WriteLine("Body");
}
private void WriteFooter(StreamWriter fs)
{
fs.WriteLine("Footer");
}
private void WriteSugarHigh(StreamWriter fs)
{
fs.WriteLine("No more sugar for you!");
}
private void btnExample5_Click(object sender, EventArgs e)
{
bool sugarhigh = true; //normally this would not be hard coded. Just for example
WriteDocument wd = new WriteDocument(lg,"textasc.txt");
wd.AddAction(new ActionDefinition() {dAction = WriteHeader,
dDescription = "Header", dOrderRun = 1} );
wd.AddAction(new ActionDefinition() {dAction = WriteBody,
dDescription = "Body", dOrderRun = 2 });
wd.AddAction(new ActionDefinition() {dAction = WriteFooter,
dDescription = "Footer", dOrderRun = 3} );
wd.Write(); //Write in ascending order
WriteDocument wddesc = new WriteDocument(lg, "textdesc.txt");
wddesc.AddAction(new ActionDefinition() { dAction = WriteHeader,
dDescription = "Header", dOrderRun = 1 });
wddesc.AddAction(new ActionDefinition() { dAction = WriteBody,
dDescription = "Body", dOrderRun = 2 });
//Notice I added a test to see if we needed to add a new section
if (sugarhigh)
wddesc.AddAction(new ActionDefinition() { dAction = WriteSugarHigh,
dDescription = "HighSugar", dOrderRun = 2 });
wddesc.AddAction(new ActionDefinition() { dAction = WriteFooter,
dDescription = "Footer", dOrderRun = 3 });
wddesc.Write(false); //Write in descending order
}
创建了三个文件:
日志文件、Testasc.txt 和 testdesc.txt。
虽然有一些设置工作要做,但我们之前有很多功能是无法实现的。
这个例子在动态创建文档的某些部分时会非常有用,这些部分取决于其他类型的数据,例如,当血液检查显示您的糖分偏高时,会在文档中插入一个部分。此外,您还可以反向打印!
//Notice I added a test to see if we needed to add a new section
if (sugarhigh)
wddesc.AddAction(new ActionDefinition() { dAction = WriteSugarHigh,
dDescription = "HighSugar", dOrderRun = 2 });
在这个例子中,降序文件会多一行关于高血糖的内容。
关注点
除了这里说明的例子,我还使用这种技术来执行需要包含在 SQL 事务中的代码块,在所有块都成功时提交,或者在任何一个失败时回滚。
在调试时,将 Lambda 表达式分成多行有助于更容易地设置断点。另一个技巧是将其写成命名函数,调试您的逻辑,然后再转换为匿名函数。