Безопасная проверка неповторяемых IEnumerables на пустоту - PullRequest
13 голосов
/ 08 февраля 2012

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

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

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

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

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

IЕсть два вопроса по этому поводу:

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 может быть огромным в моем случае, и важно не хранить их все в памяти сразу.)

Ответы [ 3 ]

3 голосов
/ 08 февраля 2012

Вы также можете просто прочитать первый элемент и, если он не нулевой, объединить этот первый элемент с остальной частью вашего ввода:

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.

2 голосов
/ 08 февраля 2012

Вам не нужно усложнять это.Обычный цикл 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;
            }
        }
    }
}
1 голос
/ 08 февраля 2012

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

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