Mixing optional parameters and params when can't simply overload
Asked Answered
A

5

18

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?

Appease answered 24/7, 2014 at 14:31 Comment(4)
We tried to make this work for the exact same reasons and failed.Soria
Considering that this is the main use-case for CallerInfo, this attribute solution is just horrible.Pasquale
All you need is an additional type (and some generic methods). I've provided a hopefully satisfactory answer to this problem.Hypaesthesia
For future reference to anyone finding this question, interpolated strings resolve this problem nicely and I'd highly recommend using them over various hacky params approaches. Best feature ever.Appease
S
18

My prefered way: Only two characters 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);
Soria answered 24/7, 2014 at 15:12 Comment(2)
This inspired the approach I wound up taking, so choosing this as the answer.Appease
I like to use a ThreadContext-Property to forward the CallerInformation to my patternlayout like I described here: linkSkiffle
H
9

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.

Hypaesthesia answered 6/11, 2014 at 16:46 Comment(3)
Very clever little trick to prevent the overload resolution issues. Would definitely need some good comments in the code to help prevent future confusion. :)Appease
With regards to comments, I did leave a comment on the LogFormatWithOptionalParameterList type. Hopefully anyone ever running into that compiler error would immediately check the comment on that type. I have updated my answer with the actual comment.Hypaesthesia
Had the same problem, came up with a similar solution, a "stopper" optional parameter, using an uninstantiable class. I'm all for descriptive names as well, but mine (thisStrangeParamPreventsAccidentalCallsToOverloadsSuchThatCallerInfoGetsSet) definitely needs some trimming before release. Btw. I don't care that my uninstantiable class could be provided as null. The problem is preventing caller info args from siphoning off arguments from unsuspecting callers.Nor do I in fact care that it is uninstantiable in the first place, for the same reason. And: Thanks for the this ILogTag trickCowes
A
4

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.

Appease answered 24/7, 2014 at 15:50 Comment(0)
C
2

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"));
}
Conducive answered 24/7, 2014 at 14:54 Comment(3)
Be aware that this has a performancedrawback, as the creation of the callstack is way more expensive compared to the constant values of those fancy attributes.Soria
@CSharpie, I added way 2.Conducive
Also: This issue provides non-useful information in async methods.Marciamarciano
H
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.

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

Higherup answered 19/8, 2016 at 11:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.