linq 有没有一种方法来记忆或具体化一个IEnumerable?

p8ekf7hl  于 2023-03-27  发布在  其他
关注(0)|答案(3)|浏览(194)

当给定一个d时,你可以处理一个固定的序列,比如一个列表或数组,一个将枚举一些外部数据源的AST,甚至是一个现有集合上的AST。有没有一种方法可以安全地“具体化”可枚举对象,这样foreach,count等枚举操作就不会每次都执行AST?
我经常使用.ToArray()来创建这种表示,但如果底层存储已经是一个列表或其他固定序列,那似乎是浪费复制。

var enumerable = someEnumerable.Materialize();

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

不必担心.Any()foreach尝试枚举序列两次,并且不会不必要地复制可枚举对象。

6ioyuze2

6ioyuze21#

很简单:

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

5ssjco0h2#

原始答案:

和托马斯的答案一样,只是我觉得更好一点:

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: case。
我相信集合类型实现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();
}
gjmwrych

gjmwrych3#

看看我几年前写的这篇博客:http://www.fallingcanbedeadly.com/posts/crazy-extention-methods-tolazylist
在其中,我定义了一个名为ToLazyList的方法,它可以有效地完成您所需要的任务。
正如所写的那样,它最终将生成输入序列的完整副本,尽管您可以对其进行调整,以便IList的示例不会被 Package 在LazyList中,这将防止这种情况发生(但是,此操作将假设您获得的任何IList都已经有效地记忆)。

相关问题