【问题标题】:How do I ensure a sequence has a certain length?如何确保序列具有一定的长度?
【发布时间】:2010-09-27 08:10:05
【问题描述】:

我想检查IEnumerable 是否包含完全 一个元素。这个 sn-p 确实有效:

bool hasOneElement = seq.Count() == 1

但是效率不是很高,因为Count() 会枚举整个列表。显然,知道一个列表是空的或包含超过 1 个元素意味着它不是空的。是否有具有这种短路行为的扩展方法?

【问题讨论】:

  • 我认为我在代码中所犯的逻辑错误可能会引发人们对至少完全的看法。我认为我们现在已经找到了最好的解决方案。
  • Count() 将仅在确定传递的 IEnumerable 未强制转换为 ICollection 时迭代整个列表。因此,如果您传递 List 实例或数组,它不会迭代。
  • 我认为这都是过早的优化。
  • 我不会考虑将 O(1) 而不是 O(n) 视为“过早优化”的简单解决方案;这只是一个明智的算法选择。

标签: c# linq extension-methods


【解决方案1】:

应该这样做:

public static bool ContainsExactlyOneItem<T>(this IEnumerable<T> source)
{
    using (IEnumerator<T> iterator = source.GetEnumerator())
    {
        // Check we've got at least one item
        if (!iterator.MoveNext())
        {
            return false;
        }
        // Check we've got no more
        return !iterator.MoveNext();
    }
}

可以进一步省略,但我不建议您这样做:

public static bool ContainsExactlyOneItem<T>(this IEnumerable<T> source)
{
    using (IEnumerator<T> iterator = source.GetEnumerator())
    {
        return iterator.MoveNext() && !iterator.MoveNext();
    }
}

这是一种很时髦的技巧,但可能不应该在生产代码中使用。只是不够清楚。 && 运算符在 LHS 中的副作用是 RHS 正常工作所必需的,这一事实令人讨厌......虽然很有趣 ;)

编辑:我刚刚看到你想出了完全相同的东西,但长度是任意的。不过,您的最终退货声明是错误的 - 它应该是退货!en.MoveNext()。这是一个完整的方法,具有更好的名称 (IMO)、参数检查和优化 ICollection/ICollection&lt;T&gt;

public static bool CountEquals<T>(this IEnumerable<T> source, int count)
{
    if (source == null)
    {
        throw new ArgumentNullException("source");
    }
    if (count < 0)
    {
        throw new ArgumentOutOfRangeException("count",
                                              "count must not be negative");
    }
    // We don't rely on the optimizations in LINQ to Objects here, as
    // they have changed between versions.
    ICollection<T> genericCollection = source as ICollection<T>;
    if (genericCollection != null)
    {
        return genericCollection.Count == count;
    }
    ICollection nonGenericCollection = source as ICollection;
    if (nonGenericCollection != null)
    {
        return nonGenericCollection.Count == count;
    }
    // Okay, we're finally ready to do the actual work...
    using (IEnumerator<T> iterator = source.GetEnumerator())
    {
        for (int i = 0; i < count; i++)
        {
            if (!iterator.MoveNext())
            {
                return false;
            }
        }
        // Check we've got no more
        return !iterator.MoveNext();
    }
}

编辑:现在对于功能爱好者来说,CountEquals 的递归形式(请不要使用它,它只是为了笑):

public static bool CountEquals<T>(this IEnumerable<T> source, int count)
{
    if (source == null)
    {
        throw new ArgumentNullException("source");
    }
    if (count < 0)
    {
        throw new ArgumentOutOfRangeException("count", 
                                              "count must not be negative");
    }
    using (IEnumerator<T> iterator = source.GetEnumerator())
    {
        return IteratorCountEquals(iterator, count);
    }
}

private static bool IteratorCountEquals<T>(IEnumerator<T> iterator, int count)
{
    return count == 0 ? !iterator.MoveNext()
        : iterator.MoveNext() && IteratorCountEquals(iterator, count - 1);
}

编辑:请注意,对于 LINQ to SQL 之类的东西,您应该使用简单的Count() 方法 - 因为这样可以在数据库中完成,而不是在获取实际结果之后。

【讨论】:

  • 是的,它很丑,是的,它很漂亮 :-) 但是不,当你第一次看到这段代码时,它并不是很明显。
  • 哈哈@Jon 为他的return iterator.MoveNext() &amp;&amp; !iterator.MoveNext(); 。我的答案是“两者”!
  • 如果在 linq-to-sql 查询中调用 ContainsExactlyOneItem 会发生什么?是否会从数据库中获取数据?
  • @diamandiev:是的。对于 LINQ to SQL,我建议改用 Count() 在数据库中执行此操作。
  • 您可以像 Count() 一样为 ICollection 添加测试,并跳过所有迭代。
【解决方案2】:

没有,但你可以自己写一个:

 public static bool HasExactly<T>(this IEnumerable<T> source, int count)
 {
   if(source == null)
      throw new ArgumentNullException("source");

   if(count < 0)
      return false;

   return source.Take(count + 1).Count() == count;
 }

编辑:在澄清后从 atleast 更改为 exactly

对于更通用和更有效的解决方案(仅使用 1 个枚举器并检查序列是否实现 ICollectionICollection&lt;T&gt; 在这种情况下不需要枚举),您可能想看看我的答案 @987654321 @,可让您指定是在寻找ExactAtLeast 还是AtMost 测试。

【讨论】:

  • 难道你最终还没有在那个 sn-p 中枚举整个列表吗? (由于Take,然后是Count
  • 我不是要求至少,我要求的是正是
  • 但是,你会重复迭代项目,不是吗?首先是拍摄,然后是计数。只有我们感兴趣的项目的长度,但仍然是所需迭代次数的两倍。
  • @SamStephens:是的,没错。这就是为什么更有效(但丑陋)的解决方案可能更合适的原因。
【解决方案3】:

seq.Skip(1).Any() 会告诉您列表中是否包含零个或一个元素。

我认为您所做的编辑是关于检查长度的最有效方法是 n。但是有一个逻辑错误,长度小于 long 的项目将返回 true。看看我对第二个 return 语句做了什么。

    public static bool LengthEquals<T>(this IEnumerable<T> en, int length)
    {
        using (var er = en.GetEnumerator())
        {
            for (int i = 0; i < length; i++)
            {
                if (!er.MoveNext())
                    return false;
            }
            return !er.MoveNext();
        }
    }

【讨论】:

  • 是的,我自己也注意到了这一点。我更喜欢你的方法名,但我的变量名 :) - 实际上我已经决定毕竟我更喜欢 CountEquals,因为它更适合 Count() 方法:)
【解决方案4】:

这个怎么样?

public static bool CountEquals<T>(this IEnumerable<T> source, int count) {
    return source.Take(count + 1).Count() == count;
}

Take() 将确保我们调用MoveNext 的次数不会超过count+1 次。

我想指出,对于 ICollection 的任何实例,原始实现 source.Count() == count 应该更快,因为 Count() 已优化为仅查看 Count 成员。

【讨论】:

    【解决方案5】:

    我相信您正在寻找的是.Single()。除了一个之外的任何东西都会抛出你可以捕获的 InvalidOperationException。

    http://msdn.microsoft.com/nb-no/library/bb155325.aspx

    【讨论】:

    • 我想我想要做的事情可以用这个来实现,但我不喜欢为控制流抛出异常。
    • 然后使用 SingleOrDefault() 并自己检查返回值是否为空。
    • SingleOrDefault 如果列表中有多个项目,仍然会抛出异常。我不认为这是要走的路,带有枚举器的代码应该更高效。
    • @SamStephens 我的错,我没有考虑到这一点。我同意在这种情况下抛出/捕获异常不是要走的路。
    猜你喜欢
    • 1970-01-01
    • 2019-01-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-29
    相关资源
    最近更新 更多