2012-02-08 1 views
13

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

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

(Примечание: Я знаю, что нет необходимости делать проверку в данном случае - это просто пример реального мира пример выполнения Zip против правой IEnumerable, который потенциально может быть! . пустой Если он пуст, я хочу, чтобы результат левая 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; 
     } 
    } 
} 

В основном, вещи, которые приходят из пользовательского ввода или поток, и т.д.

EDIT # 2:

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

+1

Это умный подход. Мне будет интересно услышать отзывы других, но для меня это выглядит довольно элегантным способом решения вашей проблемы. –

+0

Не уверен, что вы подразумеваете под «non-repeatable ienumerable». У вас есть пример? В первом примере кода foreach не должен пропустить первый элемент, просто прочитайте его во второй раз. Я не понимаю, в какой ситуации это было бы невозможно. –

+0

@ Мета-рыцарь: Я добавил пример, надеюсь, что это поможет. –

ответ

2

Вам не нужно усложнять. Регулярный цикл 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 - довольно простая функция, которая легко переопределяется, и ваша функция замены может использовать нечто похожее на вышесказанное.

Edit: Вы могли бы рассмотреть

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

+1 это очень полезно. Я действительно написал версию «Zip», которая сделала что-то вроде этого, когда я пыталась изложить идеи, но в итоге я подумал, что лучше придумать более широко используемое многоразовое решение. –

+1

@StuartGolodetz В этом случае я не вижу, как вы можете найти лучшее общее решение, чем то, что у вас уже есть. :) – hvd

3

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

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.

+4

И если первый элемент ввода == 0? – Oded

+1

Да, это будет реально работать только для ссылочных типов. Однако он предлагает более сжатую версию решения OP ... –

+0

@Oded: Если это тип значения, и значение по умолчанию может отображаться в последовательности, вы можете использовать тип NULL для перечислимого. –

1

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

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

Это, к сожалению, не ленивое решение - если вы используете его с 'ReadNumbers', он будет ждать, пока пользователь перестанет вводить цифры, прежде чем что-либо делать. +1 хотя все равно, потому что я не сделал этого ясно в вопросе. –