简体   繁体   中英

Optional parameters together with an array of parameters

I have a logging interface which I extend with some helpful extension methods as to make it possible for me to pass in a format and a list of arguments to avoid having to use string format every time a call the method. (it also help me follow FXCops culture info rules)

So I can call:

logger.Debug("Created {0} with id {1}",typeof(MyObject).Name ,myObject.Id);

Instead of:

logger.Debug(string.Format("Created {0} with id {1}", typeof(MyObject).Name, myObject.Id));

I now found myself in a bit of a tricky situation because it would be immensely helpful to also get some info in the logs about where the logging was written such as the file, method, and line number. This could be achieved with the neat [CallerMemberName] , [CallerFilePath] , and [CallerLineNumber] attribute.

logger.Debug("Created {0} with id {1}", typeof(MyObject).Name, myObject.Id);

would then give me a log entry such as:

"MyObjectProvider.cs, Provide, line:50 | Created MyObject with id 1564"

The issue here is that the method signature would look like this:

public static void Debug(this ILogger logger, string format [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0, params object[] args)

and that is not possible because the [Caller*] attributes makes the parameters optional and that doesn't work with the args parameter.

I also tried to make multiple implementations with fixed amount of strings as parameters like this:

public static void Debug(this ILogger logger, string format [CallerMemberName] string callerMemberName = "",string arg, string arg2 , ...etc... , [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0)

but then I get compiler errors saying the "The call is ambiguous between the following methods or properties"

I have almost given up on this issue now but I thought to myself, "Maybe SO can find a solution for me". So here it is... Is it possible to use both params object[] args and [CallerFilePath] in any way or is there another way to get the intended result?

You can't combine the two in a method signature. What you could do is one or the other and pass in null to where you need optional parameters, would this work for you?

Foo(s, null);
public void Foo(string s, params string[] sArray)
{

}

Foo(new string[] {""});
private static void Foo(string[] sArray,  string s = "")
{
}

OR

Why not use a class which handles your formatting and make that optional?

public class LogArgs
{
  private string _formatString;
  private string[] _args;
  public LogArgs(string formatString, params string[] args)
  {
    _formatString = formatString;
    _args = args;
  }
  public override string ToString()
  {
    return string.Format(_formatString, _args);
  }
}

public void Foo(string mandatory, LogArgs optionalParam = null)
{
  //Do Stuff
}

Foo("", new LogArgs("{0} is formatted", ""));

I found another way to get the information I want by using StackTrace. It's a bit unsafe in optimized code and it's very slow but for debuging purposes it works great as long as it's possible to shut it off in release builds.

StackTrace stackTrace = new StackTrace();
var callerMember = stackTrace.GetFrame(1).GetMethod();
var callerMemberName = callerMember.Name;
var callerType = callerMember.ReflectedType.Name;

I ran into the same issue, but solved it a little differently. It's not the most elegant solution, but it works and it's relatively clean:

public class SrcLoc
{
    public string sourceFile { get; set; }
    public int lineNumber { get; set; }
    public SrcLoc([CallerFilePath] string sourceFile = "",
                  [CallerLineNumber] int lineNumber = 0)
    {
      this.sourceFile = sourceFile;
      this.lineNumber = lineNumber;
    }
}
public class Logger
{
   public void Log(SrcLoc location,
                int level = 1,
                string formatString = "",
                params object[] parameters)
  {
     string message = String.Format(formatString, parameters);
  }
}
public MainTest
{
    public static void Main()
    {
        string file="filename";
        logger.Log(new SrcLoc(), (int)LogLevel.Debug, "My File: {0}", file);
    }
}

The most elegant way (or least inelegant way) I've found is to create a method with the required name that extracts the attribute information and returns an Action delegate. You then setup this delegate with the signature that you actually want to call.

So, from

public static void Debug(this ILogger logger, string format, [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0, params object[] args)

create a delegate

public delegate void LogDelegate(string format, params object[] args);

which is returned from your method call:

public static void Debug(this ILogger logger, [CallerMemberName] string callerMemberName = "", [CallerFilePath] string callerFilePath = "", [CallerLineNumber] int callerLineNumber = 0)
{
  return (format, args)
  {
    LogWithCallerSiteInfo(format, args, callerMemberName, callerFilePath, callerLineNumber, logAction);
  }
}

and calls a helper method with the captured data:

private static void LogWithCallerSiteInfo(string format, object[] args, string callerMemberName, string callerFilePath, int callerLineNumber, Action<string, object[]> logRequest)
    {
        if (args == null)
        {
            args = new object[0];
        }
        var args2 = new object[args.Length + 3];
        args.CopyTo(args2, 0);
        args2[args.Length] = sourceFile;
        args2[args.Length + 1] = memberName;
        args2[args.Length + 2] = lineNumber;

        logRequest(format + " [{callerFilePath:l}.{callerMemberName:l}-{callerLineNumber}]", args2);
    }

And make the call, thus:

logger.Debug()("Created {0} with id {1}",typeof(MyObject).Name ,myObject.Id);

So, in usage terms, you insert an extra set of () , which captures the call-site info and the set set makes the call on the delegate. That's as neat as I've managed to make it.

I've recreated the params array adding in the captured data, otherwise, (at least with SeriLog , the results are unpredictable.

将所有默认参数移动到右侧。

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM