有没有一种方法可以记忆或实现一个IEnumerable?

12
当给出一个 d 时,你可能正在处理类似于列表或数组的固定序列,将枚举某些外部数据源的AST,甚至是现有集合上的AST。是否有一种安全的方法来“实例化”可枚举对象,使得像 foreach、count 等枚举操作不会每次执行AST?
我经常使用 .ToArray() 来创建这种表示,但如果底层存储已经是列表或其他固定序列,则似乎是浪费复制。如果能这样做就好了:
var enumerable = someEnumerable.Materialize();

if(enumberable.Any() {
  foreach(var item in enumerable) {
    ...
  }
} else {
  ...
}

不必担心.Any()foreach尝试两次枚举序列,也不必将可枚举对象复制一遍。


1
这是一个不错的想法,但我要指出通常会执行existingCollection.ToList以防止对现有集合进行更改。 - Ani
1
.ToList() 的问题在于它会创建一个枚举器的列表,这些枚举器不是列表(数组、ICollections 等),并返回一个可变集合。 - Arne Claassen
3个回答

11

很简单:

public static IList<TSource> Materialize<TSource>(this IEnumerable<TSource> source)
{
    if (source is IList<TSource>)
    {
        // Already a list, use it as is
        return (IList<TSource>)source;
    }
    else
    {
        // Not a list, materialize it to a list
        return source.ToList();
    }
}

4
这是一个好方法。我认为最好返回IEnumerable<T>,并且还要检查ICollectionICollection<T> - Ani
6
这与Linq.ToList()的实现略有不同,后者似乎总是返回一个新副本,因此对结果的更改不会影响原始内容。而Materialize方法根据输入的类型有时会返回副本,有时会返回原始内容 - 因此对结果进行更改有时会影响原始内容。 - Handcraftsman
1
其实,我不确定ToArray是否比ToList更便宜...它们大多数情况下都做相同的工作,但ToArray必须修剪多余的大小,而ToList则不需要(因为它单独跟踪容量和计数)。 - Thomas Levesque
1
是的,ToList() 总是返回一个新的 List<T> 实例。在 Reflector 中查看代码:它只是将源序列传递给 List<T> 的构造函数。 - Thomas Levesque
3
一句话概括,就是将源(source)转换为 IList<TSource> 类型,如果无法转换,则将其转换为 List<TSource> 类型。具体代码为:return source as IList<TSource> ?? source.ToList(); - nawfal
显示剩余4条评论

11

原始答案:

和Thomas的答案一样,只是我认为更好一些:

public static ICollection<T> Materialize<T>(this IEnumerable<T> source)
{
    // Null check...
    return source as ICollection<T> ?? source.ToList();
}

请注意,如果它是一个有效的集合类型,它往往会返回现有的集合本身,否则会生成一个新的集合。虽然这两者略有不同,但我认为这不会成为问题。

编辑:

今天这是一个更好的解决方案:

public static IReadOnlyCollection<T> Materialize<T>(this IEnumerable<T> source)
{
    // Null check...
    switch (source)
    {
        case IReadOnlyCollection<T> readOnlyCollection:
            return readOnlyCollection;

        case ICollection<T> collection:
            return new ReadOnlyCollectionAdapter<T>(collection);

        default:
            return source.ToList();
    }
}

public class ReadOnlyCollectionAdapter<T> : IReadOnlyCollection<T>
{
    readonly ICollection<T> m_source;

    public ReadOnlyCollectionAdapter(ICollection<T> source) => m_source = source;

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public int Count => m_source.Count;

    public IEnumerator<T> GetEnumerator() => m_source.GetEnumerator();
}

请注意,上述解决方案忽略了一种特定的协变情况,即集合类型实现了 ICollection<T> 但未实现 IReadOnlyCollection<T>。例如,请考虑以下集合:
class Collection<T> : ICollection<T>
{
}

// and then
IEnumerable<object> items = new Collection<Random>();

由于IEnumerable<T>是协变的,因此上述代码可以编译通过。

// later at some point if you do
IReadOnlyCollection<object> materialized = items.Materialize();

上述代码创建了一个new List<Random>(O(N)),即使我们传递了一个已经实例化的集合。原因是ICollection<T>不是协变接口(它不能是),因此我们从Collection<Random>ICollection<object>的转换失败,因此在switch语句中执行default:情况。

我认为,实现ICollection<T>但不实现IReadOnlyCollection<T>的集合类型是极为罕见的情况。我会忽略这种情况。扫描BCL库,我只能找到很少的几个,而且也很少听说过。如果您确实需要涵盖这种情况,可以使用一些反射。例如:

public static IReadOnlyCollection<T> Materialize<T>(this IEnumerable<T> source)
{
    // Null check...

    if (source is IReadOnlyCollection<T> readOnlyCollection)
        return readOnlyCollection;

    if (source is ICollection<T> collection)
        return new ReadOnlyCollectionAdapter<T>(collection);
    
    // Use your type checking logic here.
    if (source.GetType() (is some kind of typeof(ICollection<>))
        return new EnumerableAdapter<T>(source);

    return source.ToList();
}

public class EnumerableAdapter<T> : IReadOnlyCollection<T>
{
    readonly IEnumerable<T> m_source;

    public EnumerableAdapter(IEnumerable<T> source) => m_source = source;

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public int Count => ((dynamic)m_source).Count;

    public IEnumerator<T> GetEnumerator() => m_source.GetEnumerator();
}

3
请查看我几年前写的这篇博客文章:http://www.fallingcanbedeadly.com/posts/crazy-extention-methods-tolazylist。在这篇文章中,我定义了一个名为ToLazyList的方法,它能够有效地完成你所需的功能。但需要注意的是,这个方法会最终复制整个输入序列,但你可以对其进行调整,以避免将IList实例包装在LazyList中,从而防止发生这种情况(不过这样做的前提是任何你得到的IList都已经被有效地记忆化)。

2
这是一个非常有趣的扩展,但我认为它与 OP 想要的不相关。那会根据需要推迟序列的实例化,而 OP 希望以高效的方式急切地实例化序列;如果需要,获取对现有集合的引用。 - Ani

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接