Там приводился сырой вариант, как избавится от строковых констант, при проверке аргументов на null, то есть избавится от хард-кодед констант. Имеем пример:
private static void FillStringBuilderCommon(StringBuilder stringBuilder, String stringArgument)
{
if (stringBuilder == null)
{
throw new ArgumentNullException("stringBuilder"); }
if (stringArgument == null)
{
throw new ArgumentNullException("stringArgument");
}
stringBuilder.Append(stringArgument);
}
Выделенное было давно не по душе, и искал пути, как бы сделать прямое вытягивание имен аргументов. Первоначальную идею задал топик про удобное получение имени свойства. В результате чего было получен следующий Extension method.
static class Extensions
{
public static string GetName<TResult>(this Expression<Func<TResult>> expression)
{
return ((MemberExpression)expression.Body).Member.Name;
}
}
Что привело к следующему варианту функции проверки аргумента на null
public static void NullValidate1<TResult>(Expression<Func<TResult>> argumentExpresion)
where TResult : class
{
if (argumentExpresion.Compile()() == null)
{
throw new ArgumentNullException(argumentExpresion.GetName());
}
}
Жутко медленная вещь получилась, я вам скажу, что естественно меня не утраивало ни коим образом. (на этом месте заканчивается выше указанное обсуждение). Что данная функция позвляет, кратко и понятно записать вызов валидации, например:
Tools.NullValidate1(() => stringBuilder);
Красиво, компактно, но, повторюсь, жутко медленно, а целью было — создание высокопроизводительного инструмента. Дальнейшее изучение данной темы привело к выводу, что одним экспрешеном не обойтись, и функционал проверки аргумента придется делать отдельно и к нему прикреплять собственно сам экспрешшен. В результате получился, следующий вариант:
[DebuggerStepThrough]
public static void NullValidate2<TResult>(Expression<Func<TResult>> argumentExpresion, TResult argument)
where TResult : class
{
if (argument == null)
{
throw new ArgumentNullException(argumentExpresion.GetName());
}
}
Получилось лучше. Стало гораздо быстрее работать, но, забегая вперед, недостаточно быстро. Вызов же стал немногим сложнее. На один аргумент.
Как показали тесты на произволительность, потери были значительными, на порядок. Вся проблема заключалась в первом аргументе. Объяснять "почему?" думаю не надо.
В итоге пришел к заключительному виду, коим пользуюсь сейчас:
[DebuggerStepThrough]
public static void NullValidate<TResult>(TResult argument, Func<Expression<Func<TResult>>> nameRetreiver)
where TResult : class
{
if (argument == null)
{
throw new ArgumentNullException(nameRetreiver().GetName());
}
}
Что-то я не догоняю..... Как я понял, конечная цель — не писать слово "stringArgument" 2 раза (примерно как сделано в C с командой препроцессора ##). В твоем решении это слово все-равно приходится писать 2 раза.
ИМХО правильный подход — это писать что-то типа "Assert.CheckNotNull(expr)" и иметь пост-билд степ, который будет эмитить в бинарник вызов
"Assert.CheckNotNull(expr, "expr", file, linenumber)"
С уважением, Евгений
JetBrains, Inc. "Develop with pleasure!"
Здравствуйте, xvost, Вы писали:
X>Здравствуйте, Uzzy, Вы писали:
X>Что-то я не догоняю..... Как я понял, конечная цель — не писать слово "stringArgument" 2 раза (примерно как сделано в C с командой препроцессора ##). В твоем решении это слово все-равно приходится писать 2 раза.
Нет, конечная цель избавится от строковых константи и ощутимо не потерять производительность.
X>ИМХО правильный подход — это писать что-то типа "Assert.CheckNotNull(expr)" и иметь пост-билд степ, который будет эмитить в бинарник вызов X>"Assert.CheckNotNull(expr, "expr", file, linenumber)"
Вариант. Покурю в эту сторону.
Здравствуйте, Uzzy, Вы писали:
U>Доброго времени суток.
Что-то терзают меня смутные сомнения... Exception — это исключительная ситуация. Какая, к черту, производительность? Если аргумент null, а не должен бы, не все ли равно, как долго будет получаться имя виновного? А вот за идею внешнего метода — спасибо, буду юзать. Но и это имеет ограниченную применимость: в большинстве случаев исключение не имеет конструктора с именем переменной. ЕМНИП, ArgumentOutOfRange такой. В принципе, внешний метод может генерировать целую строку под каждый тип исключений, но все равно как-то...
Ну не знаю. По моему это уже лишнее.
Минусы видны сразу:
1. Снижает читабельность кода по сравнению с обычным if + throw. К вам в проект пришел новый программист. Представьте сколько времени он будет вникать в этот проект, если всё написано в таком стиле.
2. Надо понять, что это за функция, как она вызывается, и главное при написании не запутаться в этих скобках и стрелочках.
3. Даже по вашему тесту скорость падает в 3 раза, хотя действие то примитивно (по сути одна операция всего) — проверка на null, которая при обычных обстоятельствах всегда будет false (то есть переменная не будет равна null). Вы скажете, что падение скорости несущественна, но если все примитивные действия в программе писать так сложно, то скорость работы сильно упадет.
Единственный плюс — избавление от строковой константы. Очень сомнительный плюс. Я даже не могу придумать, чем строка может повредить.
Хотя отдаю вам должное, экстешн метод написан красиво
Здравствуйте, Uzzy, Вы писали:
U>Что привело к следующему варианту функции проверки аргумента на null
U>
U> public static void NullValidate1<TResult>(Expression<Func<TResult>> argumentExpresion)
U> where TResult : class
U> {
U> if (argumentExpresion.Compile()() == null)
U> {
U> throw new ArgumentNullException(argumentExpresion.GetName());
U> }
U> }
U>
U>Жутко медленная вещь получилась, я вам скажу, что естественно меня не утраивало...
Вообще, все это от бедности. Подобные вещи решались бы элементарно и работали бы так же быстро как рукописный код если бы была возможность вмешиваться в процесс компиляции.
Вот здесь
рассказывается о макросе NotNull, входящего в стандартную библиотеку Nemerle, который решает эту проблему самым оптимальным образом.\
Помечаем параметр специальным атрибутом:
public class A
{
public static Method1([NotNull] parameter : string) : void
{
WriteLine(parameter)
}
}
и все! Все остальное делает макрос. При этом есть проверки времени компиляции и генерируемый код оптимален.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
U>Там приводился сырой вариант, как избавится от строковых констант, при проверке аргументов на null, то есть избавится от хард-кодед констант. Имеем пример...
U><skipped>
U>В итоге пришел к заключительному виду, коим пользуюсь сейчас...
Поглядел на Ваш пост и подумал, а как ещё можно узнать имя передаваемого аргумента?
Бросил курсовую и написал вот такой изврат:
static class Arg
{
#region Public Methods
// Не инлайнится, сука такая >:opublic static void NotNullRef<T>(T value) where T : class
{
if (value == null)
{
throw new ArgumentNullException(GetArgumentName());
}
}
// А это инлайнится, но меньше контроля в compile-time:
// можно valuetype передать случайно
// или ref-аргумент (они не поддерживаются) :(public static void NotNull(object value)
{
if (value == null)
{
throw new ArgumentNullException(GetArgumentName());
}
}
#endregion
#region Crazy Implementation
static readonly MethodBase NotNullInfo1 = typeof(Arg).GetMethod("NotNull");
static readonly MethodBase NotNullInfo2 = typeof(Arg).GetMethod("NotNullRef");
const byte Ldarg0 = 0x02;
const byte Ldarg3 = 0x05;
const byte LdargS = 0x0E;
// выглядит как шутка, но всё же...
[MethodImpl(MethodImplOptions.NoInlining)]
private static string GetArgumentName()
{
var stackTrace = new StackTrace(false);
// смотрим кто нас вызвалvar frame = stackTrace.GetFrame(1);
if (frame == null) return null;
var method = frame.GetMethod();
// проверяем, заинлайнило ли вызов NotNullRefif (method == NotNullInfo1 // а вот вдруг?
|| method == NotNullInfo2)
{
frame = stackTrace.GetFrame(2); // нижеif (frame == null) return null;
method = frame.GetMethod();
}
// метод получили, теперь берём его внутренности :)byte[] il;
try// может упасть из-за reflection permission?
{
var methodBody = method.GetMethodBody();
if (methodBody == null) return null;
il = methodBody.GetILAsByteArray();
}
catch (System.Security.SecurityException) { return null; }
var offset = frame.GetILOffset();
var parameters = method.GetParameters();
// теперь исследуем MSIL перед вызовом NotNullRef()
// сдвиг на 4 байта токена метода и 1 байт опкода call
offset -= 5;
Debug.Assert(il[offset] == 0x28); // это точно call
offset--;
if (offset < 0) return null; // на всякий пожарныйstring nameShort = null,
nameLong = null;
// ищем короткие опкоды ldarg.xif (il[offset] >= Ldarg0 &&
il[offset] <= Ldarg3)
{
int id = il[offset] - 2; // порядковый номер аргументаif (!method.IsStatic) id--; // учитываем instance-методы
nameShort = TryGetParameterName(id, parameters);
}
offset--; // ещё левее
// ищем "длинный" вариант - ldarg.S <index>if (offset >= 0 && il[offset] == LdargS)
{
int id = il[offset + 1];
if (!method.IsStatic) id--;
nameLong = TryGetParameterName(id, parameters);
}
// а теперь оцениваем что узналиif (nameShort != null)
{
if (nameLong != null)
{
if (nameLong == nameShort) return nameShort;
// будем уж честными,
// скажем что не знаем какой именно аргумент :)return nameLong + " or " + nameLong;
}
return nameShort;
}
return nameLong;
}
static string TryGetParameterName(int id, ParameterInfo[] parameters)
{
Debug.Assert(parameters != null);
if (id < 0 || id >= parameters.Length) return null;
var type = parameters[id].ParameterType;
// оставляем только ref-типыif (type.IsValueType) return null;
return parameters[id].Name;
}
#endregion
}
Выглядит жестоко, конечно, но оно работает в 90% случаев ))))
Не работает с ref-аргументами и в случаях, когда метод с проверкой заинлайнен — .GetILAsByteArray() возвращает тело метода, в который встроен метод с проверкой
p.s. Я особо не тестировал, может упустил чего.
p.p.s. Сложно по msil перед вызовом судить что там на самом деле... может быть такая ситуация:
[какой-нить опкод][и его аргумент 0x0E][ldarg.2][вызов]
А так как 0x0E — это ещё и код опкода [ldarg.s], то для кода выше эта ситуация выглядит некорректно:
[что-то там][ldarg.s][0x04][вызов]
и вполне может выйти так, что 4ый аргумент существует и ref-типа В этом случае код вернёт ParamName = "realNullArg or someOtherRefArg".
Здравствуйте, VladD2, Вы писали:
VD>Вообще, все это от бедности. Подобные вещи решались бы элементарно и работали бы так же быстро как рукописный код если бы была возможность вмешиваться в процесс компиляции. VD>Вот здесь
рассказывается о макросе NotNull, входящего в стандартную библиотеку Nemerle, который решает эту проблему самым оптимальным образом.\ VD>Помечаем параметр специальным атрибутом: VD>
VD>public class A
VD>{
VD> public static Method1([NotNull] parameter : string) : void
VD> {
VD> WriteLine(parameter)
VD> }
VD>}
VD>
VD>и все! Все остальное делает макрос. При этом есть проверки времени компиляции и генерируемый код оптимален.
Эх, круто А если Assertion-метод, который используется макросом, будет отмечен [ConditionalAttribute("NEVER")], то будет ли устранён его вызов в макросе? Даже если вызов будет устранён, то в макросе останется проверка, можно ли устранить и её (эдакий conditional-макрос)?
Здравствуйте, Аноним, Вы писали:
А>Здравствуйте, Uzzy, Вы писали:
U>>Доброго времени суток.
А>Что-то терзают меня смутные сомнения... Exception — это исключительная ситуация. Какая, к черту, производительность? Если аргумент null, а не должен бы, не все ли равно, как долго будет получаться имя виновного?
вты не понял, Tools.NullValidate1 просаживает производительность в ЛЮБОМ случае. даже если аргумент != null
Здравствуйте, Jack128, Вы писали:
А>>Что-то терзают меня смутные сомнения... Exception — это исключительная ситуация. Какая, к черту, производительность? Если аргумент null, а не должен бы, не все ли равно, как долго будет получаться имя виновного?
J>вты не понял, Tools.NullValidate1 просаживает производительность в ЛЮБОМ случае. даже если аргумент != null
Да, этой глупости я и не понял. Задача ставится как? Избавиться от строковых констант ("Там приводился сырой вариант, как избавится от строковых констант, при проверке аргументов на null, то есть избавится от хард-кодед констант"). Что мешает сделать хотя бы так?
Проверки на null все равно не избежать, а если почки отказали, уже поздно о производительности думать.
Если хочется решить другую задачу (не писать два раза имя переменной), то для этого нужен соответствующий язык, поддерживающий макросы, как правильно указал VladD2. Хотя я предпочитаю препроцессор, генерирующий CS.
Здравствуйте, Пельмешко, Вы писали:
П>А если Assertion-метод, который используется макросом, будет отмечен [ConditionalAttribute("NEVER")], то будет ли устранён его вызов в макросе? Даже если вызов будет устранён, то в макросе останется проверка, можно ли устранить и её (эдакий conditional-макрос)?
Макрос не генерирует никаких вызовов методов. Он просто вставляет проверки, так как-будто их написали по месту.
Но так как это макрос, то конечно можно реализовать его и так чтобы он генерировал вызов метода. Тогда, если у метода задан указанный атрибут, метод вызываться не будет. Но это уже реализуется джитом.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, Пельмешко, Вы писали:
П>(эдакий conditional-макрос)?
Макрос — это код который запускается во время компиляции. Он может все что может обычный код. В том числе из мкроса можно проверить опции компиляции или значение некоторых констант. Так что нет никаких проблем делать работу макросов зависящей от условий.
Я почти уверен, что если бы в языках дотнета исходно поддерживались макросы, то conditional-атрибутов не появилось бы.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Здравствуйте, VladD2, Вы писали:
VD>Здравствуйте, Пельмешко, Вы писали:
П>>А если Assertion-метод, который используется макросом, будет отмечен [ConditionalAttribute("NEVER")], то будет ли устранён его вызов в макросе? Даже если вызов будет устранён, то в макросе останется проверка, можно ли устранить и её (эдакий conditional-макрос)?
VD>Макрос не генерирует никаких вызовов методов. Он просто вставляет проверки, так как-будто их написали по месту.
То есть Assert здесь — это не вызов, а встроенный в Nemerle оператор?
VD>Но так как это макрос, то конечно можно реализовать его и так чтобы он генерировал вызов метода. Тогда, если у метода задан указанный атрибут, метод вызываться не будет. Но это уже реализуется джитом.
Не соглашусь, устранение [Conditional]-вызовов является чисто compile-time преобразованием... Это слишком сложное преобразование для этапа jit'а, да и так поздно его делать нету смысла, так как информации о define'ах вообще нет в сборке.
Я спросил потому что интересно, когда именно компилятор Nemerle устраняет [Conditional]-вызов, после "разворачивания" макросов?
VD>В том числе из макроса можно проверить опции компиляции или значение некоторых констант.
Здравствуйте, _FRED_, Вы писали:
_FR>Здравствуйте, Uzzy, Вы писали:
U>>Данный пост навеян этим сообщением и последующим обсуждением: http://rsdn.ru/forum/dotnet/3590447.1.aspx
Здравствуйте, Аноним, Вы писали:
А>Здравствуйте, Jack128, Вы писали:
А>>>Что-то терзают меня смутные сомнения... Exception — это исключительная ситуация. Какая, к черту, производительность? Если аргумент null, а не должен бы, не все ли равно, как долго будет получаться имя виновного?
J>>вты не понял, Tools.NullValidate1 просаживает производительность в ЛЮБОМ случае. даже если аргумент != null
А>Да, этой глупости я и не понял. Задача ставится как? Избавиться от строковых констант ("Там приводился сырой вариант, как избавится от строковых констант, при проверке аргументов на null, то есть избавится от хард-кодед констант"). Что мешает сделать хотя бы так? А>
Ой. Вру. В статье использовался Trace.Assert(). Просто в реальной библиотеке используется исключение:
[Nemerle.MacroUsage (MacroPhase.WithTypedMembers, MacroTargets.Parameter,
Inherited = true, AllowMultiple = false)]
macro NotNull(_ : TypeBuilder, m : MethodBuilder, p : ParameterBuilder)
{
if (p.ty.CanBeNull)
{
def name = <[ $(p.AsParsed().PName : name) ]>;
def nameLoc = p.NameLocation;
def loc = m.Body.Location;
def paramName = p.Name.ToString();
def msg = $<#The ``NotNull'' contract of parameter ``$paramName'' has been violated. See $nameLoc.#>;
def condition = if (p.ty.Fix().IsValueType) name
else <[ $name : object ]>;
m.Body = <[
when ($condition == null)
{
//System.Diagnostics.Debug.Fail($(msg : string));throw System.ArgumentNullException($(paramName : string), $(msg : string))
}
$(m.Body)
]>;
m.Body.Location = loc;
}
else
Message.Warning(p.Location,
$"The ``NotNull'' contract for parameter ``$(p.Name)'' has no effect. Instance of type ``$(p.ty)'' can't be null reference.");
}
VD>>Но так как это макрос, то конечно можно реализовать его и так чтобы он генерировал вызов метода. Тогда, если у метода задан указанный атрибут, метод вызываться не будет. Но это уже реализуется джитом.
П>Не соглашусь, устранение [Conditional]-вызовов является чисто compile-time преобразованием... Это слишком сложное преобразование для этапа jit'а,
Ну, почему? Джит бы справился бы.
П>да и так поздно его делать нету смысла, так как информации о define'ах вообще нет в сборке.
А вот это — да. Тут ты прав. define-ы — это чисто компайл-тайм сущность, так что действительно ConditionalAttribute видимо обрабатывается компилятором. Не уверен, что немерловый компилятор это делает.
П>Я спросил потому что интересно, когда именно компилятор Nemerle устраняет [Conditional]-вызов, после "разворачивания" макросов?
Не факт, что он их вообще устраняет. Боюсь, что это просто не реализовано. Я думал — это делает джит. Но действительно без дефайнов это не сделать.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
можно сократить запись, если для получения имени использовать не expression trees, а анонимные типы
static class NullValidator<T>
{
private static readonly T Default = (T) FormatterServices.GetSafeUninitializedObject(typeof (T));
public static void Validate(T argument)
{
if (argument.Equals(Default))
{
throw new ArgumentNullException(typeof(T).GetProperties()[0].Name);
}
}
}
public static void NullValidate<TResult>(TResult argument)
where TResult : class
{
NullValidator<TResult>.Validate(argument);
}
// исходный вариант
Tools.NullValidate(stringArgument, () => (() => stringArgument));
Tools.NullValidate(stringBuilder, () => (() => stringBuilder));
// вариант с анонимными типами
Tools.NullValidate(new {stringArgument});
Tools.NullValidate(new {stringBuilder});
A member declarator can be abbreviated to a simple name (§7.5.2) or a member access (§7.5.4). This is called a projection initializer and is shorthand for a declaration of and assignment to a property with the same name. Specifically, member declarators of the forms identifier expr . identifier
are precisely equivalent to the following, respectively: identifer = identifier identifier = expr . identifier