Mixing optional parameters and params when can'

2020-08-10 07:16发布

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:

    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.

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条回答
爱情/是我丢掉的垃圾
2楼-- · 2020-08-10 07:46

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.

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

查看更多
forever°为你锁心
3楼-- · 2020-08-10 07:56

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.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.

查看更多
姐就是有狂的资本
4楼-- · 2020-08-10 08:02

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.

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.

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.

查看更多
仙女界的扛把子
5楼-- · 2020-08-10 08:03

Way 1.

I You can use StackFrame instead of 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"));
}
查看更多
干净又极端
6楼-- · 2020-08-10 08:08

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

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);
查看更多
登录 后发表回答