Метод расширения для IEnumerable, необходимый для перетасовки

Мне нужен метод расширения, который будет перемешивать IEnumerable<T>. Он также может принять int, чтобы указать размер возвращаемого IEnumerable. Лучше сохранить неизменность IEnumerable. Мое текущее решение для IList -

public static IList<T> Shuffle<T>(this IList<T> list, int size)
{
    Random rnd = new Random();
    var res = new T[size];

    res[0] = list[0];
    for (int i = 1; i < size; i++)
    {
        int j = rnd.Next(i);
        res[i] = res[j];
        res[j] = list[i];
    }
    return res;
}

public static IList<T> Shuffle<T>(this IList<T> list)
{ return list.Shuffle(list.Count); }

Ответ 1

Вы можете использовать Fisher-Yates-Durstenfeld shuffle. Нет необходимости явно передавать аргумент размера самому методу, вы можете просто нажать на вызов Take, если вам не нужно вся последовательность:

var shuffled = originalSequence.Shuffle().Take(5);

// ...

public static class EnumerableExtensions
{
    public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source)
    {
        return source.Shuffle(new Random());
    }

    public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
    {
        if (source == null) throw new ArgumentNullException("source");
        if (rng == null) throw new ArgumentNullException("rng");

        return source.ShuffleIterator(rng);
    }

    private static IEnumerable<T> ShuffleIterator<T>(
        this IEnumerable<T> source, Random rng)
    {
        var buffer = source.ToList();
        for (int i = 0; i < buffer.Count; i++)
        {
            int j = rng.Next(i, buffer.Count);
            yield return buffer[j];

            buffer[j] = buffer[i];
        }
    }
}

Ответ 2

С некоторыми LINQ love:

public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> list, int size)
{
    var r = new Random();
    var shuffledList = 
        list.
            Select(x => new { Number = r.Next(), Item = x }).
            OrderBy(x => x.Number).
            Select(x => x.Item).
            Take(size); // Assume first @size items is fine

    return shuffledList.ToList();
}

Ответ 3

Антон получил эту идею, но вы могли бы сделать ее двухстрочным:

public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> enumerable)
{
    var r = new Random();
    return enumerable.OrderBy(x=>r.Next()).ToList();
}

К сожалению, он не может быть лениво оценен, потому что r будет недоступен при его выполнении. Вы можете создать реализацию IEnumerable, которая инкапсулирует этот код и возвращает его, но это становится более сложным.