你所谓的item实际上是Order。
首先,您将具有相同{CreatedTime, Quantity} 组合的Orders 分组:
AllOrders.GroupBy(order => new {order.CreatedTime, order.Quantity})
结果是一系列组。每个组都有一个 Key {CreatedTime, Quantity};每个组都是具有此 Key 的 Orders 序列。
然后您过滤您的订单组。您只想保留那些至少有一个标签等于“Name1”的订单和至少一个标签等于“Name2”的订单的订单组
.Where(group => group.Any(order => order.Tag == "Name1")
&& group.Any(order => order.Tag == "Name2"))
所以你拿了小组的第一个订单。它有 Tag=="Name1" 吗?不,拿小组的第二名。它是否有 Tag=="Name1"。没有。等等,直到您在 20 号订单上找到标签
然后你从第一个订单重新开始。它有 Tag=="Name2" 吗?没有。等等。直到你在 15 号订单上找到标签。
问题是,在搜索“Name1”时,您已经发现第一个订单没有“Name2”。您还看到了具有 Name2 的 15th Order,但您正在寻找 Name1。
结果是你必须枚举你的序列两次。
您想要做的是记住您看到了 Name2,直到您也看到 Name1。
创建一个类似Any 的扩展方法,它检查两个谓词,并在找到与谓词1 匹配的元素以及与谓词2 匹配的元素时立即返回true:
public static bool Any<Tsource>(this IEnumerable<Tsource> source,
Func<Tsource, bool> predicate1,
Func<Tsource, bool> predicate2)
{
// TODO: exception if source or predicates null
bool predicate1Met = false;
bool pedicate2Met = false;
using (var enumerator = source.GetEnumerator())
{
// enumerate every element, and check for every element if the predicates are met
// don't check on predicates that are already met
// stop as soon as both predicates are met.
while (enumerator.MoveNext() && !predicate1Met && !predicate2Met)
{
// There is a next element in the source. predicates not met yet
// check the predicates on this element
// but only if the predicates are not met yet
predicate1met = predicate1met || predicate1(enumerator.Current);
predicate2met = predicate2met || predicate2(enumerator.Current);
}
}
// if here, all elements enumerated, or both predicates met
return predicate1met && predicate2met;
}
所以现在你可以优化你的Where:
.Where(group => group.Any(item => item.Tag == "Name1", // predicate 1
item => item.Tag == "Name2")) // predicate 2
在您按 CreatedTime 的降序排列所有组的位置之后。然后你扔掉大部分这些有序组:
.OrderByDescending(item => item.Key.CreatedTime)
.Take(1)
.Select(g => g.Key.Quantity)
.FirstOrDefault();
只找到 CreatedTime 最大的组不是更好吗?这样您就不必多次枚举该组。您需要某种Max or default 方法,您可以在其中决定要最大化哪个键。
再次:让我们创建一个类似于 LINQ 的扩展方法。就像我们在 LINQ 中看到的那样,我们创建了带有和不带有比较器的重载。
public static TSource MaxOrDefault<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
Func<TSource, TResult> resultSelector)
{
// call the Max method with a null comparer
return MaxOrDefault(source, keySelector, resultSelector, null);
}
比较器的重载:
public static TSource MaxOrDefault<TSource, TKey>(
this IEnumerable<TSource> source,
this Func<TSource, TKey> keySelector,
Func<TSource, TResult> resultSelector,
IComparer<TKey> comparer)
{
// TODO: exception if input null, allow null comparer
if (comparer == null) comparer = Comparer<TKey>.Default();
// enumerate the sequence
var enumerator = source.GetEnumerator();
if (enumerator.MoveNext())
{
// the source has at least one element. For now this is the max:
TSource max = enumerator.Current;
TKey maxKey = keySelector(max);
// are there larger elements in source? let's enumerate the rest
while (enumerator.MoveNext())
{
TKey currentKey = keySelector(enumerator.Current);
if (comparer.Compare(currentKey, maxKey) > 0)
{
// we found a larger element:
max = enumerator.Current;
maxKey = currentKey;
}
}
// all elements are enumerated. Create the result
TResult result = resultSelector(max);
return result;
}
else
{
// the source is empty; return default:
return default(TResult);
}
}
这将替换 `OrderBy(...).Take().Select(...).FirstOrDefault 为:
.MaxOrDefault(group => group.Key.CreatedTime, // keySelector: get the max value
group => group.Key.Quantity) // resultSelector: when found use this
所以你的完整 LINQ 将是:
var result = AllOrders
.GroupBy(order => new {order.CreatedTime, order.Quantity})
.Where(group => group.Any(item => item.Tag == "Name1",
item => item.Tag == "Name2"))
.MaxOrDefault(group => group.Key.CreatedTime,
group => group.Key.Quantity);
那么你将枚举你的序列多少次:
- 一次组团
- 每组最多一次,直到找到标签
- 剩余组:只有组的键,而不是组中的命令:一次
因此,如果您的 1000 个订单将分为 10 个组,您需要检查每个组大约一半才能找到标签。可能会枚举一个组中没有标签的所有订单,但话又说回来:你不会再使用它们了。
最后,从剩余的组中,您只枚举键一次。
好消息是,Any with two predicates 和 MaxOrDefault with predicate 是相当有用的方法,您可能会在其他情况下使用它们。特别是 MaxOrDefault 是我在类似于 FirstOrDefault 的情况下经常使用的。标准 Max 不适用于空序列。
进一步优化
我认为您可以通过将所有内容放在一个扩展方法中来进一步优化它。但是,此方法不可重复使用。但是你可以在只枚举你的序列一次的同时做到这一点。
public int ToMaxNewestOrder(this IEnumerable<Order> orders)
{
// create a Dictionary with key `{CreatedTime, Quantity}`.
// Values are the booleans: name1Found, name2Found
var dictionary = new Dictionary<Tuple<DateTime, int>, Tuple<bool, bool>();
foreach (var order in orders)
{
bool tag1Mach = order.Tag == "Name1");
bool tag2Mach = order.Tag == "Name2");
var orderKey = Tuple.Create(order.CreatedTime, order.Quantity);
if (dictionary.TryGetValue(orderKey, out Tuple<bool, bool, int> value)
{
// the key is in the dictionary. Update the tags
value.Item1 = value.Item1 || tag1Match);
value.Item2 = value.Item1 || tag2Match);
}
else
{
// this key not in dictionary yet
dictionary.Add(orderKey, Tuple.Create(tag1Match, tag2Match));
}
}
// all order processed. We only need to enumerate the KeyValues from the dictionary
// to keep the ones with boot tags matching
// remember the maximum value for quantity
int maxQuantity = 0;
foreach(var keyValuePair in dictionary)
{
if (keyValuePair.Value.Item1 // any of the Tags matches name1
&& keyValuePair.Value.Item2 // any of the Tags matches name2
&& keyValuePair.Key.Quantity > maxQuantity)
{
maxQuantity = quantity;
}
}
return maxQuantity;
}
现在您确定您只列举了两次序列:一次是所有订单,一次是所有组。如果每个订单都有不同的 CreatedTime 和 Quantity,那么每个订单都将在自己的组中。最大值是您将 Orders 枚举两次。