以 IEnumerable<T> 序列作为参数调用方法,如果该序列不为空

Calling method with IEnumerable<T> sequence as argument, if that sequence is not empty

我有方法 Foo,它执行一些 CPU 密集计算和 returns IEnumerable<T> 序列。我需要检查该序列是否为空。如果不是,则调用方法 Bar 并将该序列作为参数。

我考虑了三种方法...

条件

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 不是选项...

编辑: 经过一番考虑,我的案例的最佳答案来自 Olivier Jacot-Descombes - 完全避免这种情况。 Accepted solution 回答了这个问题——如果真的没有别的办法。

我不知道你在Foo中的算法是否允许在不进行计算的情况下确定枚举是否为空。但如果是这种情况,return 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到returnnull。因此需要第二种方法。

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

阅读您的评论后

Foo need to initialize some resources, parse XML file and fill some HashSets, which will be used to filter (yield) returned data.

我建议另一种方法。耗时的部分似乎是初始化。为了能够将它与迭代分开,创建一个 foo 计算器 class。类似于:

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 属性 可以记住第一个 returned 元素的位置,这样如果再次调用它,它可以立即跳到它。

如果您可以更改 Bar 那么当 IEnumerable<T> 为空时将其更改为 TryBar 那 returns false 怎么样?

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

如果这对您不起作用,您可以随时创建自己的 IEnumerable<T> 包装器,在值被迭代一次后缓存值。

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 修改为 return:

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

现在,您将检查它是否return为空。

当且仅当可枚举 source 包含至少一个元素时,您想调用某个函数 Bar<T>(IEnumerable<T> source),但您 运行 遇到两个问题:

  • IEnumerable<T> 中没有方法 T Peek(),因此您需要实际开始计算可枚举的值以查看它是否为非空,但是...

  • 您甚至不想对可枚举的部分进行双重计算,因为设置可枚举的成本可能很高。

在那种情况下,您的方法看起来很合理。但是,您的实现确实存在一些问题:

  1. 使用后需要disposeenumerator

  2. 正如 Ivan Stoev in 所指出的,如果 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();
        }
    }
}

带有单元测试的演示 fiddle here

接受的答案可能是最好的方法,但是,基于,我引用:

Convert sequence to list, check if that list it empty... and pass it to Bar. This have also limitation. Bar will need only first x items, so Foo will be doing unnecessary work...

另一种方法是创建一个 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();
    }
}