有趣的是,有些人選擇IEnumerable<T>
,而其他一些人堅持IReadOnlyList<T>
。
現在說實話。 IEnumerable<T>
是有用的,非常有用。在大多數情況下,你只是想把這個方法放在某個庫中,然後把你的效用函數放到你認爲是一個集合的地方,然後用它來完成。但是,使用IEnumerable<T>
正確是有點棘手,因爲在這裏我要指出...
IEnumerable的
讓我們的第二個假設op是使用LINQ,並希望從中獲取隨機元素一個序列。基本上,他從@Yannick代碼結束,在實用的輔助函數庫結束:
public static T AnyOne<T>(this IEnumerable<T> source)
{
int endExclusive = source.Count(); // #1
int randomIndex = Random.Range(0, endExclusive);
return source.ElementAt(randomIndex); // #2
}
現在,這主要做的是兩兩件事:
- 計數元素的數量在來源中。如果源是簡單的
IEnumerable<T>
這意味着要經歷列表中的所有元素,如果它是f.ex.一個List<T>
,它將使用Count
屬性。
- 重置枚舉,轉到元素
randomIndex
,抓住並返回它。
這裏有兩件事可能會出錯。首先,你的IEnumerable可能是一個緩慢的順序存儲,而做Count
會以一種意想不到的方式破壞應用程序的性能。例如,從設備流式傳輸可能會讓您陷入麻煩。也就是說,你可能會認爲,當這是該系列的特徵所固有的時候 - 而且我個人認爲這種說法會持續下去。
其次 - 這也許更重要 - 不能保證你枚舉將在每次迭代中返回相同的序列(因此也不能保證你的代碼不會崩潰)。例如,考慮這個無辜的看着一段代碼,可能用於測試目的是有用的:
IEnumerable<int> GenerateRandomDataset()
{
Random rnd = new Random();
int count = rnd.Next(10, 100); // randomize number of elements
for (int i=0; i<count; ++i)
{
yield return new rnd.Next(0, 1000000); // randomize result
}
}
第一次迭代(呼叫Count()
),你可能會產生99個結果。你選擇元素98.接下來你調用ElementAt
,第二次迭代產生12個結果,你的應用程序崩潰。不酷。
修復了IEnumerable實現
正如我們所看到的,IEnumerable<T>
執行的問題是,你必須要經過數據的2倍。我們可以通過一次性查看數據來解決這個問題。
這裏的'技巧'其實很簡單:如果我們看到1個元素,我們肯定想要考慮返回它。考慮到所有元素,這是我們要返回的元素的50%/ 50%的機率。如果我們看到第三個因素,那麼我們會有33%/ 33%/ 33%的機會返回。等等。
因此,更好的實現可能是這樣:
public static T AnyOne<T>(this IEnumerable<T> source)
{
Random rnd = new Random();
double count = 1;
T result = default(T);
foreach (var element in source)
{
if (rnd.NextDouble() <= (1.0/count))
{
result = element;
}
++count;
}
return result;
}
在一個側面說明:如果我們使用LINQ,我們希望操作使用IEnumerable<T>
一次(也是唯一一次!)。現在你知道爲什麼了。
使其與列表和數組
雖然這是一個巧妙的技巧,我們的表現,現在會比較慢,如果我們在一個List<T>
,這沒有任何意義的工作,因爲我們知道有很多工作由於索引和Count
可用於我們的財產更好的實施可用。
我們正在尋找的是的公分母這個更好的解決方案,它被用於儘可能多的收藏,我們可以找到。我們最終得到的是接口,它實現了我們需要的一切。
因爲我們知道爲IReadOnlyList<T>
是真實的特性,我們現在可以安全地使用Count
和索引,而不運行崩潰的應用程序的風險。
但是,雖然IReadOnlyList<T>
似乎有吸引力,IList<T>
由於某種原因似乎並沒有實現它......這基本上意味着IReadOnlyList<T>
是一個實踐中的賭博。在這方面,我很肯定有比IReadOnlyList<T>
實現有更多的IList<T>
實現。因此,似乎最好只支持這兩種接口。
這使我們的解決方案在這裏:
public static T AnyOne<T>(this IEnumerable<T> source)
{
var rnd = new Random();
var list = source as IReadOnlyList<T>;
if (list != null)
{
int index = rnd.Next(0, list.Count);
return list[index];
}
var list2 = source as IList<T>;
if (list2 != null)
{
int index = rnd.Next(0, list2.Count);
return list2[index];
}
else
{
double count = 1;
T result = default(T);
foreach (var element in source)
{
if (rnd.NextDouble() <= (1.0/count))
{
result = element;
}
++count;
}
return result;
}
}
PS:對於更復雜的場景中的,檢查出的策略模式。
隨機
@Yannick Motton做,你必須要小心Random
此話,因爲如果你這樣調用了很多次的方法也不會是真正隨機的。Random是用RTC初始化的,所以如果你多次創建一個新實例,它不會改變種子。
對此的一種簡單的方法如下:
private static int seed = 12873; // some number or a timestamp.
// ...
// initialize random number generator:
Random rnd = new Random(Interlocked.Increment(ref seed));
這樣,每次你打電話的人時,隨機數發生器將接受另一個種子,它會在緊密的循環甚至工作。
總結:
所以,總結一下吧:
IEnumerable<T>
的應該重複一次,只有一次。否則可能會給用戶帶來意想不到的結果。
- 如果您可以訪問比簡單枚舉更好的功能,則不需要遍歷所有元素。最好立即抓住正確的結果。
- 考慮你正在仔細檢查的接口。雖然
IReadOnlyList<T>
絕對是最好的候選人,但它不會從IList<T>
繼承,這意味着它在實踐中效率會降低。
最終結果就是Just Works。
什麼是Random.Range()?它是否在'(start,end)'之間產生一個值,包括結束或排除? –
嗨yannick,謝謝你關注這個問題。 Random.Range真的不重要,只是考慮它的僞代碼。非常明顯,對於整數,它是0到k獨佔(arraywise),或它不會工作:) ...它只是來自最受歡迎的遊戲開發環境Unity3D的一個函數 – Fattie