为啥 C# 编译器在使用 LINQ 方法 Any() 时会创建私有 DisplayClass,我该如何避免它?

Posted

技术标签:

【中文标题】为啥 C# 编译器在使用 LINQ 方法 Any() 时会创建私有 DisplayClass,我该如何避免它?【英文标题】:Why does C# compiler create private DisplayClass when using LINQ method Any() and how can I avoid it?为什么 C# 编译器在使用 LINQ 方法 Any() 时会创建私有 DisplayClass,我该如何避免它? 【发布时间】:2015-12-02 06:02:36 【问题描述】:

我有这段代码(整个代码并不重要,但可以在this link上看到):

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 的存在:

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

在使用Any 方法时如何避免创建此类(他的实例及其垃圾收集)?

为什么 C# 编译器会创建这个类,我可以使用 Any() 的替代品吗?

【问题讨论】:

这里需要重写你的代码来为捕获的变量otherPlayerCard和trumpCard找到一个安全的家。将它们从局部变量转换为字段,以便它们的值可以保留在方法体之外。 DisplayClass 就是那个安全的家。 不要在热路径上使用 LINQ,这是 Roslyn 代码库的政策。 我通常会避免推荐微优化,但如果此代码运行 数百万 次,则重构此代码以优化速度将是这里的解决方案。 LINQ 很慢。 @DaveShaw 参考? @DanielA.White - github.com/dotnet/roslyn/wiki/Contributing-Code - 请参阅编码约定。 【参考方案1】:

要了解“显示类”,您必须了解闭包。您在此处传递的 lambda 是一个 闭包,这是一种特殊类型的方法,它神奇地将状态从其所在方法的范围内拖入并“围绕”它。

...当然,除了没有魔法这样的东西。所有这些状态都必须真正存在于某个真实的地方,某个与闭包方法相关的地方,并且可以从中轻松获得。将状态直接与一种或多种方法相关联的编程模式是什么?

没错:类。编译器将 lambda 转换为闭包类,然后在宿主方法中实例化该类,以便宿主方法可以访问类中的状态。

避免这种情况发生的唯一方法是不使用闭包。如果这确实会影响性能,请使用老式的 FOR 循环而不是 LINQ 表达式。

【讨论】:

“任何足够先进的技术都与魔法无异。” - Arthur C. Clarke【参考方案2】:

使用 Any 方法时如何避免创建此类(他的实例及其垃圾收集)?

为什么 C# 编译器会创建这个类,我可以使用 Any() 的替代方法吗?

其他海报已经解释了为什么部分,所以更好的问题是我怎样才能避免创建闭包?。答案很简单:如果 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());

【讨论】:

嗯,为什么不为参数或实例成员创建闭包?我知道这不可能被拉出来。 @usr 它将创建一个静态/实例函数并将委托绑定到它。不需要单独的类,因为整个状态由参数和/或this组成。 好的,不需要上课。确实如此。但是该课程的性能影响为零。创建新的委托和闭包实例是昂贵的。我相信问题是关于性能的。他并不关心隐藏类本身。 @usr 好吧,每次调用创建类实例肯定是额外的 GC 压力。此外,当 lambda 不使用实例成员时(如在 OP 情况下),编译器将生成一个带有已编译委托的静态字段(一次)

以上是关于为啥 C# 编译器在使用 LINQ 方法 Any() 时会创建私有 DisplayClass,我该如何避免它?的主要内容,如果未能解决你的问题,请参考以下文章

C# Linq ANY vs ALL - 性能 [关闭]

C# Any()和AII()方法

C#-Linq源码解析之Any

LINQ 扩展方法 - Any() vs. Where() vs. Exists()

LINQ Any vs Exists 性能 [重复]

Linq join and count 给我一个错误...不知道为啥 c#