Почему компилятор С# создает частный DisplayClass при использовании метода LINQ Any() и как этого избежать?

У меня есть этот код (весь код не важен, но его можно увидеть на эта ссылка):

internal static class PlayCardActionValidator
{
    public static bool CanPlayCard(...)
    {
        // ...
        var hasBigger =
            playerCards.Any(
                c => c.Suit == otherPlayerCard.Suit
                     && c.GetValue() > otherPlayerCard.GetValue());
        // ...
    }
}

Например, после открытия кода в декомпиляторе (ILSpy) я заметил существование вновь созданного класса <>c__DisplayClass0_0 компилятором C#:

введите описание изображения здесь

Для меня это не было бы проблемой, если бы этот код не был критичен для производительности системы. Этот метод вызывается миллионы раз, и сборщик мусора очищает эти <>c__DisplayClass0_0 экземпляра, что снижает производительность:

введите описание изображения здесь

Как мне избежать создания этого класса (его экземпляров и их сборки мусора) при использовании метода Any?

Почему компилятор C# создает этот класс и есть ли альтернатива Any(), которую я могу использовать?


person Nikolay Kostov    schedule 06.09.2015    source источник
comment
Необходимо переписать ваш код, чтобы найти безопасное место для захваченных переменных, otherPlayerCard и trumpCard здесь. Превращение их из локальных переменных в поля, чтобы их значение можно было сохранить за пределами тела метода. DisplayClass — это безопасный дом.   -  person Hans Passant    schedule 06.09.2015
comment
Не используйте LINQ на горячих путях, это политика кодовой базы Roslyn.   -  person DaveShaw    schedule 06.09.2015
comment
Обычно я бы не рекомендовал микро-оптимизацию, но если этот код запускается миллионы раз, решением будет его рефакторинг для оптимизации скорости. LINQ работает медленно.   -  person Nate Barbettini    schedule 06.09.2015
comment
Ссылка на @DaveShaw?   -  person Daniel A. White    schedule 06.09.2015
comment
@DanielA.White — github.com/dotnet/roslyn/wiki/Contributing-Code - см. соглашения о кодировании.   -  person DaveShaw    schedule 06.09.2015


Ответы (2)


Чтобы понять «класс отображения», вы должны понимать замыкания. Лямбда, которую вы передаете здесь, является замыканием, особым типом метода, который волшебным образом перетаскивает состояние из области действия метода, в котором он находится, и «закрывает» его.

...за исключением, конечно, того, что магии не существует. Все это состояние должно фактически жить где-то в реальном месте, где-то, что связано с методом замыкания и легкодоступно из него. И как вы называете шаблон программирования, в котором вы напрямую связываете состояние с одним или несколькими методами?

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

Единственный способ избежать этого — не использовать замыкания. Если это действительно влияет на производительность, используйте цикл FOR старой школы вместо выражения LINQ.

person Mason Wheeler    schedule 06.09.2015
comment
Любая достаточно продвинутая технология неотличима от магии. - Артур Кларк - person Gusdor; 07.09.2015

Как я могу избежать создания этого класса (его экземпляров и их сборки мусора) при использовании метода Any?

Почему компилятор C# создает этот класс и есть ли альтернатива Any(), которую я могу использовать?

Другие постеры уже объяснили часть почему, поэтому лучше задать вопрос Как я могу избежать создания замыкания?. А ответ прост: если лямбда использует только переданные параметры и/или константы, компилятор не создаст замыкание. Например:

bool AnyClub() { return playerCards.Any(c => c.Suit == CardSuit.Club); }

bool AnyOf(CardSuit suit) { return playerCards.Any(c => c.Suit == suit); }

Первый не создаст замыкание, а второй создаст.

Имея все это в виду и предполагая, что вы не хотите использовать циклы for/foreach, вы можете создать собственные методы расширения, подобные тем, что были в System.Linq.Enumerable, но с дополнительными параметрами. Для этого конкретного случая будет работать что-то вроде этого:

public static class Extensions
{
    public static bool Any<T, TArg>(this IEnumerable<T> source, TArg arg, Func<T, TArg, bool> predicate)
    {
        foreach (var item in source)
            if (predicate(item, arg)) return true;
        return false;
    }
} 

и измените рассматриваемый код на:

var hasBigger =
    playerCards.Any(otherPlayerCard, 
        (c, opc) => c.Suit == opc.Suit
             && c.GetValue() > opc.GetValue());
person Ivan Stoev    schedule 06.09.2015
comment
Хм, почему бы не создать замыкание для параметров или членов экземпляра? Я не знаю, как это можно было бы вытащить. - person usr; 07.09.2015
comment
@usr создаст статическую функцию/экземпляр и привяжет к ней делегата. нет необходимости в отдельном классе, потому что все состояние состоит из аргументов и/или this. - person Ivan Stoev; 07.09.2015
comment
Ладно, класс не нужен. Это правда. Но класс не оказывает никакого влияния на производительность. Создание новых экземпляров делегата и закрытия — это дорого. Я считаю, что вопрос о производительности. Его не волнует сам скрытый класс. - person usr; 07.09.2015
comment
@usr создание экземпляра класса для каждого вызова определенно является дополнительным давлением GC. также, когда лямбда не использует члены экземпляра (как в случае с OP), компилятор создаст статическое поле с скомпилированным делегатом (один раз) - person Ivan Stoev; 07.09.2015