当有值需要延迟执行时,从方法中返回一个 IEnumerable
是合适的。如果结果不会被修改,返回一个 List
或者 IList
应该只是为了当结果需要被修改时使用。否则,我会返回一个 IReadOnlyCollection
,这样调用方就知道他得到的结果不能被修改(并且这使得方法可以重复使用其他调用方的对象)。
然而,在参数输入方面,我的理解还不太清楚。我可以接受一个 IEnumerable
,但是如果我需要枚举多次怎么办呢?
“在发送时要保守,在接收时要开明”这句话建议使用 IEnumerable
是好的,但我不太确定。
例如,如果以下 IEnumerable
参数没有任何元素,则可以通过先检查 .Any()
来节省大量工作,而这需要在之前使用 ToList()
来避免枚举两次。
public IEnumerable<Data> RemoveHandledForDate(IEnumerable<Data> data, DateTime dateTime) {
var dataList = data.ToList();
if (!dataList.Any()) {
return dataList;
}
var handledDataIds = new HashSet<int>(
GetHandledDataForDate(dateTime) // Expensive database operation
.Select(d => d.DataId)
);
return dataList.Where(d => !handledDataIds.Contains(d.DataId));
}
我在想这里应该使用什么样的参数签名才是最好的。一种可能的方案是IList<Data> data
,但是接受一个列表意味着你计划对其进行修改,而这是不正确的——这个方法并没有改动原始列表,因此IReadOnlyCollection<Data>
似乎更好。
但是,IReadOnlyCollection
会强制调用者每次都要执行ToList().AsReadOnly()
,这有点丑陋,即使使用自定义扩展方法.AsReadOnlyCollection
也是如此。而且这并不是对可接受类型放宽要求。
在这种情况下,最佳实践是什么?
该方法不返回IReadOnlyCollection
,因为在最终的Where
中可能存在使用延迟执行,不需要枚举整个列表。然而,必须枚举Select
,因为如果没有HashSet
,执行.Contains
的代价将非常高。
我不认为调用ToList
会有问题,只是突然想到,如果我需要一个List
来避免多次枚举,那么为什么不直接在参数中请求一个呢?所以这里的问题是,如果我不想在我的方法中使用IEnumerable
,那么我是否应该接受一个IEnumerable
以保持灵活性(并自行执行ToList
),还是应该让调用者承担ToList().AsReadOnly()
的负担?
对于不熟悉IEnumerables的人的进一步信息
真正的问题不在于Any()
和ToList()
的成本。我知道枚举整个列表的成本比执行Any()
更高。然而,假设调用者将消耗上述方法返回的所有IEnumerable
项,并且假设源IEnumerable<Data> data
参数来自此方法的结果:
public IEnumerable<Data> GetVeryExpensiveDataForDate(DateTime dateTime) {
// This query is very expensive no matter how many rows are returned.
// It costs 5 seconds on each `.GetEnumerator` call to get 1 value or 1000
return MyDataProvider.Where(d => d.DataDate == dateTime);
}
现在,如果你这样做:
var myData = GetVeryExpensiveDataForDate(todayDate);
var unhandledData = RemoveHandledForDate(myData, todayDate);
foreach (var data in unhandledData) {
messageBus.Dispatch(data); // fully enumerate
)
如果RemovedHandledForDate
使用了Any
和Where
,那么你会承担两次5秒的成本,而不是一次。这就是为什么你应该尽力避免对一个IEnumerable
进行多次枚举。不要依赖于你知道实际上它是无害的这一点,因为将来可能会有不幸的开发人员在某一天调用你的方法并传入一个你从未考虑过的、具有不同特性的新实现IEnumerable
。
IEnumerable
的合约规定可以枚举它,但它没有保证重复枚举的性能特征。事实上,一些
IEnumerables
是易变的,并且在后续枚举时不会返回任何数据!如果与多次枚举组合使用,则切换到其中一个将完全破坏代码(如果稍后添加了多次枚举,则非常难以诊断)。不要对
IEnumerable
进行多次枚举。
如果你接受一个IEnumerable参数,实际上是在承诺对其进行0或1次枚举。
ToList()
或Any()
。在回答之前,请努力理解IEnumerables
。成本在于具有大量启动惩罚的IEnumerable
(例如查询数据库)。如果您开始两次枚举,则可能会调用两次数据库!你在这里走错了方向。 - ErikEToList
没有问题,但随后我想到,如果我需要一个列表,为什么不在参数中直接请求一个呢?所以这里的问题是,如果我不想在我的方法中使用IEnumerable
,那么我是否应该接受它以保持自由并自己进行ToList
转换呢? - ErikEToList()
然后再使用两个LINQ方法会比执行两次查询更快。这就是问题的要点。 - D Stanley