簡體   English   中英

C#如何回報SelectMany?

[英]C# how to yield return SelectMany?

假設我有以下通用組合生成器靜態方法:

public static IEnumerable<IEnumerable<T>> GetAllPossibleCombos<T>(
    IEnumerable<IEnumerable<T>> items)
{
    IEnumerable<IEnumerable<T>> combos = new[] {new T[0]};

    foreach (var inner in items)
        combos = combos.SelectMany(c => inner, (c, i) => c.Append(i));

     return combos;
}

也許我沒有正確理解這一點,但是這不會構建RAM中的整個組合列表嗎? 如果存在大量項目,則該方法可能導致計算機耗盡RAM。

有沒有辦法重新編寫方法以在每個組合上使用yield return ,而不是返回整個組合集?

你的問題有一些誤解,這很棒,因為現在你有機會學習事實而不是神話。


首先,您實現的方法通常稱為CartesianProduct ,而不是GetAllPossibleCombos ,因此請考慮重命名它。


也許我沒有正確理解這一點

你沒有正確理解它。

這不是在RAM中構建整個組合列表嗎?

。查詢構建器構建查詢,而不是執行查詢的結果。 當您執行SelectMany ,您獲得的是將來將進行選擇的對象。 您沒有得到該選擇的結果。

如果存在大量項目,則該方法可能導致計算機耗盡RAM。

今天是停止將內存和RAM視為同樣的事情的好日子。 當進程耗盡內存時,它不會耗盡RAM。 它耗盡了地址空間 ,而不是RAM。 考慮內存的更好方法是:內存是磁盤上的頁面文件 ,RAM是使頁面文件更快的特殊硬件 當RAM耗盡時,您的計算機可能會慢得令人無法接受,但在地址空間不足之前,您的內存不會耗盡。 請記住, 進程內存是虛擬化的

現在, 可能存在執行此代碼效率低下的情況,因為枚舉查詢會耗盡堆棧 並且可能存在執行變得低效的情況,因為您將n個項目向上移動到堆棧n深度。 我建議您對代碼進行更深入的分析,看看是否屬於這種情況,然后向您報告。


有沒有辦法重新編寫方法以在每個組合上使用yield return,而不是返回整個組合集?

SelectManyforeach循環中實現為yield return ,因此您已經將它實現為每個組合的yield return ; 你剛剛在SelectMany的調用中隱藏了yield return

也就是說, SelectMany<A, B, C>(IE<A> items, Func<A, IE<B>> f, Func<A, B, C> g)實現如下:

foreach(A a in items)
  foreach(B b in f(a))
    yield return g(a, b);

所以你已經在yield return做到了。

如果你想編寫一個直接進行yield return的方法,那就更難了; 最簡單的方法是在每個子序列上形成一個枚舉數組,然后從枚舉器的每個Current中生成一個向量, yield return向量,然后將正確的迭代器推進一步。 繼續這樣做,直到不再有正確的迭代器來推進。

正如您可以從該描述中看出的那樣,簿記變得混亂。 這是可行的,但編寫代碼並不是非常愉快。 試試吧! 該解決方案的優點在於,您可以保證具有良好的性能,因為您不會消耗任何堆棧。

更新:這個相關的問題有一個答案張貼,它做了一個迭代算法,但我沒有審查它,看它是否正確。 https://stackoverflow.com/a/57683769/88656


最后,我鼓勵您將您的實施與我的實施進行比較:

https://ericlippert.com/2010/06/28/computing-a-cartesian-product-with-linq/

我的實現是否與您的實現根本不同,或者我們是否正在做同樣的事情,只是使用稍微不同的語法? 給出一些想法。

另外,我鼓勵你閱讀Ian Griffiths關於這個函數的各種實現的分析的優秀六部分系列:

http://www.interact-sw.co.uk/iangblog/2010/07/28/linq-cartesian-1

SelectMany和其他Linq方法返回一個IEnumerable ,只有在枚舉集合時才會延遲評估。 這可以是ToList()ToArray()調用的形式,也可以在foreach循環中迭代它。 當您在調試器警告中看到消息時,擴展集合將枚舉可枚舉,這是它警告您的行為。 該集合尚未枚舉 - Linq查詢僅構建一系列調用,告訴它如何枚舉數據。

因此,您對RAM使用的擔憂不一定准確(取決於起始IEnumerable的具體類型)。 即使你調用ToList()ToArray()並在變量中存儲對它的引用,如果集合元素是引用類型,那么它也不是副本。

在您的示例中,如果您想延遲構建元素集合而不將其存儲在單獨的集合中(例如,需要額外復制的返回列表或數組),則yield return會為您提供便利。 我不認為它適用於你想要做的事情,因為SelectMany已經有了這種行為。

如果你想嘗試一下,Linq可以很容易地使用Enumerable.Repeat生成大型列表

// Define a collection with 10000000 items (items not created yet)
var manyItems = Enumerable.Repeat(123, 10000000);

// Enumerate the enumerable via ToList: creates the int 10000000 times
var manyItemsConcrete = manyItems.ToList();

// same deal with reference types
var manyReferenceTypes = Enumerable.Repeate(new object(), 10000000);
var manyReferenceTypesConcrete = manyReferenceTypes.ToList();

// This list already exists in RAM taking up space
var list = new List<object> { new object(), new object() /* ... x10000000 */ }
// This defines a transform on list, but doesn't take up RAM
var enumerable = list.Select(x => x.ToString());

// Now, there are two lists taking up RAM
var newList = enumerable.ToList();

暫無
暫無

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

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