最近有点闲暇时间,就看了看Kotlin的语法,发现一些非常有意思的东西,比如对于集合的Map与Reduce操作,为此很值得做点读书笔记。下面总结一下Kotlin中的MapReduce操作及其在C#中的实现。
现在有一个很简单的需求,将1-100这100个数中3的倍数找出来,并且用”3,6,9…”这样每两个数字中间有一个逗号间隔的形式输出,那么采用原始方法也不复杂:
1 2 3 4 5 6 7 8 9 10 var list = Enumerable.Range(1 , 100 ).ToList();var result = string .Empty;foreach (var v in list){ if (v % 3 != 0 ) continue ; result += $"{v} ," ; } if (!string .IsNullOrEmpty(result)) result = result.TrimEnd(',' ); Console.WriteLine(result);
但是使用MapReduce方法的话就更简单了,看看Kotlin中是怎么做的:
1 2 3 4 5 6 val myList = 1 ..100 val reduceStr = myList .filter { it % 3 == 0 } .map { it.toString() } .reduce { a, b -> "$a ,$b " } println(reduceStr)
看起来非常简单,其实这个东西在C#中使用Linq同样可以做到类似简洁的函数式编程,下面就使用C#写一下(需要引用System.Linq名称空间):
1 2 3 4 5 6 var list = Enumerable.Range(1 , 100 ).ToList();var reduceStr = list .FindAll(v => v % 3 == 0 ) .Select(v => v.ToString()) .Aggregate((v1, v2) => $"{v1} ,{v2} " ); Console.WriteLine(reduceStr);
这样,看起来C#和Kotlin的语法基本一致,只是使用的方法名称不同,如果我们追求完全一样的方法名称,可以对C#现存的类进行扩展:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static class Extension { public static List<T> Filter<T>( this List<T> list, Predicate<T> match) => list.FindAll(match); public static IEnumerable<TResult> Map<T, TResult>( this IEnumerable<T> list, Func<T, TResult> selector) => list.Select(selector); public static T Reduce<T>( this IEnumerable<T> list, Func<T, T, T> func) => list.Aggregate(func); }
这里的扩展只是将原来的FindAll/Select/Aggregate方法换了个名称,扩展之后就可以在C#代码中写出和kotlin完全一致的代码了。
另外,从这里也可以看出kotlin从C#中借鉴了很多东西,比如这里的lambda及Linq的语法,除此之外还有扩展方法和可空类型等,例如C#中可以使用int?表明这是一个可以为空的整型变量,而Kotlin中也有相同的Int?类型,当然,两者从int/Int这种值类型转化为int?/Int?这种引用类型都需要进行装箱操作。
其实,这种映射聚合的思想可以解决很多问题,一般都是采用和前面例子中类似的三步走战略:筛选—>映射—>聚合,便可以使用很少的代码得到我们想要的结果,比如下面的例子:
求首项为1,公差为2的等差数列的前20项的乘积:
1 2 3 var list = Enumerable.Range(1 , 20 );var result = list.Map(v => 1 + 2 * (v - 1 )).Reduce((v1, v2) => v1 * v2);Console.WriteLine(result);
词频统计:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 var texts = new List<string >(){ "Good Morning" , "Good Evening" , "Have a nice day" , "Have a nice Morning" }; var wordCountDict = texts .Map(v => { var dict = new Dictionary<string , int >(); foreach (var item in v.Split()) { if (dict.ContainsKey(item)) dict[item]++; else dict.Add(item, 1 ); } return dict; }) .Reduce((v1, v2) => { foreach (var key in v2.Keys) { if (v1.ContainsKey(key)) v1[key] += v2[key]; else v1.Add(key, v2[key]); } return v1; }); wordCountDict.Keys .ToList() .ForEach(v => Console.WriteLine($"{v,-10 } :{wordCountDict[v]} " ));
输出结果:
1 2 3 4 5 6 7 Good :2 Morning :2 Evening :1 Have :2 a :2 nice :2 day :1
例2只是作为一个说明的例子,但是这样的解决方案付出的代价是比较高的,因为需要在Map的过程中实例化很多Dictionary的实例,如果数据比较多的话对内存的压力较高。
MapReduce在分布式计算中起着举足轻重的作用,一般而言,分布式计算就是采用这种先分后合的思想。将一项很复杂的任务交给分布式计算机去处理,其做法是先将整个大任务分割成很多小任务,让每一台子计算机处理其中一个小任务,这便是Map的过程,然后将Map形成的中间结果再汇总起来,得到最后需要的结果,这便是Reduce的过程。