为什么使用LINQ方法Any()时,C#编译器会创建私有的DisplayClass类?如何避免它?

32

我有这样的代码(整个代码并不重要,但可以在此链接中查看):

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

在使用反编译工具(例如ILSpy)打开代码后,我注意到了由C#编译器新创建的类<>c__DisplayClass0_0的存在:

enter image description here

如果这段代码对系统性能不是至关重要,这对我来说不是问题。但是因为这个方法被调用了数百万次,垃圾回收器正在清理这些<>c__DisplayClass0_0实例,从而降低了性能:

enter image description here

当使用 Any 方法时如何避免创建这个类(它的实例和它们的垃圾回收),是否有替代方案?

C#编译器为什么会创建这个类?是否有其他可替代的方法可以使用 Any() 方法?


4
需要重写你的代码,为捕获的变量otherPlayerCard和trumpCard找到一个安全的家。将它们从局部变量转换为字段,以便它们的值可以在方法体之外得以保留。DisplayClass就是这个安全的家。 - Hans Passant
11
在Roslyn代码库中的热路径上不要使用LINQ,这是一项策略。 - DaveShaw
5
通常情况下,我会避免推荐微小的优化,但如果这段代码将被运行数百万次,为了提高速度而对其进行优化是解决问题的方法。LINQ很慢。 - Nate Barbettini
1
@DaveShaw 参考资料? - Daniel A. White
3
@DanielA.White - https://github.com/dotnet/roslyn/wiki/Contributing-Code - 请查看"编码规范"。 - DaveShaw
2个回答

42
为了理解“显示类”,您需要理解闭包。您在此处传递的lambda是一个闭包,一种特殊类型的方法,可以神奇地从其所在方法的范围中拖入状态并“封闭”它。
当然,这里没有魔法。所有状态实际上都必须存在于某个真实的位置,即与闭包方法相关联并且可以轻松从其中访问的位置。当您将状态直接与一个或多个方法关联时,您如何称呼编程模式?
没错:类。编译器将lambda转换为闭包类,然后在托管方法内实例化该类,以便托管方法可以访问类中的状态。
唯一不会发生这种情况的方法是不使用闭包。如果这真的影响性能,请改用老派的FOR循环而不是LINQ表达式。

12
任何足够先进的技术都和魔法一样让人难以分辨。- 亚瑟·克拉克(Arthur C. Clarke) - Gusdor

25
如何在使用Any方法时避免创建这个类(它的实例和垃圾回收)?为什么C#编译器会创建这个类?我可以使用任何替代方法吗?
其他发帖者已经解释了为什么部分,所以更好的问题是如何避免创建闭包。答案很简单:如果lambda仅使用传递的参数和/或常量,则编译器不会创建闭包。例如:
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());

1
嗯,为什么参数或实例成员不会创建闭包呢?我不知道有什么方法可以做到这一点。 - usr
@usr 这将创建一个静态/实例函数并将委托绑定到它上面。不需要单独的类,因为整个状态都包含参数和/或 this - Ivan Stoev
好的,不需要一个类。没错。但是该类没有任何性能影响。创建新的委托和闭包实例才是昂贵的。我认为这个问题是关于性能的。他并不关心隐藏类本身。 - usr
@usr,每次调用创建类实例肯定会增加GC压力。此外,当lambda表达式不使用实例成员(如在OP案例中),编译器将生成一个带有已编译委托的静态字段(一次)。 - Ivan Stoev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接