有没有更好的方法调用LINQ Any + NOT All?

23

我需要检查一系列项是否有任何一项满足某个条件,但同时又不是所有项都满足相同的条件。

例如,对于一个包含10个项的序列,如果该序列至少有一个满足条件但不全部满足,则结果为TRUE:

  • 有10个满足条件的项,0个不满足,结果为FALSE
  • 有0个满足条件的项,10个不满足,结果为FALSE
  • 有1个满足条件的项,9个不满足,结果为TRUE
  • 有9个满足条件的项,1个不满足,结果为TRUE

我知道可以这样做:

mySequence.Any (item => item.SomeStatus == SomeConst) && !mySequence.All (item => item.SomeStatus == SomeConst)

但这并不是最优解。

有更好的方法吗?


1
你可以使用.Aggregate和一个自定义类型,该类型包含所需信息的属性 - 只是一个输入... - user57508
1
@AndreasNiedermair 那样做是可行的,但我认为你无法在找到两个选项后立即中止聚合,因此即使您不需要完成迭代,也会完成迭代。 - Rup
@Rup 绝对正确。我不知道 OP 是在寻找一个短路变体... - user57508
如果您的序列是具有“Count”或“Length”属性的集合,则获取.Count(item => item.SomeStatus == SomeConst)可能更便宜。如果该值大于0且小于集合大小,则返回true。如果序列是IEnumerable<T>,则由于短路,您的实现可能更优。 - Dmitry S.
8个回答

30

你会喜欢这个。

var anyButNotAll = mySequence
    .Select(item => item.SomeStatus == SomeConst)
    .Distinct()
    .Take(2)
    .Count() == 2;

Take(2) 方法可以阻止它迭代比所需更多的元素。


6
请注意,您几乎肯定希望将此代码放入一个方法中,例如SomeButNotAll,仅出于清晰起见,因为这段代码本身并没有很好地自我记录。 - Servy
4
不会。 - Rawling
2
@Servy的逻辑思考可能也意味着一些幼稚的想法(我自己也曾有过......),因此,坚持原始资料-那才是唯一的真理 :) - user57508
3
Enumerable.Count()需要完全求值,而Enumerable.Take()可以快速截断执行。 - user57508
2
@Maarten,“Take”不会改变结果,但它可以防止查询请求第三个项目,这需要完全评估其余序列以确定是否存在第三个项目。 - Servy
显示剩余15条评论

6
如果您担心需要遍历大型集合中的所有元素,没关系 - 只要有可能,AnyAll 就会立即短路。
语句:
mySequence.Any (item => item.SomeStatus == SomeConst)

只要找到符合条件的元素,就会立即返回true。
!mySequence.All (item => item.SomeStatus == SomeConst)

只要有一个元素不符合条件,此语句就会返回true。

由于这两个条件是互斥的,一个语句保证在第一个元素后返回,而另一个语句保证在找到第一个元素后立即返回。


正如其他人指出的那样,这个解决方案需要开始两次遍历集合。如果获得集合的成本很高(例如在数据库访问中),或者对集合进行迭代不能每次产生相同的结果,则这不是一个适合的解决方案。


但是如果源序列在多次迭代时不返回相同的项,或者需要进行昂贵的计算才能计算其项呢? - Servy
所以它尽可能地好,除了所有极为常见的情况之外,它并不是完美的...... 承认你的答案在你的回答中是错误的,并不能使它变得正确。 - Servy
@Servy 不需要这么讽刺。LINQ经常用于静态的内存集合,这种情况下不会出现上述问题。解决方案的适用范围有限并不意味着它是错误的。 - BJ Myers
它经常用于其他类型的序列,其中它不适用。因此,如果在频繁出现比替代方案表现明显更差的情况下说某个解决方案“尽善尽美”,那么实际上它并不是“尽善尽美”。如果您想要一个在某些情况下“尽善尽美”的解决方案,那么只需让实现“返回true;”它是所有满足条件和不满足条件的项目的最佳实现。 - Servy
1
解决方案的适用范围有限并不意味着它是错误的。如果OP只是在寻找任何解决问题的方法,并且不关心性能,那么这是正确的。但是当你说“它已经达到了最好的状态”,而实际上它并没有达到最佳状态时,那么它就是错误的。说它是一个解决方案是正确的,但当你声称它是最优解时,是不正确的 - Servy
1
问题是在问,“有没有更好的方法”。你的回答是“没有,你现在的方法很好”。这个回答是不正确的,有更好的方法,这些方法更高效,而且在性能和正确性方面都没有这个回答那么明显的缺陷。 - Servy

3

如果数据源是数据库,那么这可能是一个相当优化的解决方案。根据您的数据源,这个扩展方法可能更好(我只是随意编写的代码,可能有很多错误,请将其视为伪代码)。 这里的好处是它只枚举一次,并在读取足够量的数据以确定结果后立即执行短路操作:

static bool SomeButNotAll<TSource>(this IEnumerable<TSource> source,
                                   Func<TSource, bool> predicate)
{
   using(var iter=source.GetEnumerator())
   {
     if (iter.MoveNext())
     {
       bool initialValue=predicate(iter.Current);
       while (iter.MoveNext())
         if (predicate(iter.Current)!=initialValue)
           return true;
     }
   }     
   return false; /* All */
}

你需要接受一个 Func 而不是 Expression,因为你打算调用它,并且由于你只计划迭代 source 并且不做其他操作,所以你应该接受一个 IEnumerable 而不是 IQueryable - Servy
1
处理完枚举器后请将其释放。 - Jon Hanna

3

您可以定义自己的扩展方法。这个版本更冗长,但仍然易读,并且它只枚举了一次IEnumerable

bool AnyButNotAll<ItemT>(this IEnumerable<ItemT> sequence, Func<ItemT, bool> predicate)
{
    bool seenTrue = false;
    bool seenFalse = false;

    foreach (ItemT item in sequence)
    {
        bool predResult = predicate(item);
        if (predResult)
            seenTrue = true;
        if (!predResult)
            seenFalse = true;

        if (seenTrue && seenFalse)
            return true;
    }

    return false;
}

代码更加简短,但是会两次枚举 IEnumerable

bool AnyButNotAll<ItemT>(this IEnumerable<ItemT> sequence, Func<ItemT, bool> predicate)
{
    return sequence.Any(predicate) && !sequence.All(predicate);
}

你可以将 if (!predResult) 替换为 else - romanoza
@roman 你说得对;我不确定为什么我没有想到这一点。我猜我认为这两个if语句是相对独立的。 - Tanner Swett

2
你可以尝试这个:

你可以尝试这个:

var result = mySequence.Select(item => item.SomeStatus == SomeConst)
                      .Distinct().Count() > 1 ? false : true;

基本上我为每个值选择truefalse,使用distinct仅获取其中一个,然后计算它们的数量。


1
请注意,这可以防止在第一项为True和第二项为False的情况下出现短路。 - Servy

1
你可以将谓词放入变量中,这样你就不必重复谓词两次:
Func<MyItemType, bool> myPredicate = item => item.SomeStatus == SomeConst;
if (mySequence.Any(myPredicate) && !mySequence.All(myPredicate))
    ...

这个更改纯粹是样式上的,对其性能没有任何影响,而这也是问题所询问的。 - Servy
@Servy 我不知道提问者对纯样式建议不感兴趣。 - Tanner Swett

1
如果你想将其定义为一个方法,可以采用Linq的方法,同时定义IEnumerable<T>IQueryable<T>扩展方法。这样可以自动选择最佳方法:
public static bool SomeButNotAll<T>(this IQueryable<T> source, Expression<Func<T, bool>> predicate)
{
  if(source == null)
    throw new ArgumentNullException("source");
  if(predicate == null)
    throw new ArgumentNullException("predicate");
  return source.
    Select(predicate)
    .Distinct()
    .Take(2)
    .Count() == 2;
}
public static bool SomeButNotAll<T>(this IEnumerable<T> source, Func<T, bool> predicate)
{
  if(source == null)
    throw new ArgumentNullException("source");
  if(predicate == null)
    throw new ArgumentNullException("predicate");
  using(var en = source.GetEnumerator())
    if(en.MoveNext())
    {
      bool first = predicate(en.Current);
      while(en.MoveNext())
        if(predicate(en.Current) != first)
          return true;
    }
  return false;
}

如果您正在使用EntityFramework(或提供CountAsync的其他提供程序),您也可以轻松提供异步版本:
public static async Task<bool> SomeButNotAllAsync<T>(this IQueryable<T> source, Expression<Func<T, bool>> predicate, CancellationToken cancel)
{
  if(source == null)
    throw new ArgumentNullException("source");
  if(predicate == null)
    throw new ArgumentNullException("predicate");
  cancel.ThrowIfCancellationRequested();
  return await source.
    Select(predicate)
    .Distinct()
    .Take(2)
    .CountAsync(cancel)
    .ConfigureAwait(false) == 2;
}
public static Task<bool> SomeButNotAllAsync<T>(this IQueryable<T> source, Expression<Func<T, bool>> predicate)
{
  return source.SomeButNotAllAsync(predicate, CancellationToken.None);
}

0

2
“Aggregate” 不会短路 - 这将需要遍历整个集合。 - BJ Myers

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接