多模式 .NET,第 9 部分:函数式编程
.Java
两件事之一发生了一系列文章获取靠近双位数,任何时间: 要么作者是够狂妄,认为他的读者实际上感兴趣的主题很多次在行中,或他是太傻蛋想出一个新的主题。或者,我猜想,有时主题只是值得这么多的覆盖范围。无论是在此情况下,放心我们现在家里一片。
在六月号上的一段 (msdn.microsoft.com/magazine/hh205754),提供基于名称的轴的变异性的想法在显微镜下,使用的命名约定和动态规划 — — 就绑定的名称,在其中。网络通常意味着某些层面的思考 — — 以解决一些有趣的设计问题。大多数。净的开发人员,我想象,希望他们遇到的动态规划的大部分将通过 C# 4 提供的"动态"关键字。旧手 Visual Basic 开发人员都知道,但是,C# 只是由其活力最近,而 Visual Basic 程序员知道它 — — 和相当成功地使用它,在许多情况下 — — 几十年。
但这不是最后的范式 — — 之一更仍有待探讨,并再次,这是一个被隐藏于眼前几年了。而容易当然 (如果稍微冒失) 形容为通用性变异算法轴设计的功能设计,这同时便含糊其辞,掩盖了它的能力。
一句话,功能编程是有关处理函数值,就像任何其他数据值类型,意味着我们可以通过周围的功能,就像我们可以作为数据的值,以及派生出这些值的新值。或者,更准确地说,功能应视为语言中的一流公民: 他们可以创建、 传递给方法和方法返回正如其他值。但是,解释,再次,不正是启发,让我们开始简单的案例研究。
想象设计工作是创建一个小的命令行计算器: 用户类型 (或管道) 中,一个数学表达式和计算器进行分析和打印结果。设计这是非常简单,如图所示,在图 1。
图 1一个简单的计算器
class Program
{
static void Main(string[] args)
{
if (args.Length < 3)
throw new Exception("Must have at least three command-line arguments");
int lhs = Int32.Parse(args[0]);
string op = args[1];
int rhs = Int32.Parse(args[2]);
switch (op)
{
case "+": Console.WriteLine("{0}", lhs + rhs); break;
case "-": Console.WriteLine("{0}", lhs - rhs); break;
case "*": Console.WriteLine("{0}", lhs * rhs); break;
case "/": Console.WriteLine("{0}", lhs / rhs); break;
default:
throw new Exception(String.Format("Unrecognized operator: {0}", op));
}
}
}
如写,它的工作原理 — — 直到计算器收到基数四营办商以外的其他内容。 什么更糟的是,虽然,大量的代码 (相比,程序的整体大小) 是重复的代码,并将继续是重复的代码,因为我们向系统添加新的数学运算 (如模运算符、 %或指数的运算符,^)。
步进了一会儿后,很明显,实际操作 — — 这两个数字正在采取哪些措施 — — 是什么而异,并最好能写这更通用的格式,如中所示图 2。
图 2更通用的计算器
class Program
{
static void Main(string[] args)
{
if (args.Length < 3)
throw new Exception("Must have at least three command-line arguments");
int lhs = Int32.Parse(args[0]);
string op = args[1];
int rhs = Int32.Parse(args[2]);
Console.WriteLine("{0}", Operate(lhs, op, rhs));
}
static int Operate(int lhs, string op, int rhs)
{
// ...
}
}
很明显,我们可能只是重新创建的开关/箱块的操作,但这真的不会有很大益处。 理想情况下,我们想要的字符串操作查找某种 (其中,表面上看,一种形式的动态编程再次,绑定名称"+"添加剂的操作,例如)。
内设计模式世界中,这将是一例战略模式,在具体的子类实现的基类或接口,为安全起见,东西沿线的提供所需的签名和编译时拼写检查:
interface ICalcOp
{
int Execute(int lhs, int rhs);
}
class AddOp : ICalcOp { int Execute(int lhs, int rhs) { return lhs + rhs; } }
其中工程 … …。 它是很详细,需要为每个操作,我们想要创建一个新类。 也不是很面向对象,因为我们真的只需要一个它的实例,过,宿主内的匹配和执行的查找表:
private static Dictionary<string, ICalcOp> Operations;
static int Operate(int lhs, string op, int rhs)
{
ICalcOp oper = Operations[op];
return oper.Execute(lhs, rhs);
}
不知怎么感觉像这可以简化 ; 有些读者可能已经认识到,这是一次之前,已经解决了的问题,并在事件处理程序回调的情况下除外。 这是完全委托构造在 C# 中为创建:
delegate int CalcOp(int lhs, int rhs);
static Dictionary<string, CalcOp> Operations =
new Dictionary<string, CalcOp>();
static int Operate(int lhs, string op, int rhs)
{
CalcOp oper = Operations[op];
return oper(lhs, rhs);
}
而且,当然,操作人员必须用计算器承认,但添加新的变得有点容易操作正确初始化:
static Program()
{
Operations["+"] = delegate(int lhs, int rhs) { return lhs + rhs; }
}
精明 3 C# 程序员都会立刻意识到可以更进一步,缩短使用lambda 表达式,其中介绍了该语言的版本。 Visual Basic 在 Visual Studio 2010,可以做类似:
static Program()
{
Operations["+"] = (int lhs, int rhs) => lhs + rhs;
}
这是代表和一般的大多数 C# 和 Visual Basic 开发思路停止的位置。 但也不和委托是有趣得多,尤其是当我们开始更进一步扩展。 这个想法,周围的传递函数并使用它们,在各方面的深入。
减少、 映射和折叠 — — 哦我 !
通过各地的函数不是我们习惯于在主流的东西。净的发展,所以这可以怎样利用设计的一个更具体的例子是必要。
假设我们有人对象的集合,如中所示的一下图 3。
图 3人对象的集合
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Person> people = new List<Person>()
{
new Person() { FirstName = "Ted", LastName = "Neward", Age = 40 },
new Person() { FirstName = "Charlotte", LastName = "Neward", Age = 39 },
new Person() { FirstName = "Michael", LastName = "Neward", Age = 17 },
new Person() { FirstName = "Matthew", LastName = "Neward", Age = 11 },
new Person() { FirstName = "Neal", LastName = "Ford", Age = 43 },
new Person() { FirstName = "Candy", LastName = "Ford", Age = 39 }
};
}
}
现在,确实是这样管理要庆祝的东西 (或许他们都取得配额)。 他们想要做什么是给这些人,每一杯啤酒,很容易完成使用传统的 foreach 循环中,如中所示的图 4。
图 4传统 foreach 循环
static void Main(string[] args)
{
List<Person> people = new List<Person>()
{
new Person() { FirstName = "Ted", LastName = "Neward", Age = 40 },
new Person() { FirstName = "Charlotte", LastName = "Neward", Age = 39 },
new Person() { FirstName = "Michael", LastName = "Neward", Age = 17 },
new Person() { FirstName = "Matthew", LastName = "Neward", Age = 11 },
new Person() { FirstName = "Neal", LastName = "Ford", Age = 43 },
new Person() { FirstName = "Candy", LastName = "Ford", Age = 39 }
};
foreach (var p in people)
Console.WriteLine("Have a beer, {0}!", p.FirstName);
}
有一些小的 bug (大多是在您的代码啤酒交给我 11 岁的儿子),但此代码的最大问题吗? 这是本质上 un-reusable。 尝试后给大家另一个啤酒要求另一个 foreach 循环,这是违反不重复自己 (干) 原则。 我们当然可以,到一种方法 (经典程序共同响应),收集啤酒发行代码如下所示:
static void GiveBeer(List<Person> people)
{
foreach (var p in people)
if (p.Age >= 21)
Console.WriteLine("Have a beer, {0}!", p.FirstName);
}
(请注意我添加过 21 岁检查 ; 我的妻子,夏洛特,坚持我包括它之前这篇文章可以去出版物。)但如果愿望是找到每一个 16 岁以上的人,而是给他们张免费的 R 级电影吗? 或以找到所有人是 39 岁以上,给他们"圣牛你是旧 !"气球? 或发现大家都 65 岁以上,给他们每个人一个小笔记本,把事情记他们很有可能忘记 (像他们的名字、 年龄、 地址 … …) 吗? 或发现大家都具有姓氏以外"福特",并邀请他们参加万圣节派对吗?
更多我们折腾过,愈看得清楚这些示例的每种情况下提出了两个元素的变化: 筛选人对象,并要与每个这些人对象执行的操作。 鉴于代表 (和行动 <T> 电源 和谓词 <T> 在推出的类型。NET 2.0),如中所示,我们可以在时仍公开必要的变异性,创建通用性图 5。
图 5过滤人对象
static List<T> Filter<T>(List<T> src, Predicate<T> criteria)
{
List<T> results = new List<T>();
foreach (var it in src)
if (criteria(it))
results.Add(it);
return results;
}
static void Execute<T>(List<T> src, Action<T> action)
{
foreach (var it in src)
action(it);
}
static void GiveBeer(List<Person> people)
{
var drinkers = Filter(people, (Person p) => p.Age >= 21);
Execute(drinkers,
(Person p) => Console.WriteLine("Have a beer, {0}!", p.FirstName));
}
一个更为常见操作是,"变换"(或者,若要将这件事更准确,"项目") 到另一种类型,例如,当我们想要从人的对象列表中最后一个名称解压的字符串列表对象 (请参见图 6)。
图 6从列表的字符串列表对象转换
public delegate T2 TransformProc<T1,T2>(T1 obj);
static List<T2> Transform<T1, T2>(List<T1> src,
TransformProc<T1, T2> transformer)
{
List<T2> results = new List<T2>();
foreach (var it in src)
results.Add(transformer(it));
return results;
}
static void Main(string[] args)
{
List<Person> people = // ...
List<string> lastnames = Transform(people, (Person p) => p.LastName);
Execute(lastnames, (s) => Console.WriteLine("Hey, we found a {0}!", s);
}
请注意,多亏了泛型使用筛选器、 执行和变换 (更多公共/变异性 !) 的声明中,我们可以重用执行显示每个找到的最后一个名称。 通知,太,如何使用 lambda 表达式使有趣的暗示,开始来明确 — — 之一,当我们编写另一个常见的功能操作变得更明显减少,其中"折叠"集合下到单个值所指定的方式相结合的所有值。 例如,我们可以在其中添加了每个人的年龄来检索使用 foreach 循环的总和的全年龄值如下所示:
int seed = 0;
foreach (var p in people)
seed = seed + p.Age;
Console.WriteLine("Total sum of everybody's ages is {0}", seed);
中所示,也可以编写它使用泛型的减少,图 7。
图 7使用一个通用的减少
public delegate T2 Reducer<T1,T2>(T2 accumulator, T1 obj);
static T2 Reduce<T1,T2>(T2 seed, List<T1> src, Reducer<T1,T2> reducer)
{
foreach (var it in src)
seed = reducer(seed, it);
return seed;
}
static void Main(string[] args)
{
List<Person> people = // ...
Console.WriteLine("Total sum of everybody's ages is {0}",
Reduce(0, people, (int current, Person p) => current + p.Age));
}
此减少操作通常称为"折叠",顺便。 (明眼人功能程序员,这两个词都略有不同,但差别并不是讨论的主要问题的关键。)是的如果你已经开始怀疑这些行动是真的只不过 LINQ 提供的对象 (爱太少,当它最初发布的 LINQ 对象功能),您会准确 (请参见图 8)。
图 8折叠操作
static void Main(string[] args)
{
List<Person> people = new List<Person>()
{
new Person() { FirstName = "Ted", LastName = "Neward", Age = 40 },
new Person() { FirstName = "Charlotte", LastName = "Neward", Age = 39 },
new Person() { FirstName = "Michael", LastName = "Neward", Age = 17 },
new Person() { FirstName = "Matthew", LastName = "Neward", Age = 11 },
new Person() { FirstName = "Neal", LastName = "Ford", Age = 43 },
new Person() { FirstName = "Candy", LastName = "Ford", Age = 39 }
};
// Filter and hand out beer:
foreach (var p in people.Where((Person p) => p.Age >= 21))
Console.WriteLine("Have a beer, {0}!", p.FirstName);
// Print out each last name:
foreach (var s in people.Select((Person p) => p.LastName))
Console.WriteLine("Hey, we found a {0}!", s);
// Get the sum of ages:
Console.WriteLine("Total sum of everybody's ages is {0}",
people.Aggregate(0, (int current, Person p) => current + p.Age));
}
工作企业。网络开发人员,这看起来愚蠢。 不是像真正程序员花时间寻找重用年龄求和代码的方法。 真正的程序员编写代码,循环访问对象的集合,每个名字串联成字符串,适用于 OData 请求或东西里面的 XML 表示形式:
Console.WriteLine("XML: {0}", people.Aggregate("<people>",
(string current, Person p) =>
current + "<person>" + p.FirstName + "</person>")
+ "</people>");
低下高贵的头颅。 猜 LINQ 对象的东西毕竟可能很有用。
更多的功能吗?
如果你是经典受过训练的面向对象的开发人员,这看起来可笑同时又优雅。 它可以是一个令人兴奋的时刻,因为这种方法确实是在软件设计中近截然相反的对象的方式: 重点放在"东西"在系统中,并使行为附加到每个这些东西的东西,而不是函数编程看上去识别系统中的"动词",看看他们如何可以对不同类型的数据操作。 两种方法是比其他更合适 — — 每个捕获的共同性和提供了非常不同的轴线,沿变异性和可能想象的有的地方每在哪里优雅而简单,和其中每个可以是丑陋又笨拙。
请记住,在经典的对象定位变异是一级结构,提供创建通过添加字段和方法或替换现有的方法 (通过重写),但没有捕获特设算法行为的积极变化的能力。 事实上,直到。净了匿名方法此轴通用性/变异性变得可行。 很可能像下面这样做在 C# 1.0 中,但每个 lambda 不得不声明某个地方的命名的方法,每种方法不得不 System.Object 条款 (这意味着这些方法中的向下转换),在键入,因为 C# 1.0 没有参数化的类型。
功能语言的长期从业人员会感到害怕这一事实我结束这篇文章在这里,因为有许多其他功能的语言可以的做除了只传递函数周围为值 — — 部分应用程序的功能是一个巨大的概念,使很多函数编程非常紧和优雅,直接支持的语言 — — 但必须满足编辑的需要和我这样推我长度限制,因为它是。 即便如此,看到只是这么多的功能的方法 (和武装与功能已经存在于 LINQ) 可以提供一些功能强大的新设计洞察力。
更重要的是,开发人员希望看到更多的这应该看长、 硬 F #,其中的所有。网络语言,是作为一流公民语言中捕获这些功能的概念 (部分应用程序和讨好) 的唯一。 C# 和 Visual Basic 开发人员可以做类似的事,但需要一些图书馆援助 (新的类型和方法做 F # 什么自然)。 幸运的是,几个这种正在努力,包括"功能 C#"库中,可用 CodePlex 上 (functionalcsharp.codeplex.com)。
各种范式
喜欢与否,multiparadigm 语言很常用,和他们看起来像他们会留下来。 每个 Visual Studio 2010 语言表现出某种程度的每个不同的模式 ; C + +,例如,有一些不可能在托管代码中,由于对 c + + 编译器操作的方法的参数化编程设施和最近刚 (在最新的 C + + 0 x 标准) 获得了 lambda 表达式。 甚至饱受诟病 JScript/ECMAScript/JavaScript 语言可以做对象,程序,元编程、 动态和功能范式 ; 事实上,JQuery 的大部分是建基于这些想法。
编码愉快 !
.Java 是首席 Neward & 将关联,独立的公司,专门从事企业。NET 框架和 Java 平台系统。他写了 100 多个文章,是 C# MVP 和 INETA 的扬声器,和有创作或合著了十几本书,其中包括"专业 F # 2.0"(Wrox,2010年)。他咨询、 顾问服务定期 — — 达到他在ted@tedneward.com 如果你有兴趣在他来处理您的团队,或阅读他的博客上有 blogs.tedneward.com。
衷心感谢以下技术专家对本文的审阅: Matthew Podwysocki