簡體   English   中英

參數的最佳實踐:IEnumerable與IList對比IReadOnlyCollection

[英]Best practice for parameter: IEnumerable vs. IList vs. IReadOnlyCollection

當得到延遲執行中的值時,我會從方法返回 IEnumerable時得到。 返回ListIList應該只是在修改結果時,否則我將返回一個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 ,我是否應該真正接受一個為了自由(並自己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次。

有一些方法可以讓你接受IEnumerable<T> ,只枚舉一次並確保你不多次查詢數據庫。 我能想到的解決方案:

  • 而不是使用AnyWhere你可以直接使用枚舉器。 調用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));
}

您可以在方法中使用IEnumerable<T> ,並使用類似於此處的CachedEnumerable來包裝它。

此類包裝IEnumerable<T>並確保僅枚舉一次。 如果您嘗試再次枚舉它,它會從緩存中生成項目。

請注意,這樣的包裝器不會立即從包裝的可枚舉中讀取所有項目。 當您從包裝器枚舉單個項目時,它僅枚舉包裝的可枚舉項中的各個項目,並在此過程中緩存各個項目。

這意味着如果在包裝器上調用Any ,則只會從包裝的枚舉中枚舉單個項目,然后將緩存此類項目。

如果您再次使用枚舉,它將首先從緩存中生成第一個項目,然后繼續枚舉它離開的原始枚舉器。

你可以做這樣的事情來使用它:

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

請注意,方法本身正在包裝參數data 這樣,您不會強制您的方法的使用者做任何事情。

IReadOnlyCollection<T>IEnumerable<T> IReadOnlyCollection<T>添加一個Count屬性和相應的承諾,即沒有延遲執行 如果參數是您要解決此問題的位置,那么它將是要求的適當參數。

但是,我建議請求IEnumerable<T> ,並在實現本身中調用ToList()

觀察:兩種方法都有一個缺點,即多重枚舉可能會在某些時候被重構,導致參數更改或ToList()調用冗余,我們可能會忽略。 我不認為這是可以避免的。

這個案例的確代表在方法體中調用ToList() :由於多個枚舉是一個實現細節,避免它應該也是一個實現細節。 這樣,我們就可以避免影響API了。 我們也避免更改 API如果多個枚舉不斷被重構了。 我們還避免通過一系列方法傳播需求,否則可能都會決定要求IReadOnlyCollection<T> ,這只是因為我們的多次枚舉。

如果您擔心創建額外列表的開銷(當輸出已經是列表時),Resharper建議采用以下方法:

param = param as IList<SomeType> ?? param.ToList();

當然,我們可以做得更好,因為我們只需要防止延遲執行 - 不需要一個成熟的IList<T>

param = param as IReadOnlyCollection<SomeType> ?? param.ToList();

我不認為只需更改輸入類型就可以解決這個問題。 如果你想允許比List<T>IList<T>更多的通用結構,那么你必須決定是否/如何處理這些可能的邊緣情況。

要么計划最壞的情況,花一點時間/內存創建一個具體的數據結構,要么計划最好的情況,並冒險偶爾查詢執行兩次。

您可以考慮記錄該方法多次枚舉該集合,以便調用者可以決定是否要傳遞“昂貴”查詢,或者在調用該方法之前水合查詢。

我認為IEnumerable<T>是參數類型的一個很好的選擇。 它是一種簡單,通用且易於提供的結構。 IEnumerable合同沒有任何內在的含義,暗示一個人只應該迭代一次。

一般來說,測試.Any()的性能成本可能不高,但當然不能保證這樣。 在您描述的情況下,顯然可能是迭代第一個元素有相當大的開銷,但這絕不是普遍的。

將參數類型更改為類似IReadOnlyCollection<T>IReadOnlyList<T>的選項是一個選項,但在需要該接口提供的部分或全部屬性/方法的情況下可能只是一個好選項。

如果您不需要該功能,而是希望保證您的方法只迭代IEnumerable一次,您可以通過調用.ToList()或將其轉換為其他適當類型的集合來實現,但這是一個實現細節方法本身。 如果您正在設計的合同需要“可以迭代的東西”,那么IEnumerable<T>是一個非常合適的選擇。

您的方法有權保證任何集合的迭代次數,您不需要將該細節暴露在方法的邊界之外。

相反,如果您確實選擇在方法中重復枚舉IEnumerable<T>那么您還必須考慮可能是該選擇的結果的每個可能性,例如由於延遲執行可能在不同情況下獲得不同的結果。

也就是說,作為最佳實踐的一點,我認為盡可能避免自己的代碼返回的IEnumerables任何副作用是有意義的 - 像Haskell這樣的語言可以安全地使用惰性評估,因為它們去了努力避免副作用。 如果不出意外,那些使用您的代碼的人在防止多次枚舉時可能不會像您那樣煩惱。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM