禅宗和函数式 C#
现在,去拥抱禅意吧,我的兄弟们。
编写函数式程序可以带来类似禅宗的体验。函数式 C# 使用简洁、简短的代码块,清晰地展现其意图和内部工作原理,从而解开代码中的各种难题,使代码空间更加简洁。
不再有副作用
深吸一口气,屏住呼吸5秒钟,然后呼气。
是时候从功能性角度思考问题了,也是时候告别那些令人讨厌的、毫无益处的副作用了。
- 当你在当前作用域之外修改状态时,就会产生副作用,无意中改变了程序其他地方的状态。这会导致意想不到的行为和因果关系。
- 相反,最好选择没有副作用的函数。这些函数被称为纯函数。(免责声明:编写纯函数很容易上瘾——一旦开始就停不下来。)
禅宗准则建议:
避免:不纯的函数
public void UpdatePrice(decimal vat, decimal cost, decimal markup)
{
this.price = (cost + markup) * (1 + vat);
}
首选:纯函数
public decimal PriceIncludingVat(decimal vat, decimal cost, decimal markup)
{
return (cost + markup) * (1 + vat);
}
price = PriceIncludingVat(0.2m, 10m, 2m);
调用此函数PriceIncludingVat不会修改现有状态;它会返回新状态。您可以根据需要多次调用此函数,price直到您实际设置当前状态之前,都不会受到影响。
练习将冗长的非纯方法拆分成纯函数。将方法的副作用提取出来,并将它们转化为返回新状态的独立函数。你可能会发现自己正在梳理一些非常棘手的代码——这会让你感到释怀,并让你更接近禅定。
不变性
闭上眼睛。吸气。呼气。拥抱永恒的宁静。
你的状态将不再可变。新的不可变对象取代new了旧的不可变对象。通过将对象设为不可变,可以确保任何作用域内的任何事物都无法更改它们。这完全避免了副作用。要更改不可变对象的属性,唯一的方法是用包含更改的新对象替换该对象。
禅宗准则建议:
避免:可变对象
public class ZenGarden
{
public int Tranquillity { get; set; } // mutable
public int Peace { get; set; } // mutable
public ZenGarden(int tranquillity, int peace)
{
Tranquillity = tranquillity;
Peace = peace;
}
}
这导致
var garden = new ZenGarden(10, 20);
garden.Peace = -1000;
// or even worse
public int TranquilityAndPeace(ZenGarden garden)
{
garden.Peace -= 1; // a side effect
return garden.Tranquillity + garden.Peace;
}
更喜欢:
public class ZenGarden
{
public readonly int Tranquillity;
public readonly int Peace;
public ZenGarden(int tranquillity, int peace)
{
Tranquillity = tranquillity;
Peace = peace;
}
}
这导致
var garden = new ZenGarden(10, 20);
garden = new ZenGarden(10, 21); // peace goes up, because of immutability :]
// pure function
public int TranquilityAndPeace(ZenGarden garden)
{
var calculatedGarden = new ZenGarden(garden.Tranquillity, garden.Peace - 1);
return calculatedGarden .Tranquillity + calculatedGarden .Peace;
}
- 注意,
readonly这里使用私有 setter 而不是私有 setter ,以确保即使在自身内部也无法更改这些值class。
无需再担心不必要的更改(副作用)——不可变性确保没有任何事物会改变同一状态,也友好地告别了竞态条件和线程锁。
参考透明度
拥抱清晰的禅意。引用透明的函数可以直接用它被调用时返回的值来替换。无论上下文如何,它对相同的参数都返回相同的值。引用不透明的函数则恰恰相反——通常是因为它们引用了可能会改变的外部值。
引用透明性有助于提高代码的可读性。它能理清那些从作用域外获取值的函数,并限制它们在计算返回值时只能操作自身的参数。
它隔离函数,保护它们免受代码外部更改的影响。
禅宗准则建议:
避免:不透明函数
public double PriceOfFood(double price)
{
// if the number of people changes, the price of food changes
// regardless of price changing
return this.numberOfPeople * price;
}
首选:透明功能
public double PriceOfFood(double price, int numberOfPeople)
{
return numberOfPeople * price;
}
高阶函数
C# 完全将函数视为一等公民。让我们把这些函数当作一等公民来对待。C# 将函数视为一等公民。这意味着函数可以作为方法的参数;函数可以赋值给变量;甚至可以作为其他函数的返回值。
将函数作为一等公民,使我们能够将代码拆分成一个个小的构建块,每个构建块都封装了一小段(希望是干净、纯粹且引用透明的)功能。这些小块可以组合使用,实现不同的功能。随着需求的变化,我们可以替换和互换这些小块,而对代码库的影响微乎其微。
按需生成功能——不要编写大量代码。让函数像我们所知的运转部件一样运行。
高阶函数接受函数作为参数,或者返回函数。由于 C# 将函数视为一等公民,因此允许我们创建高阶函数。
禅宗准则建议:
避免:不明确的 lambda表达式
var range = Enumerable.Range(-20, 20);
range.Where(i => i % 2 == 0);
range.Where(i => i % 5 != 0);
首选:命名函数
Func<int, bool> isMod(int n) => i => i % n == 0;
range.Where(isMod(true, 2));
range.Where(!isMod(false, 5));
避免:过程式功能调用(如适用)
public class Car {...}
public void ChangeOil(Car car) {...}
public void ChangeTyres(Car car) {...}
public void Refuel(Car car) {...}
public void PitStop(Car car)
{
ChangeOil(car);
ChangeTyres(car);
Refuel(car);
}
PitStop(car);
优先:动态函数调用(如适用)
public class Car {...}
public void ChangeOil(Car car) {...}
public void ChangeTyres(Car car) {...}
public void Refuel(Car car) {...}
public void PitStop(Car car, IEnumerable<Action<Car>> pitstopRoutines)
{
foreach(var routine in pitstopRoutines)
routine(car);
}
PitStop(car, new List<Action<Car>>{ChangeOil, ChangeTypes, Refuel});
这种方法PitStop更具可扩展性。它易于扩展,几乎不需要修改。只需将要执行的函数作为参数传递,并将它们视为一等公民,即可实现这一点。
现在,去拥抱禅意吧,我的兄弟们。