安全地检查不可重复的IEnumerables是否为空

本文关键字:IEnumerables 是否 检查 安全 | 更新日期: 2023-09-27 18:23:42

有时检查不可重复IEnumerable以查看它是否为空会很有帮助。LINQ的Any不能很好地工作,因为它消耗了序列的第一个元素,例如

if(input.Any())
{
    foreach(int i in input)
    {
        // Will miss the first element for non-repeatable sequences!
    }
}

(注意:我知道在这种情况下没有必要进行检查-这只是一个例子!真实世界的例子是对可能为空的右侧IEnumerable执行Zip。如果它为空,我希望结果是左侧IEnumerable。)

我想出了一个潜在的解决方案,看起来像这样:

private static IEnumerable<T> NullifyIfEmptyHelper<T>(IEnumerator<T> e)
{
    using(e)
    {
        do
        {
            yield return e.Current;
        } while (e.MoveNext());
    }
}
public static IEnumerable<T> NullifyIfEmpty<T>(this IEnumerable<T> source)
{
    IEnumerator<T> e = source.GetEnumerator();
    if(e.MoveNext())
    {
        return NullifyIfEmptyHelper(e);
    }
    else
    {
        e.Dispose();
        return null;
    }
}

然后可以按如下方式使用:

input = input.NullifyIfEmpty();
if(input != null)
{
    foreach(int i in input)
    {
        // Will include the first element.
    }
}

我有两个问题:

1) 这样做合理吗?从性能的角度来看,这可能有问题吗?(我想不会,但值得一问。)

2) 有没有更好的方法来实现同样的最终目标?


编辑#1:

下面是一个不可重复IEnumerable的例子,以澄清:

private static IEnumerable<int> ReadNumbers()
{
    for(;;)
    {
        int i;
        if (int.TryParse(Console.ReadLine(), out i) && i != -1)
        {
            yield return i;
        }
        else
        {
            yield break;
        }
    }
}

基本上,来自用户输入或流等的东西。

编辑#2:

我需要澄清的是,我正在寻找一种保留IEnumerablelazy特性的解决方案——在某些情况下,将其转换为列表或数组可能是一个答案,但这不是我想要的。(现实世界的原因是,在我的情况下,IEnumerable中的项目数量可能很大,不要一次将它们全部存储在内存中,这一点很重要。)

安全地检查不可重复的IEnumerables是否为空

您也可以只读取第一个元素,如果它不是null,则将第一个元素与其他输入连接起来:

var input = ReadNumbers();
var first = input.FirstOrDefault();
if (first != default(int)) //Assumes input doesn't contain zeroes
{
    var firstAsArray = new[] {first};
    foreach (int i in firstAsArray.Concat(input))
    {
        // Will include the first element.
        Console.WriteLine(i);
    }
}

对于一个正常的可枚举对象,第一个元素会重复两次,但对于一个不可重复的可枚举项目,它会起作用,除非不允许迭代两次。此外,如果你有这样一个枚举器:

private readonly static List<int?> Source = new List<int?>(){1,2,3,4,5,6};
private static IEnumerable<int?> ReadNumbers()
{
    while (Source.Count > 0) {
        yield return Source.ElementAt(0);
        Source.RemoveAt(0);
    }
}

然后它会打印:1,1,2,3,4,5,6。原因是第一个元素在返回后被消耗掉。因此,第一个枚举器在第一个元素处停止,永远没有机会消耗第一个元素。但在这里,这将是一个写得不好的枚举器的例子。如果元素被消耗,则返回。。。

while (Source.Count > 0) {
    var returnElement = Source.ElementAt(0);
    Source.RemoveAt(0);
    yield return returnElement;
}

您得到的预期输出为:1、2、3、4、5、6。

您不需要将其复杂化。一个带有单个额外bool变量的常规foreach循环就可以了。

如果你有

if(input.Any())
{
    A
    foreach(int i in input)
    {
        B
    }
    C
}

如果你不想读input两次,你可以把它改为

bool seenItem = false;
foreach(int i in input)
{
    if (!seenItem)
    {
        seenItem = true;
        A
    }
    B
}
if (seenItem)
{
    C
}

根据B的作用,您可以完全避免使用seenItem变量。

在您的情况下,Enumerable.Zip是一个相当基本的函数,可以很容易地重新实现,您的替换函数可以使用与上面类似的东西。

编辑:您可以考虑

public static class MyEnumerableExtensions
{
    public static IEnumerable<TFirst> NotReallyZip<TFirst, TSecond>(this IEnumerable<TFirst> first, IEnumerable<TSecond> second, Func<TFirst, TSecond, TFirst> resultSelector)
    {
        using (var firstEnumerator = first.GetEnumerator())
        using (var secondEnumerator = second.GetEnumerator())
        {
            if (secondEnumerator.MoveNext())
            {
                if (firstEnumerator.MoveNext())
                {
                    do yield return resultSelector(firstEnumerator.Current, secondEnumerator.Current);
                    while (firstEnumerator.MoveNext() && secondEnumerator.MoveNext());
                }
            }
            else
            {
                while (firstEnumerator.MoveNext())
                    yield return firstEnumerator.Current;
            }
        }
    }
}

如果枚举很长,这不是一个有效的解决方案,但它是一个简单的解决方案:

var list = input.ToList();
if (list.Count != 0) {
    foreach (var item in list) {
       ...
    }
}