Вызов метода с IEnumerable <T>sequence в качестве аргумента, если эта последовательность не пуста - PullRequest
0 голосов
/ 02 декабря 2018

У меня есть метод Foo, который выполняет некоторые ресурсоемкие вычисления и возвращает последовательность IEnumerable<T>.Мне нужно проверить, если эта последовательность пуста.А если нет, вызовите метод Bar с этой последовательностью в качестве аргумента.

Я подумал о трех подходах ...

  • Проверьте, пуста ли последовательность с Any().Это нормально, если последовательность действительно пуста, что будет иметь место в большинстве случаев.Но это будет иметь ужасную производительность, если последовательность будет содержать некоторые элементы, и Foo потребуется их вычисление снова ...
  • Преобразовать последовательность в список, проверить, не является ли этот список пустым ... и передать его Bar.Это также имеет ограничение.Bar потребуются только первые x элементы, поэтому Foo будет выполнять ненужную работу ...
  • Проверьте, является ли последовательность пустой без фактического сброса последовательности.Это звучит как беспроигрышный вариант, но я не могу найти какой-либо простой способ, как это сделать.Поэтому я создаю этот непонятный обходной путь и задаюсь вопросом, действительно ли это лучший подход.

Условие

var source = Foo();

if (!IsEmpty(ref source))
    Bar(source);

с IsEmpty, реализованным как

bool IsEmpty<T>(ref IEnumerable<T> source)
{
    var enumerator = source.GetEnumerator();

    if (enumerator.MoveNext())
    {
        source = CreateIEnumerable(enumerator);
        return false;
    }

    return true;

    IEnumerable<T> CreateIEnumerable(IEnumerator<T> usedEnumerator)
    {
        yield return usedEnumerator.Current;

        while (usedEnumerator.MoveNext())
        {
            yield return usedEnumerator.Current;
        }
    }
}

Также обратите внимание, что вызов Bar с пустой последовательностью не возможен ...

РЕДАКТИРОВАТЬ: После некоторого рассмотрения лучший ответ для моего случая от Оливье Жако-Дескомб - полностью избежать этого сценария.Принятое решение отвечает на этот вопрос - если это действительно не так.

Ответы [ 5 ]

0 голосов
/ 02 декабря 2018

Принятый ответ, вероятно, является наилучшим подходом, но, основываясь на, и я цитирую:

Преобразовать последовательность в список, проверить, пуст ли этот список ... и передать его в Bar.Это также имеет ограничение.Для Bar понадобятся только первые x элементов, поэтому Foo будет выполнять ненужную работу ...

Еще один вариант - создать IEnumerable<T>, который частично кэширует основное перечисление.Что-то вроде следующего:

interface IDisposableEnumerable<T>
    :IEnumerable<T>, IDisposable
{
}

static class PartiallyCachedEnumerable
{
    public static IDisposableEnumerable<T> Create<T>(
        IEnumerable<T> source, 
        int cachedCount)
    {
        if (source == null)
            throw new NullReferenceException(
                nameof(source));

        if (cachedCount < 1)
            throw new ArgumentOutOfRangeException(
                nameof(cachedCount));

        return new partiallyCachedEnumerable<T>(
            source, cachedCount);
    }

    private class partiallyCachedEnumerable<T>
        : IDisposableEnumerable<T>
    {
        private readonly IEnumerator<T> enumerator;
        private bool disposed;
        private readonly List<T> cache;
        private readonly bool hasMoreItems;

        public partiallyCachedEnumerable(
            IEnumerable<T> source, 
            int cachedCount)
        {
            Debug.Assert(source != null);
            Debug.Assert(cachedCount > 0);
            enumerator = source.GetEnumerator();
            cache = new List<T>(cachedCount);
            var count = 0;

            while (enumerator.MoveNext() && 
                   count < cachedCount)
            {
                cache.Add(enumerator.Current);
                count += 1;
            }

            hasMoreItems = !(count < cachedCount);
        }

        public void Dispose()
        {
            if (disposed)
                return;

            enumerator.Dispose();
            disposed = true;
        }

        public IEnumerator<T> GetEnumerator()
        {
            foreach (var t in cache)
                yield return t;

            if (disposed)
                yield break;

            while (enumerator.MoveNext())
            {
                yield return enumerator.Current;
                cache.Add(enumerator.Current)
            }

            Dispose();
        }

        IEnumerator IEnumerable.GetEnumerator()
            => GetEnumerator();
    }
}
0 голосов
/ 02 декабря 2018

Вы хотели бы вызвать некоторую функцию Bar<T>(IEnumerable<T> source) тогда и только тогда, когда перечислимое source содержит хотя бы один элемент, но вы столкнулись с двумя проблемами:

  • Тамэто не метод T Peek() в IEnumerable<T>, поэтому вам нужно было бы на самом деле начать оценку перечислимого, чтобы увидеть, не пусто ли оно, но ...

  • ВыЯ не хочу даже частично оценивать перечислимое дважды, поскольку настройка перечислимого может быть дорогой.

В этом случае ваш подход выглядит разумным.Однако у вас есть некоторые проблемы с вашей реализацией:

  1. Вам необходимо утилизировать enumerator после его использования.

  2. Как указывает Иван Стоев в комментариях , если метод Bar() пытается оценить IEnumerable<T> более одного раза (например, позвонив Any() затем foreach (...)), тогда результаты будут неопределенными, поскольку usedEnumerator будет исчерпано при первом перечислении.

Чтобы решить эти проблемы, я быпредложите немного изменить свой API и создать метод расширения IfNonEmpty<T>(this IEnumerable<T> source, Action<IEnumerable<T>> func), который вызывает указанный метод, только если последовательность непуста, как показано ниже:

public static partial class EnumerableExtensions
{
    public static bool IfNonEmpty<T>(this IEnumerable<T> source, Action<IEnumerable<T>> func)
    {
        if (source == null|| func == null)
            throw new ArgumentNullException();
        using (var enumerator = source.GetEnumerator())
        {
            if (!enumerator.MoveNext())
                return false;
            func(new UsedEnumerator<T>(enumerator));
            return true;
        }
    }

    class UsedEnumerator<T> : IEnumerable<T>
    {
        IEnumerator<T> usedEnumerator;

        public UsedEnumerator(IEnumerator<T> usedEnumerator)
        {
            if (usedEnumerator == null)
                throw new ArgumentNullException();
            this.usedEnumerator = usedEnumerator;
        }

        public IEnumerator<T> GetEnumerator()
        {
            var localEnumerator = System.Threading.Interlocked.Exchange(ref usedEnumerator, null);
            if (localEnumerator == null)
                // An attempt has been made to enumerate usedEnumerator more than once; 
                // throw an exception since this is not allowed.
                throw new InvalidOperationException();
            yield return localEnumerator.Current;
            while (localEnumerator.MoveNext())
            {
                yield return localEnumerator.Current;
            }
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
}

Демонстрационная скрипка с юнит-тестами здесь .

0 голосов
/ 02 декабря 2018

Если вы можете изменить Bar, то как насчет изменить его на TryBar, который возвращает false, когда IEnumerable<T> пусто?

bool TryBar(IEnumerable<Foo> source)
{
  var count = 0;
  foreach (var x in source)
  {
    count++;
  }
  return count > 0;
}

Если это не сработает, вы всегда можетесоздайте свою собственную IEnumerable<T> оболочку, которая кэширует значения после того, как они были повторены один раз.

0 голосов
/ 02 декабря 2018

Одним из улучшений для вашего IsEmpty будет проверка, если source равно ICollection<T>, и если это так, отметьте .Count (также удалите счетчик):

bool IsEmpty<T>(ref IEnumerable<T> source)
{
    if (source is ICollection<T> collection)
    {
        return collection.Count == 0;
    }
    var enumerator = source.GetEnumerator();
    if (enumerator.MoveNext())
    {
        source = CreateIEnumerable(enumerator);
        return false;
    }
    enumerator.Dispose();
    return true;
    IEnumerable<T> CreateIEnumerable(IEnumerator<T> usedEnumerator)
    {
        yield return usedEnumerator.Current;
        while (usedEnumerator.MoveNext())
        {
            yield return usedEnumerator.Current;
        }
        usedEnumerator.Dispose();
    }
}

Этобудет работать для массивов и списков.

Я бы, однако, переделал IsEmpty, чтобы вернуть:

IEnumerable<T> NotEmpty<T>(IEnumerable<T> source)
{
    if (source is ICollection<T> collection)
    {
        if (collection.Count == 0)
        {
           return null;
        }
        return source;
    }
    var enumerator = source.GetEnumerator();
    if (enumerator.MoveNext())
    {
        return CreateIEnumerable(enumerator);
    }
    enumerator.Dispose();
    return null;
    IEnumerable<T> CreateIEnumerable(IEnumerator<T> usedEnumerator)
    {
        yield return usedEnumerator.Current;
        while (usedEnumerator.MoveNext())
        {
            yield return usedEnumerator.Current;
        }
        usedEnumerator.Dispose();
    }
}

Теперь вы проверите, вернул ли он значение null.

0 голосов
/ 02 декабря 2018

Я не знаю, позволяет ли ваш алгоритм в Foo определить, будет ли перечисление пустым, без выполнения вычислений.Но если это так, верните null, если последовательность будет пустой:

public IEnumerable<T> Foo()
{
    if (<check if sequence will be empty>) {
        return null;
    }
    return GetSequence();
}

private IEnumerable<T> GetSequence()
{
    ...
    yield return item;
    ...
}

Обратите внимание, что если метод использует yield return, он не может использовать простой return для возврата null,Поэтому необходим второй метод.

var sequence = Foo();
if (sequence != null) {
    Bar(sequence);
}

После прочтения одного из ваших комментариев

Foo необходимо инициализировать некоторые ресурсы, проанализировать XML-файл и заполнить некоторые HashSets, которыебудет использоваться для фильтрации (выдачи) возвращаемых данных.

Я предлагаю другой подход.Трудоемкая часть, кажется, инициализация.Чтобы иметь возможность отделить его от итерации, создайте класс калькулятора foo.Что-то вроде:

public class FooCalculator<T>
{
     private bool _isInitialized;
     private string _file;

     public FooCalculator(string file)
     {
         _file = file;
     }

     private EnsureInitialized()
     {
         if (_isInitialized) return;

         // Parse XML.
         // Fill some HashSets.

         _isInitialized = true;
     }

     public IEnumerable<T> Result
     {
         get {
             EnsureInitialized();
             ...
             yield return ...;
             ...
         }
     }
}

Это гарантирует, что дорогостоящая инициализация выполняется только один раз.Теперь вы можете безопасно использовать Any().

Возможны другие варианты оптимизации.Свойство Result может запоминать позицию первого возвращаемого элемента, поэтому, если он вызывается снова, он может сразу перейти к нему.

...