我无法确定返回IEnumerable<T>的方法本身是否应该是惰性的,还是应该构建一个列表并返回该列表。我经常选择后者,因此可以确保每个值的枚举不会多次执行。
例如:

public IEnumerable<UserProfile> GetUsers()
{
    var allDepartments = GetAllDepartments(active: true); // Returns IEnumerable<Department>
    var allUsers = GetAllUserDepartments(active: true); // Returns IEnumerable<User>
    var users
        = allDepartments
            .Join(allUsers, x => x.Department, y => y.Department, (x, y) => y, StringComparer.OrdinalIgnoreCase)
            .Distinct()
            .Select(x => GetUserProfile(x))
            .ToList(); // Is this recommended or not

    return users;
}


关键部分每个枚举是否都在做一些琐碎的事情(GetUserProfile),可能会很昂贵,却可能不会,但是对于返回IEnumerable<T>的方法有何建议?

假设调用方只要求IEnumerable<T>功能,是否可以在乎调用方是否可以枚举几次?我的问题可以改写为:

如何做我向调用者表示每个枚举可能会很昂贵?

如果多次枚举全部内容,则更改其执行决策的代价可能是多少?枚举全部,那么ToList()对我(或呼叫者)执行来说会很浪费吗?

评论

如果查看返回类型,您会期望什么?就个人而言,如果我看到IEnumerable,我希望得到一个惰性的评估集合。如果调用ToList(),只需返回一个IList或ICollection。不要试图在方法中处理太多。请说清楚。

@dusky这很有意义,但是请记住IList和ICollection都是可变合同。 IEnumerable具有双重作用,既具有惰性求值语义,又是对用户不可变的结构。

@DanPantry .net 4.5 List实现IReadOnlyList和IReadOnlyCollection。但是,如果您想要一个适当的只读集合,请返回一个适当的不可变列表。

@dusky谢谢,我想可能是IReadOnlyList是我应该在我急切的方法(调用ToList())中返回的内容。我尽量避免使用IList,因为最终我对之后修改结果感到不舒服。 IEnumerable对于惰性方法非常有意义。

取决于“好”的标准。另请参见不可变列表msdn.microsoft.com/zh-cn/library/dn467185(v=vs.111).aspx

#1 楼

调用该方法的代码是否总是期望List功能(通过索引等进行访问)?返回List。调用该方法的代码是否只希望对其进行迭代?返回一个IEnumerable

您不必在乎调用方如何处理它,因为返回类型清楚地说明了返回值可以执行的操作。任何获得IEnumerable结果的调用者都知道,如果要对结果进行索引访问,则必须转换为List,因为IEnumerable simple在枚举并放入索引结构之前无法使用。不要以为调用者是愚蠢的,否则您最终将无法使用它们。例如,通过返回一个List,您已经取消了流传输结果的功能,而该结果可能具有其自身的性能优势。您的实现可能会发生变化,但是如果需要,调用者可以始终将IEnumerable转换为List。采用。延迟执行虽然有好处,但也可能造成混淆。举个例子:

public static IEnumerable<int> GetRecordIds()
{
    return dbContext.Records.Select(r => r.Id);
}

IEnumerable<int> currentRecordIds = GetRecordIds();

dbContext.Add(new Record { Id = 7 });

// Includes "7", despite GetRecordIds() being called before the Add():
currentRecordIds.Dump();


返回之前,可以通过GetRecordIds调用ToList来“纠正”。这里的“正确”用法仅取决于类的期望值(“实时”结果或调用结果的时间)。通过对使用方式的假设来从调用者那里夺走功能。记住,该界面告诉您期望返回什么。 IEnumerable仅表示您正在获取可以迭代的内容(可能会流传输结果并利用延迟执行),List仅表示您正在获取一个内存中集合,该集合可以添加到索引,从索引中删除,等。

编辑-解决“更新1”问题:

如果您真的很担心,可以在文档中添加一些信息,但是我认为没有必要。 IEnumerable接口不会声称对重复枚举(甚至是每次迭代)的性能做出任何保证,因此,调用方可以智能地处理它。

评论


\ $ \ begingroup \ $
您的观点很有趣。我只是为无法枚举的执行而烦恼。但是,这仍然是一个很好的答案。
\ $ \ endgroup \ $
–IEatBagels
14年10月15日在16:03

\ $ \ begingroup \ $
感谢您的回答,但我有点困惑,您问的是调用它的代码,然后说我不在乎调用者的操作。
\ $ \ endgroup \ $
–user2000095-tim
2014-10-16 14:38

\ $ \ begingroup \ $
第一行的意思是:您试图向调用者公开的API是否希望暗示结果可用作可修改的索引集?
\ $ \ endgroup \ $
–Ocelot20
14-10-17在14:44

\ $ \ begingroup \ $
我也不是说“不在乎呼叫者做什么”。我说的是“不要通过强迫他们使用List来假设您比调用者更了解(除非该方法的预期使用始终需要索引访问,等等)。
\ $ \ endgroup \ $
–Ocelot20
14-10-17在15:07

\ $ \ begingroup \ $
@ Ocelot20好,我认为我的Update 1阐明了我要实现的目标。它更有意义吗?
\ $ \ endgroup \ $
–user2000095-tim
14-10-17在16:37

#2 楼

我认为这里的真正问题是可以懒洋洋地评估返回值吗?基本上,这就是结果。

IEnumerable的评估是懒惰的,因为您对其进行的任何操作(通过Linq或任何明智的作者)都将延迟到最后一刻,即直到尝试通过迭代或使用折叠(IEnumerableFirst())观察FirstOrDefault()。在大多数情况下,这是一件好事,因为这意味着您可以完全避免做任何工作,直到需要时为止-有时这意味着实际上不会发生任何潜在的昂贵操作。非常棒!您的例子很完美-数据库。由数据库支持的IEnumerable不应被懒惰地评估,因为它所绑定的连接可能会在枚举时被关闭。将连接的生命周期绑定到IEnumerable-换句话说,您可以教IEnumerable如何实例化连接/其他资源,如何使用它,然后对其进行处置。在某些应用程序中,由于性能影响,该解决方案可能不可行,并且您可能会对此失去控制感到不安(这是IoC派上用场的地方)。

有一个名为Rx的框架.NET的(自适应扩展)实际上具有一个内置的功能,称为Observable.Using()。一个observable是可以观察的一类,并且被懒惰地评估-非常类似于IEnumerable。实际上,IObservable实现了IEnumerable。通过使用Observable.Using(),您可以通过工厂方法将对象的创建绑定到IObservable的生存期。首次观察到IObservable时,将创建资源。当观察到IObservable完成时,资源为Dispose() d。整洁。

回到问题的症结;在这种情况下,我会说是,然后再将控件返回给调用者。否则,您将遇到并发问题,并且无法保证当您将控制权返回给调用方时,该连接仍然有效。但是请确保仍返回ToList()-IEnumerable具有双重用途,因为它非常擅长表示只读集合。

评论


\ $ \ begingroup \ $
“您会遇到并发问题”有点苛刻。有很多完全可以接受的原因,可以从数据库中返回延迟评估的IEnumerable。一切归结为每个阶级声称负责的事情。另外,“您不能保证连接会一直存在”是类返回IEnumerable而不是调用方的问题。
\ $ \ endgroup \ $
–Ocelot20
14-10-15在17:21

\ $ \ begingroup \ $
我假设OP正在使用EntityFramework。如果他是,那么如果他不使用ToList()这个可枚举对象,那么他将遇到并发问题,而我只是出于相反的考虑而生。我确实指出了,但是,如果可以将资源的生存期绑定到IEnumerable,则可以避免这种情况。
\ $ \ endgroup \ $
–丹
14-10-15在17:22

\ $ \ begingroup \ $
要明确,这并不意味着苛刻。但是我要说的是,如果一个懒惰的评估结构直接来自数据库层并离开数据库层,那么如果它直接与数据库连接通信,那将是一件坏事。正如我提到的,当然在Ef中,这样做会遇到很多问题。
\ $ \ endgroup \ $
–丹
2014年10月15日17:24

\ $ \ begingroup \ $
“苛刻”对我而言是一个糟糕的选择。我想我的意思是“过于笼统的陈述”。他是否会遇到并发问题,在很大程度上取决于他对结果的公开/处理方式。就像我之前提到的,有很多可以接受的原因可以从数据库中返回延迟评估的IEnumerable,但是如果没有看到更多代码,我们就无法真正说出他会遇到什么问题。当然,这无疑是一个潜在的问题。
\ $ \ endgroup \ $
–Ocelot20
14-10-15在17:47

\ $ \ begingroup \ $
@DanPantry感谢您的回答,没有数据库,已缓存用户和部门,并且UserProfile可能驻留在缓存中,可能驻留在DB中,可能驻留在表存储或磁盘或其他地方。我没有使用IObservable ,与针对IEnumerable 使用LINQ扩展方法相比,它是否使代码复杂化。听起来确实像是值得研究的。您对如何实施有任何想法吗?
\ $ \ endgroup \ $
–user2000095-tim
2014年10月16日14:51

#3 楼

如果要在注释中注明返回类型,则不应该使用var;如果不清楚该属性是否包含注释,请使用实型而不是var,这样更易​​读。 >要回答您的问题,我想是的,您应该在返回枚举之前致电ToList,主要是因为您的IEnumerable<UserProfile>可能是EF查询,并且您的班级用户无法知道这一点,这意味着该人可以多次使用此枚举并在不知道的情况下启动大量的SQL查询(直到出现性能问题并且诊断将导致IEnumerable启动查询)。

使用ToList的性能成本与使用new List<>([your enumerable])构造函数的性能成本相同,这是一项O(n)操作,因为您需要复制整个数组。如果您有一个很大的枚举,不想一直复制,那么也许应该考虑在方法中添加一个参数,该参数可以让您决定是否以这种方式执行IEnumerable: />
public IEnumerable<UserProfile> GetUsers(bool deferExecution)
{
    IEnumerable<Department> allDepartments = GetAllDepartments(active: true);
    IEnumerable<User> allUsers = GetAllUserDepartments(active: true);
    IEnumerable<UserProfile> users
        = allDepartments
            .Join(allUsers, x => x.Department, y => y.Department, (x, y) => y, StringComparer.OrdinalIgnoreCase)
            .Distinct()
            .Select(x => GetUserProfile(x));

    if(!deferExecution) 
    {
        users = users.ToList();
    }

    return users;
}


虽然说实话,但我从未在现实生活中尝试过使用布尔值,因此我无法确定这是否是一个好主意,但我觉得这还不错!

如果您想要有关ToList的性能成本的更多信息,请查看此帖子。

评论


\ $ \ begingroup \ $
带有deferExecution参数的有趣想法。
\ $ \ endgroup \ $
–xDaevax
2014年10月15日12:53

\ $ \ begingroup \ $
这不是IEnumerable的工作方式。将IQueryable作为IEnumerable或List返回将与WRT执行多少次查询没有什么不同。该方法被调用的次数以及消费者是否对结果进行迭代将是确定因素。
\ $ \ endgroup \ $
–Ocelot20
2014年10月15日14:03



\ $ \ begingroup \ $
WRT是什么意思?
\ $ \ endgroup \ $
–IEatBagels
2014年10月15日15:59



\ $ \ begingroup \ $
我假设“关于”
\ $ \ endgroup \ $
–丹
2014年10月15日下午16:58

\ $ \ begingroup \ $
是的,我的意思是“关于”。
\ $ \ endgroup \ $
–Ocelot20
14-10-15在17:03