在现实生活中,分组统计是很常用的。例如人民银行要求商业银行报送的反洗钱报表中就有一个项目是当月发生的大额交易的笔数和金额, 其中大额交易定义为某个客户的当日累计发生额在人民币20万元或者外币等值1万美元以上。这样就要从大量的交易流水账中按交易日期进行分 组统计。
让我们来生成要统计的数据,如下所示:
IEnumerable<Tuple<int, double>> GetTuples(int n)
{
var tuples = new Tuple<int, double>[n];
var rand = new Random();
for (int k = 1, i = 0; i < n; i++)
{
var r = rand.Next(n);
k += (r >= n - 3) ? 2 : ((r >= n - 9) ? 1 : 0);
tuples[i] = new Tuple<int, double>(k, rand.NextDouble());
}
return tuples;
}
该方法生成 n 项已经排好序的数据。
现在,让我们来按关键字分组,并统计每组的 个数和平均值。
首先,使用 C# 的 foreach 循环,如下所示:
IEnumerable<Tuple<int, int, double>> ForEach(IEnumerable<Tuple<int, double>> tuples)
{
var result = new List<Tuple<int, int, double>>();
var count = 0;
var sum = 0.0;
int? key = null;
foreach (var v in tuples)
{
if (key != v.Item1)
{
if (key != null) result.Add(new Tuple<int, int, double>(key.Value, count, sum / count));
sum = count = 0;
key = v.Item1;
}
count++;
sum += v.Item2;
}
if (key != null) result.Add(new Tuple<int, int, double>(key.Value, count, sum / count));
return result;
}
这种方法有个 最大的缺点就是在 foreach 循环结束之后还要进行一次统计,闻到了代码的“坏味道”。
那么,就让我们来重构吧,这次 ,使用迭代器进行循环:
IEnumerable<Tuple<int, int, double>> Iterate(IEnumerable<Tuple<int, double>> tuples)
{
var result = new List<Tuple<int, int, double>>();
var count = 0;
var sum = 0.0;
int? key = null;
for (var iter = tuples.GetEnumerator(); ; count++, sum += iter.Current.Item2)
{
var hasValue = iter.MoveNext();
if (!hasValue || key != iter.Current.Item1)
{
if (key != null) result.Add(new Tuple<int, int, double>(key.Value, count, sum / count));
if (!hasValue) break;
sum = count = 0;
key = iter.Current.Item1;
}
}
return result;
}
这样,就消灭了“坏味道”。