函数组合


流水线 函数组合
每个函数输出是下一个函数输入 返回多个函数组合而成的新函数
立即求值 延迟求值

Example:

咖啡豆 -> 磨豆 -> [咖啡粒] -> 冲咖啡 -> [咖啡]

使用链的方式调用

Func<Beans,Ground> grind = beans => new Ground(beans);
Func<Ground,Coffee> brew = ground => new Coffee(ground);

Coffee coffee = brew(grind(beans));

上方代码是个可读性糟糕的选择,因为迫使使用从内到外(从右到左)的顺序阅读代码语义,与习惯不符合。

一个更佳的方式是通过一个泛型函数拓展函数组合的功能。

public static Func<A, C> Compose<A, B, C>(this Func<A, B> f, Func<B, C> g)
{
  return arg => g(f(arg));
}

Func<Beans,Coffee> makeCoffee = grind.Compose(brew);
Coffee coffee = makeCoffee(beans);

闭包


闭包对所引用的所有非局部变量进行隐式绑定,是一种方便的可以让函数访问本地状态并传递数据到后台操作的方式。

λ与匿名函数

C#通过λ表达式与匿名函数来简化闭包的使用。

λ表达式与匿名函数在多数情况下会在编译时生成一个匿名类,调用匿名方法时,相当于调用这个匿名类中的实例方法。

当匿名函数没有捕获任何外部变量时,匿名类的实例是单例。

当匿名函数捕获了外部变量时:

  • 引用的是外部局部变量时,将会在匿名类生成实例字段,并且匿名类实例没有缓存,每次对局部变量的赋值都会变成对匿名类实例的赋值
  • 引用的是外部实例字段时,lambda表达式会转成当前类的匿名方法

更多例子可参考黑洞视界大佬的文章

多线程闭包

由于C#的可变性,在多线程环境时要特别注意匿名函数捕获变量值的问题。

Action<int> num = n => Console.WriteLine(n);
int i = 10;
Task a = Task.Factory.StartNew(() => num(i));
i = 20;
Task b = Task.Factory.StartNew(() => num(i));
Task.WaitAll(a, b);

//将会输出两个20

缓存

当函数被相同参数重复调用时,重复的计算将浪费大量的性能,这时应该考虑将结果缓存下来。

public static Func<T, R> Memoize<T, R>(Func<T, R> func) where T : IComparable
{
	Dictionary<T, R> dic = new Dictionary<T, R>();
	return arg => dic.ContainsKey(arg)
			? dic[arg]
			: dic[arg] = func(arg);
}

上面展示了一个使用字典作为缓存的例子,然而字典寻值的哈希函数在某些情况下可能很慢(慢过执行运算本身),实际中需要测试和分析决定需不需要缓存结果。

多线程缓存

使用并发字典ConcurrentDictionary:

public static Func<T, R> MemoizeThreadSafe<T, R>(Func<T, R> func) where T : IComparable
{
	ConcurrentDictionary<T, R> dic = new ConcurrentDictionary<T, R>();
	return arg => dic.GetOrAdd(arg,func(arg));
}

延迟化缓存

使用Lazy来解决可能出现的并发性能问题:

public static Func<T, R> MemoizeLazyThreadSafe<T, R>(Func<T, R> func) where T : IComparable
{
	ConcurrentDictionary<T, Lazy<R>> dic = new ConcurrentDictionary<T, Lazy<R>>();
	return arg => dic.GetOrAdd(arg, new Lazy<R>(func(arg)).Value);
}

进一步优化

  • 使用弱引用避免字典可能存在的内存泄露问题,如使用CollectionConfitionalWeakDictionary。
  • 缓存过期机制
  • ...