问题:
你希望把少量状态与某种行为关联起来,但不会陷入构建新类的麻烦之中。
解决方案:
使用lambda 表达式实现闭包。闭包是声明时捕获作用域中环境状态的函数。简单地讲,它们是当前状态以及某种可以读取和修改该状态的行为。lambda 表达式能够捕获外部变量并延长它们的生存期,这使得闭包可以在C# 中使用。
关于lambda 表达式的更多信息,请参考4.0 节。
作为闭包的一个示例,我们将构建一个快速报告系统,用于跟踪销售人员及其收益和佣金。闭包的行为是,你可以构建一些代码,用于计算每个季度的佣金并且用于每个销售人员。
首先,必须定义销售人员,代码如下所示。
class SalesPerson
{
// CTOR的
public SalesPerson()
{
}
public SalesPerson(string name,
decimal annualQuota,
decimal commissionRate)
{
this.Name = name;
this.AnnualQuota = annualQuota;
this.CommissionRate = commissionRate;
}
// 私有成员
decimal _commission;
// 属性
public string Name { get; set; }
public decimal AnnualQuota { get; set; }
public decimal CommissionRate { get; set; }
public decimal Commission
{
get { return _commission; }
set
{
_commission = value;
this.TotalCommission += _commission;
}
}
public decimal TotalCommission { get; private set; }
}
销售人员有姓名、年度定额、销售佣金率,以及用于存储每季度佣金和总佣金的属性。既然有一些事情要做,就让我们编写一些代码来计算佣金。
delegate void CalculateEarnings(SalesPerson sp);
static CalculateEarnings GetEarningsCalculator(decimal quarterlySales,
decimal bonusRate)
{
return salesPerson =>
{
// 计算salesPerson的季度指标
decimal quarterlyQuota = (salesPerson.AnnualQuota / 4);
// 他达成季度指标了吗
if (quarterlySales < quarterlyQuota)
{
// 未达成指标,没有佣金
salesPerson.Commission = 0;
}
// 检查奖金级别的绩效(指标的200%)
else if (quarterlySales > (quarterlyQuota * 2.0m))
{
decimal baseCommission = quarterlyQuota *
salesPerson.CommissionRate;
salesPerson.Commission = (baseCommission +
((quarterlySales - quarterlyQuota) *
(salesPerson.CommissionRate * (1 + bonusRate))));
}
else // 常规的佣金
{
salesPersonsalesPerson.Commission =
salesPerson.CommissionRate * quarterlySales;
}
};
}
将委托类型声明为CalculationEarnings,它接受一个SalesPerson 类型参数。有一个名为GetEarningsCalculator 的工厂方法,用于构造此委托类型的一个实例。它将创建一个lambda 表达式来计算SalesPerson 的佣金,并返回一个CalculateEarnings 的实例。在开始前,必须创建salespeople 数组,代码如下所示。
// 设定salespeople……
SalesPerson[] salesPeople = {
new SalesPerson { Name="Chas", AnnualQuota=100000m, CommissionRate=0.10m },
new SalesPerson { Name="Ray", AnnualQuota=200000m, CommissionRate=0.025m },
new SalesPerson { Name="Biff", AnnualQuota=50000m, CommissionRate=0.001m }};
然后基于每季度的收入建立收入计算器,代码如下所示。
public class QuarterlyEarning
{
public string Name { get; set; }
public decimal Earnings { get; set; }
public decimal Rate { get; set; }
}
QuarterlyEarning[] quarterlyEarnings =
{
new QuarterlyEarning(){ Name="Q1", Earnings = 65000m, Rate = 0.1m },
new QuarterlyEarning(){ Name="Q2", Earnings = 20000m, Rate = 0.1m },
new QuarterlyEarning(){ Name="Q3", Earnings = 37000m, Rate = 0.1m },
new QuarterlyEarning(){ Name="Q4", Earnings = 110000m, Rate = 0.15m}
};
var calculators = from e in quarterlyEarnings
select new
{
Calculator =
GetEarningsCalculator(e.Earnings, e.Rate),
QuarterlyEarning = e
};
最后,统计每个季度的所有salespeople 数值,然后通过该数据调用WriteCommission-Report 生成年度报告。这将告诉主管人员哪些销售人员值得留下。
decimal annualEarnings = 0;
foreach (var c in calculators)
{
WriteQuarterlyReport(c.QuarterlyEarning.Name,
c.QuarterlyEarning.Earnings, c.Calculator, salesPeople);
annualEarnings += c.QuarterlyEarning.Earnings;
}
// 看一下谁值得留下
WriteCommissionReport(annualEarnings, salesPeople);
WriteQuarterlyReport 为每个SalesPerson 调用CalculateEarnings 的lambda 表达式实现(eCalc),并且基于每个销售人员的佣金率来修改状态以对每季度的佣金进行赋值。
static void WriteQuarterlyReport(string quarter,
decimal quarterlySales,
CalculateEarnings eCalc,
SalesPerson[] salesPeople)
{
Console.WriteLine($"{quarter} Sales Earnings on Quarterly Sales of
{ quarterlySales.ToString("C")}:");
foreach (SalesPerson salesPerson in salesPeople)
{
// 计算佣金
eCalc(salesPerson);
// 报告
Console.WriteLine($"\tSales person {salesPerson.Name} " +
"made a commission of : " +
$"{salesPerson.Commission.ToString("C")}");
}
}
WriteCommissionReport 对比检查各个销售人员的佣金及其所实现的收益。如果佣金超过了其产生收益的20%,就要采取建议的动作。
static void WriteCommissionReport(decimal annualEarnings,
SalesPerson[] salesPeople)
{
decimal revenueProduced = ((annualEarnings) / salesPeople.Length);
Console.WriteLine("");
Console.WriteLine($"Annual Earnings were {annualEarnings.ToString("C")}");
Console.WriteLine("");
var whoToCan = from salesPerson in salesPeople
select new
{
// 如果佣金超过了他产生收入的20%,解雇他
CanThem = (revenueProduced * 0.2m) <
salesPerson.TotalCommission,
salesPerson.Name,
salesPerson.TotalCommission
};
foreach (var salesPersonInfo in whoToCan)
{
Console.WriteLine($"\t\tPaid {salesPersonInfo.Name} " +
$"{salesPersonInfo.TotalCommission.ToString("C")} to produce" +
$"{revenueProduced.ToString("C")}");
if (salesPersonInfo.CanThem)
{
Console.WriteLine($"\t\t\tFIRE {salesPersonInfo.Name}!");
}
}
}
下面列出了收益和佣金跟踪程序的输出。
Q1 Sales Earnings on Quarterly Sales of $65,000.00:
SalesPerson Chas made a commission of : $6,900.00
SalesPerson Ray made a commission of : $1,625.00
SalesPerson Biff made a commission of : $70.25
Q2 Sales Earnings on Quarterly Sales of $20,000.00:
SalesPerson Chas made a commission of : $0.00
SalesPerson Ray made a commission of : $0.00
SalesPerson Biff made a commission of : $20.00
Q3 Sales Earnings on Quarterly Sales of $37,000.00:
SalesPerson Chas made a commission of : $3,700.00
SalesPerson Ray made a commission of : $0.00
SalesPerson Biff made a commission of : $39.45
Q4 Sales Earnings on Quarterly Sales of $110,000.00:
SalesPerson Chas made a commission of : $12,275.00
SalesPerson Ray made a commission of : $2,975.00
SalesPerson Biff made a commission of : $124.63
Annual Earnings were $232,000.00
Paid Chas $22,875.00 to produce $77,333.33
FIRE Chas!
Paid Ray $4,600.00 to produce $77,333.33
Paid Biff $254.33 to produce $77,333.33
讨论:
对C# 中闭包的最佳描述之一是把对象视为与数据关联的一组方法,并把闭包视为与一个函数关联的一组数据。如果需要对相同的数据执行多种不同的操作,使用对象可能更有意义一些。它们是处理相同问题的两种不同角度,要解决的问题类型有助于决定用哪个方法更合适。它只依赖你倾向于选择哪种方法。有时候,***纯面向对象编程可能是冗长乏味而且不必要的,可以使用闭包很好地解决其中一些问题。这里展示的SalesPerson 佣金示例演示了可以利用闭包做什么。不使用它们也可以完成任务,但是其代价是要编写更多的类和方法代码。
之前对闭包已经进行了定义,但是有一种更严格的定义:它实质上意味着与状态关联的行为不应该能够修改状态,以便使之成为真正的闭包。我们倾向于赞同***种定义,因为它表达了闭包应该是什么,而不是闭包应该如何实现,后者的限制性过强。无论你选择将其视为lambda 表达式某个方面的优雅特性,还是觉得值得将它称作闭包,它都是工具箱中的又一种编程技巧,不应该被摒弃。
参考:
范例1.17(即1.17节)和MSDN文档中的“lambda表达式”主题。