C# 参数的最佳实践:IEnumerable与IList与IReadOnlyCollection
当延迟执行中有值时,我会从方法中返回C# 参数的最佳实践:IEnumerable与IList与IReadOnlyCollection,c#,collections,parameters,C#,Collections,Parameters,当延迟执行中有值时,我会从方法中返回IEnumerable。返回列表或IList应该只在修改结果时才返回,否则我会返回IReadOnlyCollection,这样调用者就知道他得到的不是要修改的(这让该方法甚至可以重用来自其他调用者的对象) 然而,在参数输入端,我不太清楚。我可以取一个IEnumerable,但是如果我需要多次枚举呢 俗话说“发送的东西要保守,接受的东西要自由”,这意味着使用IEnumerable是好的,但我不是很确定 例如,如果以下IEnumerable参数中没有元素,则此方法
IEnumerable
。返回列表
或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));
}
现在,如果您这样做:
var myData = GetVeryExpensiveDataForDate(todayDate);
var unhandledData = RemoveHandledForDate(myData, todayDate);
foreach (var data in unhandledData) {
messageBus.Dispatch(data); // fully enumerate
)
如果RemovedHandledForDate
执行了任何操作,并且执行了Where
,则您将承担两次5秒的成本,而不是一次。这就是为什么您应该尽最大努力避免多次枚举IEnumerable
。不要依赖于你的知识,事实上它是无害的,因为将来某个倒霉的开发人员可能会用一个你从未想到的新实现的IEnumerable
调用你的方法,它具有不同的特性
IEnumerable
的合同规定可以枚举它。它并没有承诺多次这样做的性能特征
事实上,一些IEnumerables
是易变的,在后续枚举时不会返回任何数据!如果与多个枚举相结合,切换到一个将是一个完全破坏性的更改(如果后来添加了多个枚举,则很难诊断)
不要对IEnumerable执行多重枚举
如果您接受IEnumerable参数,实际上,您承诺将它精确地枚举0或1次。有一些方法可以让您接受IEnumerable,只枚举一次,并确保不多次查询数据库。我能想到的解决办法是:
- 您可以直接使用枚举器,而不是使用
Any
和Where
。调用MoveNext
而不是Any
,查看集合中是否有任何项,并在进行数据库查询后在中进一步手动迭代李>
- 使用
Lazy
初始化HashSet
第一个看起来很难看,第二个可能很有意义:
public IEnumerable<Data> RemoveHandledForDate(IEnumerable<Data> data, DateTime dateTime)
{
var ids = new Lazy<HashSet<int>>(
() => new HashSet<int>(
GetHandledDataForDate(dateTime) // Expensive database operation
.Select(d => d.DataId)
));
return data.Where(d => !ids.Value.Contains(d.DataId));
}
public IEnumerable RemoveHandledForDate(IEnumerable数据,日期时间日期时间)
{
var id=新的惰性(
()=>新哈希集(
GetHandledDataForDate(dateTime)//昂贵的数据库操作
.Select(d=>d.DataId)
));
返回数据,其中(d=>!ids.Value.Contains(d.DataId));
}
您可以在方法中使用一个IEnumerable
,并使用类似的CachedEnumerable来包装它
此类包装了一个IEnumerable
,并确保它只被枚举一次。如果您再次尝试枚举它,它将从缓存中生成项
请注意,这样的包装器不会立即从包装的枚举中读取所有项。当您从包装器中枚举单个项时,它仅从包装的可枚举项中枚举单个项,并在过程中缓存单个项
这意味着,如果在包装器上调用Any
,则只会从包装的可枚举项中枚举单个项,然后将缓存此类项
如果随后再次使用该枚举数,它将首先从缓存中生成第一个项,然后继续从其保留的位置枚举原始枚举数
您可以这样做来使用它:
public IEnumerable<Data> RemoveHandledForDate(IEnumerable<Data> data, DateTime dateTime)
{
var dataWrapper = new CachedEnumerable(data);
...
}
public IEnumerable RemoveHandledForDate(IEnumerable数据,日期时间日期时间)
{
var dataWrapper=新的CachedEnumerable(数据);
...
}
请注意,这里的方法本身正在包装参数数据
。这样,您就不会强迫您的方法的使用者做任何事情。我不认为仅仅通过更改输入类型就可以解决这个问题。如果您想允许比List
或IList
更通用的结构,那么您必须决定是否/如何处理这些可能的边缘情况
要么计划最坏的情况,花一点时间/内存创建一个具体的数据结构,要么计划最好的情况,冒偶尔执行两次查询的风险
您可以考虑记录该方法多次枚举集合,以便调用方可以决定是否要传递一个“昂贵”的查询,或者在调用方法之前对查询进行水合物。p> 我认为
IEnumerable
是参数类型的一个好选择。它是一种简单、通用且易于提供的结构。IEnumerable
契约本身并不意味着应该只迭代一次
一般来说,测试.Any()
的性能成本可能不高,但当然不能保证如此。在您描述的情况下,很明显,迭代第一个元素会有相当大的开销,但这绝不是通用的
将参数类型更改为IReadOnlyCollection
或IReadOnlyList
是一个选项,但可能只有在提供了部分或全部属性/方法的情况下才是一个好选项
public IEnumerable<Data> RemoveHandledForDate(IEnumerable<Data> data, DateTime dateTime)
{
var dataWrapper = new CachedEnumerable(data);
...
}
param = param as IList<SomeType> ?? param.ToList();
param = param as IReadOnlyCollection<SomeType> ?? param.ToList();