参数的最佳实践:IEnumerable与IList对比IReadOnlyCollection

Eri*_*ikE 12 c# collections parameters

当得到延迟执行的值时,我会从一个方法返回一个IEnumerable.并且返回a 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));
}
Run Code Online (Sandbox Code Playgroud)

所以我想知道什么是最好的签名,在这里?一种可能性是IList<Data> data,但接受列表表明您打算修改它,这是不正确的 - 这种方法不会触及原始列表,所以IReadOnlyCollection<Data>看起来更好.

但是,即使使用自定义扩展方法,也会IReadOnlyCollection强制调用者ToList().AsReadOnly()每次都会变得有点难看.AsReadOnlyCollection.在接受的东西中,这并不是自由主义者.

在这种情况下,最佳做法是什么?

此方法不返回a,IReadOnlyCollection因为最终Where使用延迟执行可能有值,因为不需要枚举整个列表.但是,Select需要列举,因为.Contains如果没有这样做,做的成本会很糟糕HashSet.

我没有调用问题,ToList我刚想到如果我需要List避免多次枚举,为什么我不只是在参数中要求一个?所以这里的问题是,如果我不想IEnumerable在我的方法中,我是否应该真正接受一个为了自由(和ToList我自己),或者我应该把负担放在呼叫者身上ToList().AsReadOnly()

有关IEnumerables不熟悉的人的更多信息

这里真正的问题不是Any()对战的成本ToList().据我所知,枚举整个列表的成本高于实现成本Any().但是,假设调用者将使用IEnumerable上述方法返回的所有项目,并假设source 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);
}
Run Code Online (Sandbox Code Playgroud)

现在,如果你这样做:

var myData = GetVeryExpensiveDataForDate(todayDate);
var unhandledData = RemoveHandledForDate(myData, todayDate);
foreach (var data in unhandledData) {
   messageBus.Dispatch(data); // fully enumerate
)
Run Code Online (Sandbox Code Playgroud)

如果RemovedHandledForDateAny Where,你会招致5秒的成本的两倍,而不是一次.这就是为什么你应该总是采取极端的痛苦,以避免IEnumerable不止一次枚举.不要依赖于你的知识,事实上它是无害的,因为一些未来倒霉的开发人员有一天会用IEnumerable你从未想过的新实现来调用你的方法,它具有不同的特征.

合同中IEnumerable说你可以枚举它.它不会对不止一次这样做的性能特征做出任何承诺.

事实上,有些IEnumerables易变的,并且在随后的枚举中不会返回任何数据!如果与多个枚举相结合,则切换到一个将是完全破坏性的变化(如果稍后添加多个枚举则很难诊断一个).

不要对IEnumerable进行多次枚举.

如果您接受IEnumerable参数,那么您实际上有希望将它精确地枚举0或1次.

Tim*_*imo 7

IReadOnlyCollection<T>添加到IEnumerable<T>属性Count和相应的承诺,即不存在延迟执行。如果该参数是您想要解决此问题的地方,那么这将是需要询问的适当参数。

不过,我建议询问IEnumerable<T>, 并调用ToList()实现本身。

观察:这两种方法都有缺点,即多重枚举可能在某些时候被重构,导致参数更改或ToList()调用冗余,而我们可能会忽略这一点。我认为这是无法避免的。

该案例确实说明了ToList()在方法主体中进行调用:由于多重枚举是一个实现细节,因此避免它也应该是一个实现细节。这样,我们就可以避免影响 API。如果多重枚举被重构,我们也会避免改回API。IReadOnlyCollection<T>我们还避免通过一系列方法传播需求,由于我们的多重枚举,所有这些方法都必须要求一个just 。

如果您担心创建额外列表的开销(当输出已经是列表左右时),Resharper 建议采用以下方法:

param = param as IList<SomeType> ?? param.ToList();
Run Code Online (Sandbox Code Playgroud)

当然,我们可以做得更好,因为我们只需要防止延迟执行 - 不需要全面的IList<T>

param = param as IReadOnlyCollection<SomeType> ?? param.ToList();
Run Code Online (Sandbox Code Playgroud)


Yac*_*sad 3

您可以在该方法中使用一个,并使用与此处IEnumerable<T>类似的 CachedEnumerable来包装它。

此类包装 anIEnumerable<T>并确保它仅被枚举一次。如果您尝试再次枚举它,它将从缓存中生成项目。

请注意,此类包装器不会立即读取包装的可枚举中的所有项目。当您从包装器枚举单个项目时,它仅枚举来自包装的可枚举的单个项目,并且它会一路缓存单个项目。

这意味着,如果您调用Any包装器,则只会从包装的枚举中枚举单个项目,然后此类项目将被缓存。

如果您随后再次使用该枚举器,它将首先从缓存中生成第一项,然后继续从其离开的位置枚举原始枚举器。

您可以执行以下操作来使用它:

public IEnumerable<Data> RemoveHandledForDate(IEnumerable<Data> data, DateTime dateTime)
{
    var dataWrapper = new CachedEnumerable(data);
    ...
}
Run Code Online (Sandbox Code Playgroud)

请注意,这里方法本身正在包装参数data。这样,您就不会强迫您的方法的使用者做任何事情。