Как использовать ключевое слово params вместе с информацией о вызывающем абоненте в C#?
Я пытаюсь объединить информацию о вызывающей стороне C# 5.0 вместе с ключевым словом C# params. Намерение состоит в том, чтобы создать оболочку для каркаса журналирования, и мы хотим, чтобы регистратор форматировал текст как String.Format. В предыдущих версиях метод выглядел так:
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 ответа
Я не думаю, что это можно сделать именно так, как вы хотите. Однако я мог бы подумать о нескольких жизнеспособных обходных решениях, которые, вероятно, дали бы вам почти такие же преимущества.
Используйте промежуточный вызов метода для захвата имени члена вызывающей стороны. Первый вызов метода возвращает делегата, который, в свою очередь, может быть вызван для предоставления дополнительных параметров. Это выглядит странно, но должно работать:
log.Log()("{0}: I canna do it cap'n, the engines can't handle warp {1}!", "Scotty", warpFactor);
Одним из недостатков здесь является то, что можно позвонить
log.Log("something")
, ожидая, что ваше сообщение будет зарегистрировано, и ничего не произойдет. Если вы используете Resharper, вы можете смягчить это, добавив[Pure]
приписать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);
Создайте свои сообщения журнала с лямбдами и позвольте string.Format позаботиться о массиве params:
log.Log(() => string.Format("{0}: I canna do it cap'n, the engines can't handle warp {1}!", "Scotty", warpFactor));
Это подход, который я обычно использую, и он имеет некоторые побочные преимущества:
- Ваш метод журнала может перехватывать исключения, возникающие при создании строки отладки, поэтому вместо взлома системы вы просто получите сообщение об ошибке: "Не удалось создать сообщение журнала: [подробности об исключении]".
Иногда объект, который вы передаете в строку формата, может повлечь дополнительные расходы, которые вы захотите понести только тогда, когда вам это нужно:
log.Info(() => string.Format("{0}: I canna do it cap'n, the engines can't handle warp {1}!", _db.GetCurrentUsername(), warpFactor));
Вы бы предпочли, чтобы вышеприведенный код не выполнял отключение базы данных, если ведение журнала на информационном уровне не включено.
В качестве примечания я использую string.Format достаточно часто, чтобы создать вспомогательный метод для небольшого сокращения синтаксиса:
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);
Регистратор не должен быть статичным, но это был простой способ продемонстрировать концепцию
Хорошо, позвольте мне упомянуть один вариант; Вы можете использовать отражение, чтобы получить точно такое же имя, как CallMemberName
, Это будет определенно медленнее. Полагаю, что вы не регистрируете каждую миллисекунду, этого будет достаточно, чтобы справиться с давлением.
var stackTrace = new StackTrace();
var methodName = stackTrace.GetFrame(1).GetMethod().Name;
Я предпочитаю проходить курс множественного взгляда Джима Кристофера (@beefarino), чтобы настроить свои собственные проекты ведения журналов. Для этого я клонирую интерфейс ILog как ILogger, я реализую это, скажем, в классе с именем LoggerAdapter, а затем я использую Jim Christophers LogManager, чтобы получить GetLogger(Type type)-Method, который возвращает упакованный 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.Property:
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>
у вас всегда будет правильный вывод с информацией о вызове первого.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)]