当不能简单地过载时,混合可选参数和参数

时间:2022-08-10 10:51:22

Similar to this question, I want to mix optional parameters with the params keyword, which of course creates ambiguity. Unfortunately, the answer of creating overloads does not work, as I want to take advantage of caller info attributes, like this:

与此问题类似,我想将可选参数与params关键字混合,这当然会产生歧义。不幸的是,创建重载的答案不起作用,因为我想利用调用者信息属性,如下所示:

    public void Info(string message, [CallerMemberName] string memberName = "", 
                     [CallerLineNumber] int lineNumber = 0, params object[] args)
    {
        _log.Info(BuildMessage(message, memberName, lineNumber), args);
    }

Creating an overload without the optional parameters would change the call-site, preventing these particular parameters from working properly.

在没有可选参数的情况下创建重载会更改调用站点,从而阻止这些特定参数正常工作。

I found a solution that almost works (though it's ugly):

我找到了一个几乎可以工作的解决方案(尽管很难看):

    public void Info(string message, object arg0, [CallerMemberName] string memberName = "",
                     [CallerLineNumber] int lineNumber = 0)
    {
        _log.Info(BuildMessage(message, memberName, lineNumber), arg0);
    }

    public void Info(string message, object arg0, object arg1, [CallerMemberName] string memberName = "",
                     [CallerLineNumber] int lineNumber = 0)
    {
        _log.Info(BuildMessage(message, memberName, lineNumber), arg0, arg1);
    }

The problem here is that if you specify a string for the last argument, the overload resolution assumes you're intending to explicitly specify memberName in the overload that takes fewer arguments, which is not the desired behavior.

这里的问题是,如果为最后一个参数指定一个字符串,则重载决策假定您打算在重载中显式指定memberName,该参数占用较少的参数,这不是所需的行为。

Is there some way to accomplish this (perhaps using some new attributes I haven't learned about?) or have we simply reached the limits of what the auto-magical compiler support can give us?

有没有办法实现这一点(也许使用一些我没有学过的新属性?)或者我们是否只是达到了自动魔法编译器支持可以给我们的极限?

5 个解决方案

#1


16  

My prefered way: Only two charachters overhead - ugly language 'hack' though;

我最喜欢的方式:开头只有两个字符 - 丑陋的语言'黑客';

public delegate void WriteDelegate(string message, params object[] args);

public static WriteDelegate Info(
      [CallerMemberName] string memberName = "", 
      [CallerLineNumber] int lineNumber = 0)
 {
     return new WriteDelegate ((message,args)=>
     {
         _log.Info(BuildMessage(message, memberName , lineNumber ), args);
     });
 }

Usage (supply your own implementation of BuildMessage

用法(提供您自己的BuildMessage实现

Info()("hello world {0} {1} {2}",1,2,3);

Alternative

The way my collegue came up to make this work was like this:

我的同事出现这种方式的方式是这样的:

public static class DebugHelper

    public static Tuple<string,int> GetCallerInfo(
      [CallerMemberName] string memberName = "", 
      [CallerLineNumber] int lineNumber = 0)
    {
        return Tuple.Create(memberName,lineNumber);
    }
}

The InfoMethod:

public void Info(Tuple<string,int> info, string message, params object[] args)
{
      _log.Info(BuildMessage(message, info.Item1, info.Item2), args);
}

usage:

  instance.Info(DebugHelper.GetCallerInfo(),"This is some test {0} {1} {2}",1,2,3);

#2


7  

So, I actually ran into this problem but for a different reason. Eventually I solved it like this.

所以,我实际上遇到了这个问题,但原因不同。最终我像这样解决了它。

First, overload resolution in C# (generic methods are ideal candidates). I used T4 to generate these extension method overloads with support for up to 9 arguments. Here is an example with just 3 arguments.

首先,C#中的重载决策(通用方法是理想的候选者)。我使用T4生成这些扩展方法重载,最多支持9个参数。这是一个只有3个参数的例子。

public static void WriteFormat<T1, T2, T3>(this ILogTag tag, string format, T1 arg0, T2 arg1, T3 arg2
    , [CallerMemberName] string callerMemberName = null, [CallerFilePath] string callerFilePath = null, [CallerLineNumber] int callerLineNumber = 0
    )
{
    if (tag != null)
    {
        var entry = new LogEntry(DateTimeOffset.Now, tag.TagName, new LogString(format, new object[] { arg0, arg1, arg2 }), callerMemberName, System.IO.Path.GetFileName(callerFilePath), callerLineNumber);
        tag.Write(entry);
    }
}

Which works fine for a while but eventually results in an ambiguity when you use any combination of arguments that match the caller info attribute list. To prevent this from happening you need a type to guard the optional parameter list and separate it from the optional parameter list.

哪个工作正常一段时间但最终导致使用与调用者信息属性列表匹配的任何参数组合时的歧义。为了防止这种情况发生,您需要一个类型来保护可选参数列表并将其与可选参数列表分开。

An empty struct will do just fine (I use long and descriptive names for such things).

一个空结构就可以了(我为这些东西使用长而具有描述性的名称)。

/// <summary>
/// The purpose of this type is to act as a guard between 
/// the actual parameter list and optional parameter list.
/// If you need to pass this type as an argument you are using
/// the wrong overload.
/// </summary>
public struct LogWithOptionalParameterList
{
    // This type has no other purpose.
}

NOTE: I thought about making this an abstract class with a private constructor but that would actually allow null to be passed as the LogWithOptionalParameterList type. A struct does not have this problem.

注意:我考虑使用私有构造函数使其成为一个抽象类,但实际上允许将null作为LogWithOptionalParameterList类型传递。结构没有这个问题。

Insert this type between the actual parameter list and the optional parameter list.

在实际参数列表和可选参数列表之间插入此类型。

public static void WriteFormat<T1, T2, T3>(this ILogTag tag, string format, T1 arg0, T2 arg1, T3 arg2
    , LogWithOptionalParameterList _ = default(LogWithOptionalParameterList)
    , [CallerMemberName] string callerMemberName = null, [CallerFilePath] string callerFilePath = null, [CallerLineNumber] int callerLineNumber = 0
    )
{
    if (tag != null)
    {
        var entry = new LogEntry(DateTimeOffset.Now, tag.TagName, new LogString(format, new object[] { arg0, arg1, arg2 }), callerMemberName, System.IO.Path.GetFileName(callerFilePath), callerLineNumber);
        tag.Write(entry);
    }
}

Voilà!

The only purpose this type has is to mess with the overload resolution procedure but it will also result in a compiler error if you accidently fill-in the caller info attribute values (that the compiler should have provided) when your methods take additional parameters I had some such calls that resulted in compiler errors right away.

这种类型的唯一目的是搞乱重载解析过程,但是当你的方法采用我有的其他参数时,如果你意外填写调用者信息属性值(编译器应该提供),它也会导致编译器错误一些此类调用立即导致编译器错误。

#3


3  

Based on the answers others provided, I can see that they are largely based on capturing the context first, then invoking the logging method with the captured context. I came up with this:

基于其他人提供的答案,我可以看到它们主要基于首先捕获上下文,然后使用捕获的上下文调用日志记录方法。我想出了这个:

    public CallerContext Info([CallerMemberName] string memberName = "", [CallerLineNumber] int lineNumber = 0)
    {
        return new CallerContext(_log, LogLevel.Info, memberName, lineNumber);
    }

    public struct CallerContext
    {
        private readonly Logger _logger;
        private readonly LogLevel _level;
        private readonly string _memberName;
        private readonly int _lineNumber;

        public CallerContext(Logger logger, LogLevel level, string memberName, int lineNumber)
        {
            _logger = logger;
            _level = level;
            _memberName = memberName;
            _lineNumber = lineNumber;
        }

        public void Log(string message, params object[] args)
        {
            _logger.Log(_level, BuildMessage(message, _memberName, _lineNumber), args);
        }

        private static string BuildMessage(string message, string memberName, int lineNumber)
        {
            return memberName + ":" + lineNumber + "|" + message;
        }
    }

If you have a LoggerProxy (class defining method Info()) named Log, the usage is like this:

如果你有一个名为Log的LoggerProxy(类定义方法Info()),用法如下:

Log.Info().Log("My Message: {0}", arg);

The syntax seems slightly cleaner to me (duplicate Log is still ugly, but so it goes) and I think using a struct for the context may make it slightly better as far as performance, though I'd have to profile to be sure.

语法似乎对我来说稍微清晰一点(重复的Log仍然很难看,但事实如此)并且我认为在上下文中使用结构可能会使性能稍微好一点,尽管我必须要进行分析才能确定。

#4


2  

If you make your format parameters optional in your "Ugly solution" you do not need speacial overload for each number of parameters but only one is enough for all! e.g:

如果你在“丑陋的解决方案”中使你的格式参数成为可选的,你不需要为每个参数提供特殊的重载,但只有一个对所有参数都足够了!例如:

public void Info(string message, object arg0=null, object arg1=null,
[CallerMemberName] string memberName = "",[CallerLineNumber] int lineNumber = 0)
{
    _log.Info(BuildMessage(message, memberName, lineNumber), arg0, arg1);
}

then you can call it with up to three parameters i.e.

然后你可以用最多三个参数调用它,即

Info("No params");
Info("One param{0}",1);
Info("Two param {0}-{1}",1,2);

You can easily minimize the risk of accidentally filling CallerMemberName and CallerLineNumber by adding much more optional formating arguments than you will ever need e.g. arg0, ... arg20.

您可以通过添加比您需要的更多可选格式化参数,轻松地将意外填充CallerMemberName和CallerLineNumber的风险降至最低。 arg0,... arg20。

or you can combine it with John Leidegren solution i.e adding guarging parameter.... between argsX and last two params...

或者你可以将它与John Leidegren解决方案结合起来,即在argsX和最后两个参数之间添加guarging参数....

#5


1  

Way 1.

I You can use StackFrame instead of CallerLineNumber:

我可以使用StackFrame而不是CallerLineNumber:

public void Info(string message, params object[] args)
{
  StackFrame callStack = new StackFrame(1, true);
  string memberName = callStack.GetMethod().Name;
  int lineNumber = callStack.GetFileLineNumber();
  _log.Info(BuildMessage(message, memberName, lineNumber), args);
}

Useful documentation pages:

有用的文档页面:

Way 2.

public class InfoMessage
{
  public string Message { get; private set; }
  public string MemberName { get; private set; }
  public int LineNumber { get; private set; }

  public InfoMessage(string message,
                     [CallerMemberName] string memberName = "", 
                     [CallerLineNumber] int lineNumber = 0)
  {
    Message = message;
    MemberName = memberName;
    LineNumber = lineNumber;
  }
}

public void Info(InfoMessage infoMessage, params object[] args)
{ 
  _log.Info(BuildMessage(infoMessage), args);
}

public string BuildMessage(InfoMessage infoMessage)
{
  return BuildMessage(infoMessage.Message, 
    infoMessage.MemberName, infoMessage.LineNumber);
}

void Main()
{
  Info(new InfoMessage("Hello"));
}

#1


16  

My prefered way: Only two charachters overhead - ugly language 'hack' though;

我最喜欢的方式:开头只有两个字符 - 丑陋的语言'黑客';

public delegate void WriteDelegate(string message, params object[] args);

public static WriteDelegate Info(
      [CallerMemberName] string memberName = "", 
      [CallerLineNumber] int lineNumber = 0)
 {
     return new WriteDelegate ((message,args)=>
     {
         _log.Info(BuildMessage(message, memberName , lineNumber ), args);
     });
 }

Usage (supply your own implementation of BuildMessage

用法(提供您自己的BuildMessage实现

Info()("hello world {0} {1} {2}",1,2,3);

Alternative

The way my collegue came up to make this work was like this:

我的同事出现这种方式的方式是这样的:

public static class DebugHelper

    public static Tuple<string,int> GetCallerInfo(
      [CallerMemberName] string memberName = "", 
      [CallerLineNumber] int lineNumber = 0)
    {
        return Tuple.Create(memberName,lineNumber);
    }
}

The InfoMethod:

public void Info(Tuple<string,int> info, string message, params object[] args)
{
      _log.Info(BuildMessage(message, info.Item1, info.Item2), args);
}

usage:

  instance.Info(DebugHelper.GetCallerInfo(),"This is some test {0} {1} {2}",1,2,3);

#2


7  

So, I actually ran into this problem but for a different reason. Eventually I solved it like this.

所以,我实际上遇到了这个问题,但原因不同。最终我像这样解决了它。

First, overload resolution in C# (generic methods are ideal candidates). I used T4 to generate these extension method overloads with support for up to 9 arguments. Here is an example with just 3 arguments.

首先,C#中的重载决策(通用方法是理想的候选者)。我使用T4生成这些扩展方法重载,最多支持9个参数。这是一个只有3个参数的例子。

public static void WriteFormat<T1, T2, T3>(this ILogTag tag, string format, T1 arg0, T2 arg1, T3 arg2
    , [CallerMemberName] string callerMemberName = null, [CallerFilePath] string callerFilePath = null, [CallerLineNumber] int callerLineNumber = 0
    )
{
    if (tag != null)
    {
        var entry = new LogEntry(DateTimeOffset.Now, tag.TagName, new LogString(format, new object[] { arg0, arg1, arg2 }), callerMemberName, System.IO.Path.GetFileName(callerFilePath), callerLineNumber);
        tag.Write(entry);
    }
}

Which works fine for a while but eventually results in an ambiguity when you use any combination of arguments that match the caller info attribute list. To prevent this from happening you need a type to guard the optional parameter list and separate it from the optional parameter list.

哪个工作正常一段时间但最终导致使用与调用者信息属性列表匹配的任何参数组合时的歧义。为了防止这种情况发生,您需要一个类型来保护可选参数列表并将其与可选参数列表分开。

An empty struct will do just fine (I use long and descriptive names for such things).

一个空结构就可以了(我为这些东西使用长而具有描述性的名称)。

/// <summary>
/// The purpose of this type is to act as a guard between 
/// the actual parameter list and optional parameter list.
/// If you need to pass this type as an argument you are using
/// the wrong overload.
/// </summary>
public struct LogWithOptionalParameterList
{
    // This type has no other purpose.
}

NOTE: I thought about making this an abstract class with a private constructor but that would actually allow null to be passed as the LogWithOptionalParameterList type. A struct does not have this problem.

注意:我考虑使用私有构造函数使其成为一个抽象类,但实际上允许将null作为LogWithOptionalParameterList类型传递。结构没有这个问题。

Insert this type between the actual parameter list and the optional parameter list.

在实际参数列表和可选参数列表之间插入此类型。

public static void WriteFormat<T1, T2, T3>(this ILogTag tag, string format, T1 arg0, T2 arg1, T3 arg2
    , LogWithOptionalParameterList _ = default(LogWithOptionalParameterList)
    , [CallerMemberName] string callerMemberName = null, [CallerFilePath] string callerFilePath = null, [CallerLineNumber] int callerLineNumber = 0
    )
{
    if (tag != null)
    {
        var entry = new LogEntry(DateTimeOffset.Now, tag.TagName, new LogString(format, new object[] { arg0, arg1, arg2 }), callerMemberName, System.IO.Path.GetFileName(callerFilePath), callerLineNumber);
        tag.Write(entry);
    }
}

Voilà!

The only purpose this type has is to mess with the overload resolution procedure but it will also result in a compiler error if you accidently fill-in the caller info attribute values (that the compiler should have provided) when your methods take additional parameters I had some such calls that resulted in compiler errors right away.

这种类型的唯一目的是搞乱重载解析过程,但是当你的方法采用我有的其他参数时,如果你意外填写调用者信息属性值(编译器应该提供),它也会导致编译器错误一些此类调用立即导致编译器错误。

#3


3  

Based on the answers others provided, I can see that they are largely based on capturing the context first, then invoking the logging method with the captured context. I came up with this:

基于其他人提供的答案,我可以看到它们主要基于首先捕获上下文,然后使用捕获的上下文调用日志记录方法。我想出了这个:

    public CallerContext Info([CallerMemberName] string memberName = "", [CallerLineNumber] int lineNumber = 0)
    {
        return new CallerContext(_log, LogLevel.Info, memberName, lineNumber);
    }

    public struct CallerContext
    {
        private readonly Logger _logger;
        private readonly LogLevel _level;
        private readonly string _memberName;
        private readonly int _lineNumber;

        public CallerContext(Logger logger, LogLevel level, string memberName, int lineNumber)
        {
            _logger = logger;
            _level = level;
            _memberName = memberName;
            _lineNumber = lineNumber;
        }

        public void Log(string message, params object[] args)
        {
            _logger.Log(_level, BuildMessage(message, _memberName, _lineNumber), args);
        }

        private static string BuildMessage(string message, string memberName, int lineNumber)
        {
            return memberName + ":" + lineNumber + "|" + message;
        }
    }

If you have a LoggerProxy (class defining method Info()) named Log, the usage is like this:

如果你有一个名为Log的LoggerProxy(类定义方法Info()),用法如下:

Log.Info().Log("My Message: {0}", arg);

The syntax seems slightly cleaner to me (duplicate Log is still ugly, but so it goes) and I think using a struct for the context may make it slightly better as far as performance, though I'd have to profile to be sure.

语法似乎对我来说稍微清晰一点(重复的Log仍然很难看,但事实如此)并且我认为在上下文中使用结构可能会使性能稍微好一点,尽管我必须要进行分析才能确定。

#4


2  

If you make your format parameters optional in your "Ugly solution" you do not need speacial overload for each number of parameters but only one is enough for all! e.g:

如果你在“丑陋的解决方案”中使你的格式参数成为可选的,你不需要为每个参数提供特殊的重载,但只有一个对所有参数都足够了!例如:

public void Info(string message, object arg0=null, object arg1=null,
[CallerMemberName] string memberName = "",[CallerLineNumber] int lineNumber = 0)
{
    _log.Info(BuildMessage(message, memberName, lineNumber), arg0, arg1);
}

then you can call it with up to three parameters i.e.

然后你可以用最多三个参数调用它,即

Info("No params");
Info("One param{0}",1);
Info("Two param {0}-{1}",1,2);

You can easily minimize the risk of accidentally filling CallerMemberName and CallerLineNumber by adding much more optional formating arguments than you will ever need e.g. arg0, ... arg20.

您可以通过添加比您需要的更多可选格式化参数,轻松地将意外填充CallerMemberName和CallerLineNumber的风险降至最低。 arg0,... arg20。

or you can combine it with John Leidegren solution i.e adding guarging parameter.... between argsX and last two params...

或者你可以将它与John Leidegren解决方案结合起来,即在argsX和最后两个参数之间添加guarging参数....

#5


1  

Way 1.

I You can use StackFrame instead of CallerLineNumber:

我可以使用StackFrame而不是CallerLineNumber:

public void Info(string message, params object[] args)
{
  StackFrame callStack = new StackFrame(1, true);
  string memberName = callStack.GetMethod().Name;
  int lineNumber = callStack.GetFileLineNumber();
  _log.Info(BuildMessage(message, memberName, lineNumber), args);
}

Useful documentation pages:

有用的文档页面:

Way 2.

public class InfoMessage
{
  public string Message { get; private set; }
  public string MemberName { get; private set; }
  public int LineNumber { get; private set; }

  public InfoMessage(string message,
                     [CallerMemberName] string memberName = "", 
                     [CallerLineNumber] int lineNumber = 0)
  {
    Message = message;
    MemberName = memberName;
    LineNumber = lineNumber;
  }
}

public void Info(InfoMessage infoMessage, params object[] args)
{ 
  _log.Info(BuildMessage(infoMessage), args);
}

public string BuildMessage(InfoMessage infoMessage)
{
  return BuildMessage(infoMessage.Message, 
    infoMessage.MemberName, infoMessage.LineNumber);
}

void Main()
{
  Info(new InfoMessage("Hello"));
}