Подтвердить что ты не робот

Безопасная проверка неповторяющихся IEnumerables для пустоты

Бывают случаи, когда полезно проверить не повторяющийся IEnumerable, чтобы узнать, пуст ли он. LINQ Any не подходит для этого, поскольку он потребляет первый элемент последовательности, например

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

(Примечание: я знаю, что нет необходимости делать проверку в этом случае - это просто пример! Пример реального мира выполняет Zip против правого IEnumerable, который потенциально может быть пусто. Если он пуст, я хочу, чтобы результат был левым IEnumerable as-is.)

Я придумал потенциальное решение, которое выглядит так:

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:

Мне нужно уточнить, что я ищу решение, которое сохраняет ленивый характер IEnumerable - преобразование его в список или массив может быть ответом при определенных обстоятельствах, но это не то, что я после здесь. (Реальная причина в том, что количество элементов в IEnumerable может быть огромным в моем случае, и важно не хранить их все в памяти сразу.)

4b9b3361

Ответ 1

Вам не нужно усложнять его. Регулярный цикл foreach с одной дополнительной переменной bool сделает трюк.

Если у вас

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;
            }
        }
    }
}

Ответ 2

Вы также можете просто прочитать первый элемент и, если он не равен 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.

Ответ 3

Это не эффективное решение, если перечисление длинное, однако это простое решение:

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