参数的最佳实践:IEnumerable与IList对比IReadOnlyCollection
当得到延迟执行中的值时,我会从方法返回 IEnumerable
时得到。 返回一个List
或IList
应该只是在修改结果的时候,否则我会返回一个IReadOnlyCollection
,所以调用者知道他得到的不是用于修改的(这使得该方法甚至可以重用对象)来自其他来电者)。
但是,在参数输入方面,我有点不太清楚。 我可以采用IEnumerable
,但如果我需要多次枚举怎么办?
俗话说“ 你发送的东西要保守,你接受的东西要自由 ”,建议拿一个IEnumerable
是好的,但我不太确定。
例如,如果以下IEnumerable
参数中没有元素,则可以通过首先检查.Any()
来保存此方法中的大量工作,这需要在此之前使用ToList()
以避免枚举两次 。
public IEnumerable RemoveHandledForDate(IEnumerable data, DateTime dateTime) { var dataList = data.ToList(); if (!dataList.Any()) { return dataList; } var handledDataIds = new HashSet( GetHandledDataForDate(dateTime) // Expensive database operation .Select(d => d.DataId) ); return dataList.Where(d => !handledDataIds.Contains(d.DataId)); }
所以我想知道什么是最好的签名,在这里? 一种可能性是IList data
,但接受列表表明您计划修改它,这是不正确的 – 此方法不会触及原始列表,因此IReadOnlyCollection
似乎更好。
但是IReadOnlyCollection
强制调用者每次执行ToList().AsReadOnly()
都会变得有点难看,即使使用自定义扩展方法.AsReadOnlyCollection
。 在接受的东西中,这并不是自由主义者。
在这种情况下,最佳做法是什么?
此方法不返回IReadOnlyCollection
因为最终使用延迟执行可能有值,因为不需要枚举整个列表。 但是,需要枚举Select
,因为没有HashSet
,执行.Contains
的成本会很糟糕。
我没有调用ToList
的问题,我刚想到如果我需要一个List
以避免多次枚举,为什么我不只是在参数中要求一个? 所以这里的问题是,如果我不想在我的方法中使用IEnumerable
,我是否真的应该接受一个以便自由(并自己ToList
),或者我应该把调用者的负担加到ToList().AsReadOnly()
?
有关IEnumerables不熟悉的人的更多信息
这里真正的问题不是Any()
与ToList()
的成本。 我知道枚举整个列表的成本比执行Any()
。 但是,假设调用者将使用上述方法返回IEnumerable
中的所有项,并假设源IEnumerable data
参数来自此方法的结果:
public IEnumerable 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次。
有一些方法可以让你接受IEnumerable
,只枚举一次并确保你不多次查询数据库。 我能想到的解决方案:
- 而不是使用
Any
和Where
可以直接使用枚举器。 调用MoveNext
而不是Any
来查看集合中是否有任何项目,并在进行数据库查询后手动迭代。 - 使用
Lazy
初始化您的HashSet
。
第一个似乎很难看,第二个可能实际上很有意义:
public IEnumerable RemoveHandledForDate(IEnumerable data, DateTime dateTime) { var ids = new Lazy>( () => new HashSet( GetHandledDataForDate(dateTime) // Expensive database operation .Select(d => d.DataId) )); return data.Where(d => !ids.Value.Contains(d.DataId)); }
您可以在方法中使用IEnumerable
,并使用类似于此处的CachedEnumerable来包装它。
此类包装IEnumerable
并确保它只枚举一次。 如果您尝试再次枚举它,它会从缓存中生成项目。
请注意,此类包装器不会立即从包装的可枚举中读取所有项目。 当您从包装器枚举单个项目时,它仅枚举包装的可枚举中的各个项目,并且它会沿途缓存各个项目。
这意味着如果在包装器上调用Any
,则只会从包装的枚举中枚举单个项目,然后将缓存此类项目。
如果再次使用枚举,它将首先从缓存中生成第一个项目,然后继续枚举它离开的原始枚举器。
你可以做这样的事情来使用它:
public IEnumerable RemoveHandledForDate(IEnumerable data, DateTime dateTime) { var dataWrapper = new CachedEnumerable(data); ... }
请注意,方法本身正在包装参数data
。 这样,您不会强制您的方法的使用者做任何事情。
IReadOnlyCollection
向IEnumerable
IReadOnlyCollection
添加一个Count
属性和相应的承诺,即没有延迟执行 。 如果参数是您要解决此问题的位置,那么它将是要求的适当参数。
但是,我建议请求IEnumerable
,并在实现本身中调用ToList()
。
观察:两种方法的缺点是多重枚举可能在某些时候被重构,使参数更改或ToList()
调用冗余,我们可能会忽略。 我不认为这是可以避免的。
这个案例的确代表在方法体中调用ToList()
:由于多个枚举是一个实现细节,避免它应该也是一个实现细节。 这样,我们就可以避免影响API了。 如果多次枚举被重构,我们也会避免更改API。 我们还避免通过一系列方法传播需求,否则可能都会决定要求IReadOnlyCollection
,这只是因为我们的多次枚举。
如果您担心创建额外列表的开销(当输出已经是列表时),Resharper建议采用以下方法:
param = param as IList ?? param.ToList();
当然,我们可以做得更好,因为我们只需要防止延迟执行 – 不需要一个成熟的IList
:
param = param as IReadOnlyCollection ?? param.ToList();
我不认为只需改变输入类型就可以解决这个问题。 如果你想允许比List
或IList
更多的通用结构,那么你必须决定是否/如何处理这些可能的边缘情况。
要么计划最坏的情况,花一点时间/内存创建一个具体的数据结构,要么计划最好的情况,并冒险偶尔查询执行两次。
您可以考虑记录该方法多次枚举该集合,以便调用者可以决定是否要传递“昂贵”查询,或者在调用该方法之前水合查询。
我认为IEnumerable
是参数类型的一个很好的选择。 它是一种简单,通用且易于提供的结构。 IEnumerable
合同没有任何内在的含义,暗示一个人只应该迭代一次。
一般来说,测试.Any()
的性能成本可能不高,但当然不能保证这样。 在您描述的情况下,显然可能是迭代第一个元素具有相当大的开销,但这绝不是普遍的。
将参数类型更改为类似IReadOnlyCollection
或IReadOnlyList
的选项是一个选项,但在需要该接口提供的部分或全部属性/方法的情况下,这可能只是一个很好的选项。
如果您不需要该function,而是希望保证您的方法只迭代IEnumerable
一次,您可以通过调用.ToList()
或将其转换为其他适当类型的集合来实现,但这是一个实现细节方法本身。 如果您正在设计的合同需要“可以迭代的东西”,那么IEnumerable
是一个非常合适的选择。
您的方法有权保证迭代任何集合的次数,您不需要将该细节暴露在方法的边界之外。
相反,如果您确实选择在方法中重复枚举IEnumerable
那么您还必须考虑可能是该选择的结果的每个可能性,例如由于延迟执行可能在不同情况下获得不同的结果。
也就是说,作为最佳实践的一点,我认为尽可能避免在您自己的代码返回的IEnumerables
任何副作用是有意义的 – 像Haskell这样的语言可以安全地使用惰性评估,因为它们去了努力避免副作用。 如果不出意外,那些使用你的代码的人在防止多次枚举时可能不会像你那样愚蠢。