使用Enumerable.Empty<T>()初始化IEnumerable<T>是否比使用new List<T>()更好?

200
假设您有一个名为Person的类:
public class Person
{
   public string Name { get; set;}
   public IEnumerable<Role> Roles {get; set;}
}

我显然应该在构造函数中实例化角色。 现在,我过去常用一个类似于这样的列表来实现:

public Person()
{
   Roles = new List<Role>();
}

但我在System.Linq命名空间中发现了这个静态方法

IEnumerable<T> Enumerable.Empty<T>();

来自MSDN:

Empty(TResult)()方法缓存了一个类型为TResult的空序列。当枚举返回的对象时,它不会产生任何元素。

在某些情况下,此方法对于将空序列传递给接受IEnumerable(T)的用户定义方法非常有用。它还可用于生成方法(如Union)的中性元素。请参见示例部分,以了解其使用示例。

那么,像这样编写构造函数是否更好?您使用它吗?为什么?或者如果不是,为什么不是?

public Person()
{
   Roles = Enumerable.Empty<Role>();
}

1
这是一个数据类。我打算在使用Entity Framework 4.0实现存储库模式时将此类用作模型类(玩一下...)。所以我认为在这里有一个公共setter是可以的,不是吗? - Stéphane
1
Serbech,当其他代码可以安装任何类型的IEnumerable派生类列表时,您将如何在Person内添加角色?您会将其转换为什么? - H H
1
我明白你的意思,我可能应该在内部使用IList,甚至是这里的List? 那样就可以解决我的原始问题了... - Stéphane
我在单元测试中使用Enumerable.Empty<T>来表示不幸路径测试,以帮助传达意图。正如许多人已经指出的那样,好处是没有在GC上分配内存,这对于单元测试数量达到数百个或更多的情况非常有帮助。 - jjhayter
3
现在可以使用Array.Empty<Role>(),它明确地是一个数组,因此也是一个IList<T>。在Core中,实际上是同一个数组,只有类型不同。 - Jon Hanna
显示剩余2条评论
6个回答

256

我认为大部分帖子都错过了主要观点。即使你使用空数组或空列表,它们也是对象并且存储在内存中。垃圾收集器必须处理它们。如果你正在处理高吞吐量应用程序,这可能会有明显的影响。

Enumerable.Empty 每次调用不会创建一个新对象,因此对GC负载较小。

如果代码位于低吞吐量位置,则归结为审美考虑。


31
这也更好地反映了意图。 - Olivier Jacot-Descombes
8
如果Empty()不创建对象,它是如何工作的? - Mohammed Noureldin
28
@MohammedNoureldin 它返回单例。 - Vadym Chekan
6
Enumerable.Empty<> 不仅返回一个单例可枚举对象,该可枚举对象还会返回一个单例枚举器。 - Drew Noakes
5
@Lennart 我不这么认为。你提供的代码使用了 Array.Empty<TResult>(),它是一个函数而不是构造函数。这个函数返回 EmptyArray<T>.Value,而它又使用了 public static readonly T[] Value = new T[0];。因此使用的单例已经改变,但仍然是一个单例。 - Vadym Chekan
显示剩余2条评论

117

我认为Enumerable.Empty<T>更好,因为它更明确:你的代码清楚地表明了你的意图。这也可能会更有效率,但这只是一个次要的优势。


7
把你的意思表达得清晰明确,让人知道这是空的还是可能会被添加内容的,对你的代码是有好处的。 - Jay Bazuzi
32
是的, Enumerable<>.Empty 避免了分配新对象,这是一个小小的优点。 - Neil
13
旁注 @NeilWhitaker:应该使用 Enumerable.Empty<T> 而不是 Enumerable<T>.Empty(这让我有一次疯掉了……) - santa
1
同意。此外,如果您需要初始化一个 IEnumerable<T>,为什么要使用比您需要的更强大的初始化方式呢?List<T> 是一个更复杂的对象,它实现了 IEnumerable<T>,以及许多其他功能。我建议以最基本的方式初始化属性,并在这种情况下也使用 Enumerable.Empty<T>() - Craig

36

在性能方面,让我们看看如何实现 Enumerable.Empty<T>

它返回EmptyEnumerable<T>.Instance,定义如下:

internal class EmptyEnumerable<T>
{
    public static readonly T[] Instance = new T[0];
}

泛型类型的静态字段是按照每个泛型类型参数分配的。这意味着运行时可以仅为用户代码需要的类型延迟创建这些空数组,并且无需对垃圾回收器施加任何压力,就可以多次重复使用这些实例。

也就是说:

Debug.Assert(ReferenceEquals(Enumerable.Empty<int>(), Enumerable.Empty<int>()));

4
这段时间以来,我一直认为实现方式是像这样的 public static IEnumerable<T> Empty<T>() { yield break; }微软可能更懂,他们可能会使用已缓存的 new T[0] 更加高效。 - nawfal
已验证,.NET Core 2.2 中的实现相同(只是类名不同)。 - nawfal

13

假设您实际上想要以某种方式填充Roles属性,那么通过将其设置器设置为私有,并在构造函数中初始化为新列表来封装它:

public class Person
{
    public string Name { get; set; }
    public IList<Role> Roles { get; private set; }

    public Person()
    {
        Roles = new List<Role>();
    }
}

如果你真的真的想要公共的setter,将Roles保留为null并避免对象分配。


1
我建议使用 ICollection 而不是 IList - CervEd
你永远不应该让集合为空。这没有任何好处,只会导致代码中无意义的额外空值检查,或者更糟糕的是由于忘记这些检查而引发异常。使用 Enumerable.Empty<string>()Array.Empty<string>() 进行初始化同样可以避免对象分配。 - JvS

7
你的方法存在问题,因为你无法向集合中添加任何项 - 我会使用一个私有结构,例如列表,然后将项作为可枚举项暴露出来:
public class Person
{
    private IList<Role> _roles;

    public Person()
    {
        this._roles = new List<Role>();
    }

    public string Name { get; set; }

    public void AddRole(Role role)
    {
        //implementation
    }

    public IEnumerable<Role> Roles
    {
        get { return this._roles.AsEnumerable(); }
    }
}

如果你打算让其他类创建角色列表(我不建议这样做),那么在Person类中根本不需要初始化可枚举类型。

这是一个好观点。事实上,在我的实现中,我会很快发现这一点的 :) - Stéphane
我会将_roles设置为只读,而且我不认为需要调用AsEnumerable()。否则,+1。 - Kent Boogaart
1
@Kent - 我同意readonly。至于AsEnumerable,它并不是必需的,但它可以防止将Roles强制转换为IList并进行修改。 - Lee

7
将私有List作为IEnumerable公开的典型问题是,您类的客户端可以通过强制转换来修改它。以下代码将起作用:
  var p = new Person();
  List<Role> roles = p.Roles as List<Role>;
  roles.Add(Role.Admin);

你可以通过实现迭代器来避免这种情况:

public IEnumerable<Role> Roles {
  get {
    foreach (var role in mRoles)
      yield return role;
  }
}

7
如果开发者把你的 IEnumerable<T> 强制转换成 List<T>,那是他的问题而不是你的问题。如果你在将来更改了内部实现(使其不再是 List<T>),那么开发者的代码出现问题并不是你的责任。你无法阻止人们编写糟糕的代码和滥用你的 API,所以我认为这不值得花费太多的精力去解决。 - Tommy Carlier
5
这不是关于破解代码,而是关于利用代码。 - Hans Passant
2
如果这是一个安全问题,我同意@Hans的观点;在其他情况下,我支持@Tommy。不良开发人员总是会犯那个致命的错误。 - TrueWill
1
枚举部分可以使用AsEnumerable()扩展方法进行编写(类似于Lee的答案)。 - Stéphane
1
@Stephane 不行,因为AsEnumerable()只会再次给你返回List<T>,所以如果你希望保证它不能被强制转换回来,你将一无所获。 - Jon Hanna

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