【问题标题】:How to use params keyword along with caller Information in C#?如何在 C# 中使用 params 关键字和调用者信息?
【发布时间】:2014-12-30 09:17:43
【问题描述】:

我正在尝试将 C# 5.0 调用方信息与 C# params 关键字结合起来。目的是为日志框架创建一个包装器,我们希望日志器像 String.Format 一样格式化文本。在以前的版本中,该方法如下所示:

void Log(
   string message,
   params object[] messageArgs = null);

我们这样称呼它:

log.Log("{0}: I canna do it cap'n, the engines can't handle warp {1}!", 
        "Scotty", warpFactor);

现在,我们要捕获来电者信息并将其记录下来。于是签名就变成了:

void Log(
    string message,
    params object[] messageArgs,
    [CallerMemberName] string sourceMemberName = null);

这不会编译,因为参数必须是最后一个参数。所以我试试这个:

void Log(
    string message,
    [CallerMemberName] string sourceMemberName = null,
    params object[] messageArgs);

有没有一种方法可以在不提供 sourceMembername 或将 messageArgs 参数显式分配为命名参数的情况下调用它?这样做违背了 params 关键字的目的:

// params is defeated:
log.Log("message", 
        messageArgs: new object[] { "Scotty", warpFactor });
// CallerMemberName is defeated:
log.Log("message", null,
        "Scotty", warpFactor);

有没有办法做到这一点?似乎传递调用者信息的“hacky”方式排除了使用 params 关键字。如果 C# 编译器识别出调用者成员信息参数根本不是 真正 参数,那就太棒了。我认为没有必要明确地传递它们。

我的备份将是跳过 params 关键字,调用者将不得不使用最后一个示例中的长签名。

【问题讨论】:

  • 你不能用不包含 sourceMemberName 参数的普通旧重载来做吗?
  • @Louis:关键是他们希望方法包含该参数。由于[CallerMemberName] 属性,它会由编译器自动填充。
  • asked this question previously 并收到了一些有用的建议。我对这个问题的回答包括我最终采取的解决这个限制的方法。
  • Dan:我想我浏览了所有标记为 callermembername 的问题。抱歉,我错过了那个。

标签: c# params-keyword callermembername


【解决方案1】:

我不认为它可以完全按照您想要的方式完成。不过,我可以想到一些可行的解决方法,它们可能会给您带来几乎相同的好处。

  1. 使用中间方法调用来捕获调用者成员名称。第一个方法调用返回一个委托,该委托又可以被调用以提供附加参数。这看起来很奇怪,但它应该可以工作:

    log.Log()("{0}: I canna do it cap'n, the engines can't handle warp {1}!", 
    "Scotty", warpFactor);
    

    这里的一个缺点是可以调用log.Log("something"),期望您的消息会被记录下来,而不会发生任何事情。如果您使用 Resharper,您可以通过将 [Pure] 属性添加到 Log() 方法来缓解这种情况,这样如果有人没有对生成的对象执行任何操作,您就会收到警告。你也可以稍微调整一下这个方法,说:

    var log = logFactory.GetLog(); // <--injects method name.
    log("{0}: I canna do it cap'n, the engines can't handle warp {1}!", 
        "Scotty", warpFactor);
    
  2. 使用 lambda 生成日志消息,并让 string.Format 处理 params 数组:

    log.Log(() => string.Format("{0}: I canna do it cap'n, the engines can't handle warp {1}!", 
        "Scotty", warpFactor));
    

    这是我通常使用的方法,它有一些附带优势:

    1. 您的日志方法可以捕获在生成调试字符串时产生的异常,因此您不会破坏您的系统,而是会收到一条错误消息:“无法生成日志消息:[异常详细信息]”。
    2. 有时您传递给格式字符串的对象可能会产生额外的成本,而您只想在需要时产生:

      log.Info(() => string.Format("{0}: I canna do it cap'n, the engines can't handle warp {1}!", 
          _db.GetCurrentUsername(), warpFactor));
      

      如果未打开信息级日志记录,您不希望上述代码执行数据库访问。

    附带说明一下,我发现自己经常使用 string.Format,因此我创建了一个辅助方法来稍微缩短语法:

    log.Log(() => "{0}: I canna do it cap'n, the engines can't handle warp {1}!" 
        .With("Scotty", warpFactor));
    

【讨论】:

  • 第一个选项看起来很酷。不过,您需要创建一个带有 params 属性的自定义委托才能使其工作。
  • 哇,这些都是一些非常创新的想法。让我玩弄那个。我将无法逐字使用#2,因为我们实际上传递了一个指向消息资源字符串的 lambda。我刚刚从问题中删除了那个细节。所以我们的调用实际上是 log.Log(() => Resources.EngineWarpError, ...) 但我认为该技术仍然可以使用。
  • @MobyDisk:谢谢。我还在答案中添加了一些您可能会觉得有用的信息。
  • 我只想在这里提一个明显的替代方案,即您始终可以在没有 lambda 的情况下执行 string.Format() 或 .With() 扩展。委托确实会增加一些开销,所以我通常将其保存在创建成本高昂的参数上。
  • @AndreasLarsen:没错,但代表的开销微乎其微。如果日志级别很高,因此不会记录消息,则委托将导致比构建字符串少得多的开销。如果消息被记录,构建字符串加上 I/O 的成本将使委托的开销相形见绌。除此之外,您可以在委托中捕获异常。我曾经让我的日志记录方法接受字符串,以防我记录一个不需要构建的常量字符串,但我发现当签名只允许一个字符串时,很容易忘记将 lambda 版本用于昂贵的字符串。
【解决方案2】:

要接受 StriplingWarrior 的建议而不是委托,您可以使用流畅的语法来完成。

public static class Logger
{
    public static LogFluent Log([CallerMemberName] string sourceMemberName = null)
    {
        return new LogFluent(sourceMemberName);
    }
}

public class LogFluent
{
    private string _callerMemeberName;

    public LogFluent(string callerMamberName)
    {
        _callerMemeberName = callerMamberName;
    }

    public void Message(string message, params object[] messageArgs)
    {

    }
}

然后这样称呼它

Logger.Log().Message("{0}: I canna do it cap'n, the engines can't handle warp {1}!", "Scotty", 10);

Logger 不必是静态的,但它是一种演示概念的简单方法

【讨论】:

  • 我只是在玩弄那个东西……它比 Stripling 的解决方案更冗长,但更容易理解。
【解决方案3】:

好吧,让我提一个选项;您可以使用反射来获得与CallMemberName 完全相同的名称。这肯定会更慢。假设您不是每毫秒都记录一次,我相信这足以应对压力。

var stackTrace = new StackTrace(); var methodName = stackTrace.GetFrame(1).GetMethod().Name;

【讨论】:

  • 这实际上是我们的旧记录器是这样做的,我只是希望速度上的好处。我可以考虑这个选项,因为它在语法上是最干净的。
【解决方案4】:

我喜欢学习 Jim Christophers (@beefarino) 的复数课程来设置我自己的日志记录项目。为此,我将 ILog 接口克隆为 ILogger,我实现了它 - 比如说在一个名为 LoggerAdapter 的类中 - 然后我使用 Jim Christophers LogManager 来拥有一个 GetLogger(Type type) - 方法,该方法返回一个包装的 log4net-logger 'LoggerAdapter':

namespace CommonLogging
{
    public class LogManager : ILogManager
    {
        private static readonly ILogManager _logManager;

        static LogManager()
        {
            log4net.Config.XmlConfigurator.Configure(new FileInfo("log4net.config"));
            _logManager = new LogManager();
        }

        public static ILogger GetLogger<T>()
        {
            return _logManager.GetLogger(typeof(T));
        }

        public ILogger GetLogger(Type type)
        {
            var logger = log4net.LogManager.GetLogger(type);
            return new LoggerAdapter(logger);
        }
    }
}

下一步是像这样创建一个通用扩展,将调用者信息设置为 ThreadContext.Property:

public static class GenericLoggingExtensions
{
    public static ILogger Log<TClass>(this TClass klass, [CallerFilePath] string file = "", [CallerMemberName] string member = "", [CallerLineNumber] int line = 0)
        where TClass : class
    {
        ThreadContext.Properties["caller"] = $"[{file}:{line}({member})]";
        return LogManager.GetLogger<TClass>();
    }
}

有了它,我总是可以在编写实际方法之前调用一个记录器,只需调用:

this.Log().ErrorFormat("message {0} {1} {2} {3} {4}", "a", "b", "c", "d", "e");

如果您将 PatternLayout 的 conversionPattern 配置为使用该属性:

<layout type="log4net.Layout.PatternLayout">
  <conversionPattern value="%utcdate [%thread] %-5level %logger - %message - %property{caller}%newline%exception" />
</layout>

您将始终获得正确的输出,其中包含第一个 .Log() 调用的调用者信息:

2017-03-01 23:52:06,388 [7] ERROR XPerimentsTest.CommonLoggingTests.CommonLoggingTests - message a b c d e - [C:\git\mine\experiments\XPerimentsTest\CommonLoggingTests\CommonLoggingTests.cs:71(Test_Debug_Overrides)]

【讨论】:

    猜你喜欢
    • 2018-01-20
    • 1970-01-01
    • 2011-11-26
    • 1970-01-01
    • 2010-09-17
    • 2013-04-18
    • 1970-01-01
    相关资源
    最近更新 更多