Подтвердить что ты не робот

Как определить, зависит ли IEnumerable <T> отсроченное исполнение?

Я всегда предполагал, что если бы я использовал Select(x=> ...) в контексте LINQ для объектов, то новая коллекция была бы немедленно создана и оставалась бы статической. Я не совсем уверен, ПОЧЕМУ Я принял это, и это очень плохое предположение, но я это сделал. Я часто использую .ToList() в другом месте, но часто не в этом случае.

Этот код демонстрирует, что даже простой "Выбрать" подлежит отсроченному исполнению:

var random = new Random();
var animals = new[] { "cat", "dog", "mouse" };
var randomNumberOfAnimals = animals.Select(x => Math.Floor(random.NextDouble() * 100) + " " + x + "s");

foreach (var i in randomNumberOfAnimals)
{
    testContextInstance.WriteLine("There are " + i);
}

foreach (var i in randomNumberOfAnimals)
{
    testContextInstance.WriteLine("And now, there are " + i);
}

Это выводит следующее (случайная функция вызывается каждый раз, когда выполняется итерация коллекции):

There are 75 cats
There are 28 dogs
There are 62 mouses
And now, there are 78 cats
And now, there are 69 dogs
And now, there are 43 mouses

У меня есть много мест, где у меня есть IEnumerable<T> как член класса. Часто результаты запроса LINQ присваиваются такому IEnumerable<T>. Обычно для меня это не вызывает проблем, но я недавно нашел несколько мест в своем коде, где он представляет собой не только проблему с производительностью.

В попытке проверить места, где я совершил эту ошибку, я подумал, что могу проверить, был ли конкретный IEnumerable<T> типом IQueryable. Это, я думал, скажет мне, была ли коллекция "отложена" или нет. Оказывается, что счетчик, созданный оператором Select выше, имеет тип System.Linq.Enumerable+WhereSelectArrayIterator``[System.String,System.String], а не IQueryable.

Я использовал Reflector, чтобы узнать, от чего этот интерфейс унаследован, и получается, что он не наследует ничего, что указывает на то, что это" LINQ 'вообще - так что нет способа протестировать на основе типа коллекции.

Я довольно счастливо теперь помещаю .ToArray() всюду сейчас, но я хотел бы иметь механизм, чтобы убедиться, что эта проблема не произойдет в будущем. Visual Studio, похоже, знает, как это сделать, потому что она дает сообщение о том, что "расширение представления результатов будет оценивать коллекцию".

Лучшее, что я придумал, это:

bool deferred = !object.ReferenceEquals(randomNumberOfAnimals.First(),
                                        randomNumberOfAnimals.First());

Изменить: Это работает только в том случае, если новый объект создается с помощью "Выбрать" и не является общим решением. Я не рекомендую это в любом случае! Это был маленький язык в щеку решения.

4b9b3361

Ответ 1

Отложенное выполнение LINQ захватило много людей, вы не одиноки.

Подход, который я предпринял для устранения этой проблемы, выглядит следующим образом:

Параметры для методов - используйте IEnumerable<T>, если нет необходимости в более конкретном интерфейсе.

Локальные переменные - обычно в точке, где я создаю LINQ, поэтому я буду знать, возможна ли ленивая оценка.

Члены класса - никогда не используйте IEnumerable<T>, всегда используйте List<T>. И всегда делайте их частными.

Свойства - используйте IEnumerable<T> и конвертируйте для хранения в установщик.

public IEnumerable<Person> People 
{
    get { return people; }
    set { people = value.ToList(); }
}
private List<People> people;

Несмотря на то, что существуют теоретические случаи, когда такой подход не сработает, я пока не сталкивался с ним, и я с энтузиазмом использовал методы расширения LINQ с позднего бета-версии.

BTW: Мне любопытно, почему вы используете ToArray(); вместо ToList(); - для меня, списки имеют намного более хороший API и там (почти) нет стоимости исполнения.

Обновление. Несколько комментаторов справедливо отметили, что массивы имеют теоретическое преимущество в производительности, поэтому я внесла поправки в свое выступление выше: "... (почти) нет стоимости исполнения".

Обновление 2. Я написал некоторый код, чтобы выполнить микро-бенчмаркинг разницы в производительности между массивами и списками. На моем ноутбуке и в моем конкретном ориентире разница составляет около 5 нс (это наносекунды) за доступ. Я предполагаю, что есть случаи, когда сохранение 5 нс за цикл было бы целесообразным... но я никогда не сталкивался с ним. Я должен был пройти свой тест до 100 миллионов итераций до того, как время выполнения получилось достаточно длинным, чтобы точно измерить.

Ответ 2

В общем, я бы сказал, вы должны стараться не беспокоиться о том, откладывается ли он.

Есть преимущества для характера выполнения потоковой передачи IEnumerable<T>. Это правда - иногда это невыгодно, но я бы рекомендовал всегда обращаться с этими (редкими) временами конкретно - либо пойти ToList(), либо ToArray(), чтобы преобразовать его в список или массив по мере необходимости.

В остальное время лучше просто отложить его. Необходимость часто проверять это кажется более сложной проблемой дизайна...

Ответ 3

Мои пять центов. Довольно часто вам приходится иметь дело с перечислимым, что вы не знаете, что внутри него.

Ваши варианты:

  • перед тем, как использовать его, переверните его в список, но есть вероятность, что он бесконечен.
  • используйте его как есть, и вы, вероятно, столкнетесь со всеми видами отложенного исполнения забавных вещей, и у вас снова проблемы.

Вот пример:

[TestClass]
public class BadExample
{
    public class Item
    {
        public String Value { get; set; }
    }
    public IEnumerable<Item> SomebodysElseMethodWeHaveNoControlOver()
    {
        var values = "at the end everything must be in upper".Split(' ');
        return values.Select(x => new Item { Value = x });
    }
    [TestMethod]
    public void Test()
    {
        var items = this.SomebodysElseMethodWeHaveNoControlOver();
        foreach (var item in items)
        {
            item.Value = item.Value.ToUpper();
        }
        var mustBeInUpper = String.Join(" ", items.Select(x => x.Value).ToArray());
        Trace.WriteLine(mustBeInUpper); // output is in lower: at the end everything must be in upper
        Assert.AreEqual("AT THE END EVERYTHING MUST BE IN UPPER", mustBeInUpper); // <== fails here
    }
}

Таким образом, нет способа уйти от него, но одно: повторите его ровно один раз по принципу "как хочешь".

Очевидно, что плохой выбор дизайна для использования одного и того же интерфейса IEnumerable для сценариев немедленного и отсроченного исполнения. Должно быть четкое различие между этими двумя, чтобы оно очистилось от имени или путем проверки свойства, независимо от того, отложено ли перечислимое число.

Подсказка: в коде используйте IReadOnlyCollection<T> вместо обычного IEnumerable<T>, потому что в дополнение к этому вы получаете свойство Count. Таким образом, вы точно знаете, что это не бесконечно, и вы можете превратить его в список без проблем.

Ответ 4

Сообщение о расширении представления результатов будет оценивать сбор, это стандартное сообщение, представленное для всех объектов IEnumerable. Я не уверен, что есть какие-либо надежные средства для проверки того, отложен ли IEnumerable, главным образом потому, что отложен еще один yield. Единственное средство, гарантирующее, что оно не отложено, - это принять ICollection или IList<T>.

Ответ 5

Абсолютно возможно вручную реализовать ленивый IEnumerator<T>, поэтому нет "совершенно общего" способа сделать это. Я помню следующее: если я изменяю вещи в списке, перечисляя что-то, связанное с ним, всегда вызывайте ToArray() перед foreach.

Ответ 6

Это интересная реакция на отложенное исполнение - большинство людей считают это позитивным, поскольку оно позволяет вам преобразовывать потоки данных без необходимости их буферизации.

Ваш предлагаемый тест не будет работать, потому что нет причин, по которым метод итератора не может дать тот же экземпляр ссылочного объекта, что и его первый объект, при двух последовательных попытках.

IEnumerable<string> Names()
{
    yield return "Fred";
}

Это будет возвращать один и тот же статический строковый объект каждый раз, как единственный элемент в последовательности.

Поскольку вы не можете надежно обнаружить класс, сгенерированный компилятором, который возвращается из метода итератора, вам нужно будет сделать обратное: проверьте несколько известных контейнеров:

public static IEnumerable<T> ToNonDeferred(this IEnumerable<T> source)
{
    if (source is List<T> || source is T[]) // and any others you encounter
        return source;

    return source.ToArray();
}

Вернув IEnumerable<T>, мы сохраняем сборник только для чтения, что важно, потому что мы можем вернуть копию или оригинал.