C#条件日志/跟踪

8
我希望给我的C#应用程序添加日志或追踪功能,但如果日志详细级别设置得如此之低以至于消息不会被记录,我不想增加格式化字符串或计算待记录的值的开销。
在C++中,您可以使用预处理器定义宏来完全阻止代码的执行,例如:
#define VLOG(level,expr) if (level >= g_log.verbosity) { g_log.output << expr; }

使用方法如下:

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

如何在C#中实现这个功能?

我已经阅读了微软文档(链接),它们声称使用 #undef DEBUG 和 #undef TRACE 可以从生成的可执行文件中删除所有跟踪和调试代码,但它是否真的删除了整个调用呢?也就是说,如果我写下:

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

如果我取消定义TRACE,它会不会调用我的昂贵函数?还是会先调用,然后决定不追踪任何内容?

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

9个回答

9
回答你的一个问题,所有必须在调用Trace.WriteLine(或其兄弟/表亲)之前计算的方法调用如果Trace.WriteLine被编译掉则不会被调用。因此,将昂贵的方法调用直接作为Trace调用的参数放入其中,并且如果您没有定义TRACE符号,则它将在编译时被删除。
现在回答你另一个关于在运行时更改详细程度的问题。这里的诀窍是Trace.WriteLine和类似方法采用“params object [] args”作为其字符串格式化参数。只有在实际发出字符串(当详细程度设置足够高时)时,才会对这些对象调用ToString以从中获取字符串。因此,我经常玩的一个技巧是将对象传递给这些方法而不是完全组装好的字符串,并将字符串创建留在我传递的对象的ToString中。这样,仅当记录实际发生时才需要运行时性能损耗,并且可以自由更改详细程度而无需重新编译应用程序。

为什么字符串也不会被编译掉呢? - sharptooth
如果你想知道为什么这个对象的ToString技巧没有被编译掉,那是因为如果你使用Trace并且没有定义TRACE常量,它会被编译掉。这个技巧只有在你使用不会被编译掉的方法时才有效。我本可以更清楚地表达。抱歉。 - Andrew Arnott
这很有趣,但似乎是真的。测试:var i=0;Debug.WriteLine(i++);Console.WriteLine("i = " + i); - gatopeich

2

所有关于条件(跟踪)的信息都很好 - 但我认为你真正想问的是,你希望在生产代码中保留Trace调用,但通常情况下在运行时禁用它们,除非你遇到问题。

如果你正在使用TraceSource(我认为你应该使用它,而不是直接调用Trace,因为它可以在运行时以组件级别提供更细粒度的跟踪控制),你可以像这样操作:

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

假设您能够在另一个函数中隔离追踪参数(即,它们大多取决于当前类的成员,而不是对此代码所在的函数的参数进行昂贵的操作)。
这种方法的优点是,因为JITer按照需要逐个编译函数,如果“if”评估为false,则函数不仅不会被调用-甚至不会被JITed。缺点是(a)您已将追踪级别的知识分开在此调用和函数OutputExpensiveTraceInformation之间(因此,例如,如果您更改TraceEventType以使其为TraceEventType.Information,那么它将无法工作,因为除非在此示例中启用了Verbose级别跟踪的TraceSource,否则您甚至都不会调用它),以及(b)需要编写更多代码。
这是一种情况,其中似乎C样式的预处理器会有所帮助(因为它可以确保,例如,ShouldTrace和最终的TraceEvent调用的参数相同),但我理解为什么C#不包括它。
安德鲁建议在传递给TraceEvent的对象的.ToString方法中隔离昂贵的操作,这也是一个好方法;在这种情况下,您可以开发一个仅用于跟踪的对象,将要构建昂贵字符串表示形式的对象传递给该对象,并将该代码隔离在跟踪对象的ToString方法中,而不是在TraceEvent调用的参数列表中执行它(这将导致即使未在运行时启用TraceLevel,它也会被执行)。
希望这有所帮助。

2
一个对我起作用的解决方案是使用一个singleton类。它可以暴露你的日志函数,并且你可以有效地控制它的行为。我们称这个类为“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 );
    }
}

通过这种方式,任何线程都可以记录日志,并且消息会在创建原始线程上处理。或者您可以创建一个专门用于处理日志输出的线程。

这个解决方案如果没有Trace方法所具有的ConditionalAttribute就毫无意义和价值。如果没有定义TRACE,对Trace中的方法进行的任何方法调用都不会被编译进去,而且昂贵的方法永远不会被调用;而在你的解决方案中,它将每次都被调用。 - Samuel
2
不,这并不是毫无意义和价值的(那很严厉)。注意,ConsoleWriteline(或您选择的任何日志记录方法)的调用位于条件语句内。因此,仅在启用日志记录时才需要付出代价。 - Foredecker
我同意Foredecker的观点,他所给出的答案与原帖中提到的示例是等价的。ConditionalAttribute的问题在于原帖作者似乎想在运行时控制它。如果该行代码已被编译器删除,则无法在生产环境下启用它。 - Ed Sykes

2
ConditionalAttribute是你最好的朋友。当未设置#define时,调用将完全被删除(就像调用站点已经被#if'd)。
编辑:有人在评论中放了这个(谢谢!),但值得注意的是主要答案体中:
Trace类的所有方法都带有Conditional(“TRACE”)修饰符。刚刚使用反射看到了这一点。
这意味着如果未定义TRACE,则Trace.Blah(... expensive ...)将完全消失。

Trace类的所有方法都使用Conditional("TRACE")进行了修饰。我刚刚使用反射看到了这一点。 - shahkalpesh
这就是它的工作原理! 现在如果我能在运行时做到这一点,我会非常高兴。 - Carlos A. Ibarra

1

1

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

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


0

针对您的评论

"因为我无法让那个又大又丑的调用看起来像C++中简单的VLOG()调用" - 您可以在下面添加一个using语句作为示例。

使用 System.Diagnostics;
.... Trace.WriteLineIf(.....)

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


长前缀并不是丑陋的部分,而是我不得不在各个地方暴露g_log.verbosity甚至函数名WriteLineIf的事实。我想要拥有自己的函数,比如VLOG(5,...)。 - Carlos A. Ibarra

0

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

将其变成一个非常昂贵的函数(例如Thread.Sleep(10000)),并计时调用。如果它需要很长时间,那么它肯定在调用你的函数。

(你可以使用#if TRACE#endif包装Trace.WriteLineIf()调用,并再次进行基准比较测试。)


0

它将调用昂贵的调用,因为可能存在需要的副作用。

您可以使用[Conditional("TRACE")]或[Conditional("DEBUG")]属性装饰昂贵的方法。如果未定义DEBUG或TRACE常量,则该方法不会编译到最终可执行文件中,并且也不会执行任何调用以执行昂贵的方法。


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