如果一次性为空,则默认可枚举



您有一个简单的扩展签名:

public static IEnumerable<T> DefaultEnumerableIfEmpty<T>(this IEnumerable<T> enumerable, IEnumerable<T> defaultEnumerable)

如果初始集合为null或为空,则返回defaultEnumerable。默认枚举值可以是null、empty或非空(换句话说,它可以是您想要的任何值)。问题是:如何实现它一次运行?这可能吗?

我想出了这个解决方案:

public static IEnumerable<T> DefaultEnumerableIfEmpty<T>(this IEnumerable<T> enumerable, IEnumerable<T> defaultEnumerable)
{
    if (enumerable != null)
    {
        var enumer = enumerable.GetEnumerator();
        if (enumer.MoveNext())
        {
            yield return enumer.Current;
            while (enumer.MoveNext())
            {
                yield return enumer.Current;
            }
            yield break;
        }
    }
    return defaultEnumerable;//of course this will fail to compile
}

当然,它将无法编译。所以,这是个问题。

您有以下两个关键需求:

  • 任何可枚举项都不应重复一次以上
  • 如果原始可枚举对象为null或为空,并且defaultEnumerablenull,则函数应返回null(而不是空可枚举对象)

同时满足这两个要求并保持完全懒惰是不可能的。为了证明这一点,让我们假设一个非空的enumerablenull的情况可以很容易地单独处理,没有问题)。

为了知道enumerable是否为空,我们需要查看它,即尝试获取第一个元素。由于这将开始枚举可枚举项,因此我们需要完成它,以避免多次枚举它。

由于这是需要执行的逻辑,所以底层枚举器需要存在(这样它就可以执行代码,并尝试查看原始枚举器)。为了使该枚举器存在,必须有一个实际的对象封装在其中:枚举器。因此,有了这个逻辑,我们就无法返回null

唯一可行的方法是尽早执行逻辑。一个简单的解决方案是先将整个可枚举项读取到内存中:

public static IEnumerable<T> DefaultEnumerableIfEmpty<T>(this IEnumerable<T> enumerable, IEnumerable<T> defaultEnumerable)
{
    if (enumerable == null)
        return defaultEnumerable;
    List<T> items = enumerable.ToList(); // enumerate it once
    if (items.Count == 0)
        return defaultEnumerable;
    return items;
}

您还可以检索枚举器以立即读取第一个元素并评估逻辑,然后从该枚举器中重构一个枚举对象:

public static IEnumerable<T> DefaultEnumerableIfEmpty<T>(this IEnumerable<T> enumerable, IEnumerable<T> defaultEnumerable)
{
    if (enumerable == null)
        return defaultEnumerable;
    var enumerator = enumerable.GetEnumerator();
    if (enumerator.MoveNext())
        return CombineBack(enumerator);
    return defaultEnumerable;
}
private static IEnumerable<T> CombineBack<T>(IEnumerator<T> enumerator)
{
    yield return enumerator.Current;
    while (enumerator.MoveNext())
        yield return enumerator.Current;
}

请注意,这实际上会立即开始迭代原始可枚举的。它并不像您期望的枚举和生成器函数那样完全懒惰。你只会在第一个元素之后得到懒惰。


如果你放松限制,你有两个选择。如果你接受偷看一次枚举,你可以使用Enumerable.Any():添加一个简单的检查

public static IEnumerable<T> DefaultEnumerableIfEmpty<T>(this IEnumerable<T> enumerable, IEnumerable<T> defaultEnumerable)
{
    if (enumerable == null || !enumerable.Any())
        return defaultEnumerable;
    return enumerable;
}

或者,如果您接受返回一个空的可枚举对象而不是null,则可以制作一个生成器,尝试对可枚举对象进行迭代并从中得出结果,如果原始可枚举对象中没有值,则返回到默认可枚举对象。这将始终返回一个非null枚举值,不过:

public static IEnumerable<T> DefaultEnumerableIfEmpty<T>(this IEnumerable<T> enumerable, IEnumerable<T> defaultEnumerable)
{
    bool didYield = false;
    if (enumerable != null)
    {
        foreach (var item in enumerable)
        {
            didYield = true;
            yield return item;
        }
    }
    if (!didYield && defaultEnumerable != null)
    {
        foreach (var item in defaultEnumerable)
            yield return item;
    }
}

最新更新