Как использовать ключевое слово params вместе с информацией о вызывающем абоненте в C#?

Я пытаюсь объединить информацию о вызывающем абоненте C# 5.0 вместе с ключевым словом c# params. Намерение состоит в том, чтобы создать оболочку для структуры ведения журнала, и мы хотим, чтобы регистратор форматировал текст как строку.Формат. В предыдущих версиях метод выглядел следующим образом:

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);

есть ли способ сделать это? Похоже, что "хакерский" способ передачи информации о вызывающем абоненте исключает использование ключевого слова params. Было бы здорово, если бы компилятор C# признал, что параметры информации вызывающего абонента не действительно параметры на всех. Я не вижу необходимости передавать их в явном виде.

моя резервная копия будет пропускать ключевое слово params, и вызывающий должен будет использовать длинную подпись в этом последнем примере.

4 ответов


Я не думаю, что это можно сделать именно так, как вы хотите это сделать. Тем не менее, я мог бы придумать несколько жизнеспособных обходных путей, которые, вероятно, дадут вам почти те же преимущества.

  1. используйте промежуточный вызов метода для захвата имени вызывающего элемента. Первый вызов метода возвращает делегат, который, в свою очередь, может быть вызван для предоставления дополнительных параметров. Это выглядит странно, но это должно работать:

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

    один недостаток здесь в том, что можно позвонить log.Log("something"), ожидая, что ваше сообщение будет зарегистрировано, и ничего не произойдет. Если вы используете Resharper, вы можете смягчить это, добавив до 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. создайте свои сообщения журнала с lambdas, и пусть строка.Формат позаботьтесь о 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));
      

      вы бы предпочли, чтобы приведенный выше код не выполнял поездку по базе данных, Если ведение журнала на уровне информации не включено.

    в качестве примечания я использую строку.Форматируйте достаточно часто, чтобы я создал вспомогательный метод, чтобы немного сократить синтаксис:

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

чтобы согласиться с предложением 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 не должен быть статическим, но это был простой способ продемонстрировать концепцию


Ну, позвольте мне упомянуть один вариант, вы можете использовать отражение, чтобы получить точно такое же имя, как CallMemberName. Это будет определенно медленнее. Полагаю, если вы не будете регистрировать каждую миллисекунду, этого будет достаточно, чтобы справиться с давлением.

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


мне нравится следовать курсу Джима Кристофера (@beefarino) pluralsight для настройки моих собственных проектов ведения журнала. Для этого я клонирую интерфейс ILog как ILogger, я реализую это - скажем, в классе LoggerAdapter - а затем я использую Jim Christophers LogManager, чтобы иметь GetLogger (тип типа)-метод, который возвращает завернутый 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.Свойство:

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");

и если у вас есть conversionPattern вашего PatternLayout настроен на использование свойства:

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

у вас всегда будет правильный вывод с callerinformation первого .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)]