8

我想向我的 C# 应用程序添加日志记录或跟踪,但如果日志详细级别设置得太低以至于不会记录消息,我不希望记录格式化字符串或计算值的开销。

在 C++ 中,您可以使用预处理器来定义将阻止代码执行的宏,如下所示:

#define VLOG(level,expr) if (level >= g_log.verbosity) { g_log.output << expr; }

像这样使用:

VLOG(5,"Expensive function call returns " << ExpensiveFunctionCall());

你如何在 C# 中做到这一点?

我已经阅读了解释 Trace 和 Debug 设施的 Microsoft 文档他们声称使用 #undef DEBUG 和 #undef TRACE 会从生成的可执行文件中删除所有跟踪和调试代码,但它真的会删除整个调用吗?意思是,如果我写

System.Diagnostics.Trace.WriteLineIf(g_log.verbosity>=5,ExpensiveFunctionCall());

如果我取消定义 TRACE,它不会调用我昂贵的函数吗?还是打了电话,然后决定不追踪任何东西?

无论如何,即使它确实删除了它,这也不如 C++ 宏,因为我不能让那个丑陋的大调用看起来像我在 C++ 中的简单 VLOG() 调用并且仍然避免评估参数,可以吗?我也不能像在 C++ 中那样通过在运行时定义更低的详细程度来避免开销,对吧?

4

9 回答 9

9

要回答您的问题之一,如果 Trace.WriteLine 被编译出来,则不会调用所有必须评估才能调用 Trace.WriteLine(或其兄弟姐妹/表兄弟)的方法调用。因此,继续将昂贵的方法调用直接作为 Trace 调用的参数放入,如果您未定义 TRACE 符号,它将在编译时删除。

现在关于在运行时更改详细程度的其他问题。这里的诀窍是 Trace.WriteLine 和类似的方法将 'params object[] args' 作为其字符串格式参数。只有在实际发出字符串时(当详细程度设置得足够高时),该方法才会对这些对象调用 ToString 以从中获取字符串。所以我经常玩的一个技巧是将对象而不是完全组装的字符串传递给这些方法,并将字符串创建留在我传入的对象的 ToString 中。这样,只有在实际发生日志记录时才支付运行时性能税,它使您可以自由地更改详细程度,而无需重新编译您的应用程序。

于 2009-02-17T05:31:19.410 回答
2

ConditionalAttribute是你最好的朋友。当未设置#define 时,呼叫将被完全删除(就像呼叫站点是#if'd)。

编辑:有人将其放在评论中(谢谢!),但在主要答案正文中值得注意:

Trace 类的所有方法都用 Conditional("TRACE") 修饰。刚刚看到这个使用反射器。

这意味着如果未定义 TRACE,则 Trace.Blah(...expensive...) 确实会完全消失。

于 2009-02-17T05:40:11.090 回答
2

一个对我有用的解决方案是使用单例类。它可以公开您的日志记录功能,并且您可以有效地控制其行为。让我们调用类“AppLogger”。她就是一个例子

public class AppLogger
{
   public void WriteLine(String format, params object[] args)
    {
        if ( LoggingEnabled )
        {
            Console.WriteLine( format, args );
        }
    }
}

请注意,Singleton 的东西不在上面的例子中。试管中有很多很好的例子。现在有趣的是如何支持多线程。我是这样做的:(为简洁起见,哈哈哈)

public static void WriteLine( String format, params object[] args )
{
    if ( TheInstance != null )
    {
        TheInstance.TheCreatingThreadDispatcher.BeginInvoke(  Instance.WriteLine_Signal, format, args );
    }
}

这样,任何线程都可以记录,并且消息在原始创建线程上处理。或者您可以创建一个专门的线程来处理日志输出。

于 2009-02-17T05:54:20.773 回答
2

有关 Conditional(Trace) 的所有信息都很好 - 但我认为您真正的问题是您希望将 Trace 调用留在生产代码中,但(通常)在运行时禁用它们,除非您遇到问题。

如果您正在使用 TraceSource(我相信您应该这样做,而不是直接调用 Trace,因为它可以让您在运行时对组件级别的跟踪进行更细粒度的控制),您可以执行以下操作:

if (Component1TraceSource.ShouldTrace(TraceEventType.Verbose))
     OutputExpensiveTraceInformation()

这假设您能够隔离另一个函数中的跟踪参数(即,它们主要依赖于当前类的成员,而不是对该代码所在函数的参数进行昂贵的操作)。

这种方法的优点是因为 JITer 可以根据需要逐个函数编译,如果“if”评估为 false,则不仅不会调用该函数 - 它甚至不会被 JITed。缺点是 (a) 您已经在此调用和函数 OutputExpensiveTraceInformation 之间分离了跟踪级别的知识(因此,例如,如果您将 TraceEventType 更改为 TraceEventType.Information,它将无法工作,因为您永远不会甚至调用它,除非在此示例中为详细级别跟踪启用了 TraceSource)和(b)它需要编写更多代码。

在这种情况下,类似 C 的预处理器似乎会有所帮助(例如,因为它可以确保 ShouldTrace 和最终 TraceEvent 调用的参数相同),但我理解为什么 C# 没有包括那个。

Andrew 建议在传递给 TraceEvent 的对象的 .ToString 方法中隔离昂贵的操作,这也是一个很好的建议;在这种情况下,您可以例如开发一个仅用于 Trace 的对象,您可以向该对象传递您想要构建昂贵的字符串表示的对象,并在跟踪对象的 ToString 方法中隔离该代码,而不是在TraceEvent 调用的参数列表(即使在运行时未启用 TraceLevel 也会导致它被执行)。

希望这可以帮助。

于 2010-07-15T21:20:06.397 回答
1

您是否尝试过像 log4net ( http://logging.apache.org/log4net/index.html ) 这样的复杂日志记录 API?

于 2009-02-17T07:10:17.160 回答
1

其中两个答案(Andrew Arnott 和 Brian 的)确实回答了我的部分问题。如果 TRACE 或 DEBUG 为 #undef'd,则应用于 Trace 和 Debug 类方法的 ConditionalAttribute 会导致删除对方法的所有调用,包括昂贵的参数评估。谢谢!

对于第二部分,是否可以在运行时而不是在编译时完全删除所有调用,我在 log4net fac中找到了答案。根据他们的说法,如果您在启动时设置只读属性,运行时将编译掉所有未通过测试的调用!这不会让您在启动后更改它,但这很好,这比在编译时删除它们要好。

于 2009-02-18T03:24:10.500 回答
0

对于您的评论

“因为我不能让那个丑陋的大调用看起来像我在 C++ 中的简单 VLOG() 调用” - 您可以添加一个 using 语句,如下例所示。

using System.Diagnostics;

....
Trace.WriteLineIf(.....)

据我了解,如果您取消定义 Trace 符号,它将删除包含 Trace 的行。

于 2009-02-17T05:14:46.453 回答
0

我不确定,但你可以自己找出答案。

让它成为一个非常昂贵的功能(如Thread.Sleep(10000))并计时。如果需要很长时间,那么无论如何它都会调用您的函数。

(您可以使用 and 包装Trace.WriteLineIf()调用#if TRACE#endif再次对其进行测试以进行基本比较。)

于 2009-02-17T05:17:26.480 回答
0

它将调用昂贵的调用,因为它可能具有所需的副作用。

你可以做的是用 [Conditional("TRACE")] 或 [Conditional("DEBUG")] 属性来装饰你昂贵的方法。如果未定义 DEBUG 或 TRACE 常量,则该方法将不会被编译为最终的可执行文件,也不会任何调用来执行昂贵的方法。

于 2009-02-17T05:48:05.390 回答