Об атрибуте

В C# 10 конструкция
��ас интересует вызов вышеобъявленной функции. А самое интересное происходит во время компиляции:
Параметр функции
Наиболее интересный сценарий использования этой возможности заключается в проверке аргументов. Раньше для решения той же задачи создавали множество вспомогательных методов:
Этими методами можно было пользоваться так:
Проблема этого кода заключается в необходимости постоянной передачи имени аргумента. Такая однообразная работа сильно надоедает программистам. Есть способы, позволяющие избавиться от ручной передачи аргумента, но у этих способов есть собственные проблемы. Например, можно воспользоваться лямбда-выражением с замыканием:
А эта ��ерсия
Вот мой материал о замыканиях, и о том, как они компилируются в C#.
Лямбда-выражения могут быть, кроме того, скомпилированы в деревья выражений. Поэтому
Эти подходы несут с собой необходимость использования синтаксиса лямбда-выражений и повышенную нагрузку на систему во время выполнения кода. Подобные конструкции, кроме того, чрезвычайно легко «поломать». Теперь же, в C# 10, благодаря
В результате проверка аргументов выполняется с использованием более компактного и быстрого кода:
Имя аргумента генерируется во время компиляции, а во время выполнения кода нет вообще никакой дополнительной нагрузки на систему.
Ещё один интересный сценарий применения новых возможностей открывается в сферах проверки утверждений и логирования:
Если на компьютере установлен .NET 6.0 SDK и доступен C# 10,
Всё это должно быть представлено внутренними механизмами. В результате — когда на эту сборку сошлётся другая сборка, не будет конфликта со встроенной версией
Пользуетесь ли вы CallerArgumentExpression в своих C#-проектах?

CallerArgumentExpression говорят уже много лет. Предполагалось, что он станет частью C# 8.0, но его внедрение в язык отложили. А в этом месяце он, наконец, появился — вместе с C# 10 и .NET 6.
Класс CallerArgumentExpressionAttribute и обработка аргументов во время компиляции кода
В C# 10 конструкция
[CallerArgumentExpression(parameterName)] может быть использована для того, чтобы указать компилятору на необходимость захвата текстового представления указанного аргумента. Например:using System.Runtime.CompilerServices; void Function(int a, TimeSpan b, [CallerArgumentExpression("a")] string c = "", [CallerArgumentExpression("b")] string d = "") { Console.WriteLine($"Called with value {a} from expression '{c}'"); Console.WriteLine($"Called with value {b} from expression '{d}'"); }
��ас интересует вызов вышеобъявленной функции. А самое интересное происходит во время компиляции:
Function(1, default); // Компилируется в: Function(1, default, "1", "default"); int x = 1; TimeSpan y = TimeSpan.Zero; Function(x, y); // Компилируется в: Function(x, y, "x", "y"); Function(int.Parse("2") + 1 + Math.Max(2, 3), TimeSpan.Zero - TimeSpan.MaxValue); // Компилируется в: Function(int.Parse("2") + 1 + Math.Max(2, 3), TimeSpan.Zero - TimeSpan.MaxValue, "int.Parse(\"2\") + 1 + Math.Max(2, 3)", "TimeSpan.Zero - TimeSpan.MaxValue");
Параметр функции
c декорируется с помощью [CallerArgumentExpression(«a»)]. В результате — при вызове функции C#-компилятор возьмёт выражение, переданное в a, и использует текст этого выражения для c. И, аналогично, текст выражения, использованного для b, будет использован для d.Проверка аргументов
Наиболее интересный сценарий использования этой возможности заключается в проверке аргументов. Раньше для решения той же задачи создавали множество вспомогательных методов:
public static partial class Argument { public static void NotNull<T>([NotNull] T? value, string name) where T : class { if (value is null) { throw new ArgumentNullException(name); } } public static void NotNullOrWhiteSpace([NotNull] string? value, string name) { if (string.IsNullOrWhiteSpace(value)) { throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.StringCannotBeEmpty, name)); } } public static void NotNegative(int value, string name) { if (value < 0) { throw new ArgumentOutOfRangeException(name, value, string.Format(CultureInfo.CurrentCulture, Resources.ArgumentCannotBeNegative, name)); } } }
Этими методами можно было пользоваться так:
public partial record Person { public Person(string name, int age, Uri link) { Argument.NotNullOrWhiteSpace(name, nameof(name)); Argument.NotNegative(age, nameof(age)); Argument.NotNull(link, nameof(link)); this.Name = name; this.Age = age; this.Link = link.ToString(); } public string Name { get; } public int Age { get; } public string Link { get; } }
Проблема этого кода заключается в необходимости постоянной передачи имени аргумента. Такая однообразная работа сильно надоедает программистам. Есть способы, позволяющие избавиться от ручной передачи аргумента, но у этих способов есть собственные проблемы. Например, можно воспользоваться лямбда-выражением с замыканием:
public partial record Person { public Person(Uri link) { Argument.NotNull(() => link); this.Link = link.ToString(); } }
А эта ��ерсия
NotNull может принимать функцию:public static partial class Argument { public static void NotNull<T>(Func<T> value) { if (value() is null) { throw new ArgumentNullException(GetName(value)); } } private static string GetName<T>(Func<T> func) { // func: () => arg компилируется в DisplayClass с полем и методом. Метод - это func. object displayClassInstance = func.Target!; FieldInfo closure = displayClassInstance.GetType() .GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance) .Single(); return closure.Name; } }
Вот мой материал о замыканиях, и о том, как они компилируются в C#.
Лямбда-выражения могут быть, кроме того, скомпилированы в деревья выражений. Поэтому
NotNull можно реализовать и в расчёте на то, чтобы эта функция принимала бы выражение (вот мой материал о деревьях выражений и об их компиляции в C#):public static partial class Argument { public static void NotNull<T>(Expression<Func<T>> value) { if (value.Compile().Invoke() is null) { throw new ArgumentNullException(GetName(value)); } } private static string GetName<T>(Expression<Func<T>> expression) { // expression: () => arg компилируется в DisplayClass с полем. Здесь тело выражения нужно для организации доступа к полю экземпляра DisplayClass. MemberExpression displayClassInstance = (MemberExpression)expression.Body; MemberInfo closure = displayClassInstance.Member; return closure.Name; } }
Эти подходы несут с собой необходимость использования синтаксиса лямбда-выражений и повышенную нагрузку на систему во время выполнения кода. Подобные конструкции, кроме того, чрезвычайно легко «поломать». Теперь же, в C# 10, благодаря
CallerArgumentExpression, наконец появилось более приличное решение задачи проверки аргументов:public static partial class Argument { public static T NotNull<T>([NotNull] this T? value, [CallerArgumentExpression("value")] string name = "") where T : class => value is null ? throw new ArgumentNullException(name) : value; public static string NotNullOrWhiteSpace([NotNull] this string? value, [CallerArgumentExpression("value")] string name = "") => string.IsNullOrWhiteSpace(value) ? throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.StringCannotBeEmpty, name), name) : value; public static int NotNegative(this int value, [CallerArgumentExpression("value")] string name = "") => value < 0 ? throw new ArgumentOutOfRangeException(name, value, string.Format(CultureInfo.CurrentCulture, Resources.ArgumentCannotBeNegative, name)) : value; }
В результате проверка аргументов выполняется с использованием более компактного и быстрого кода:
public record Person { public Person(string name, int age, Uri link) => (this.Name, this.Age, this.Link) = (name.NotNullOrWhiteSpace(), age.NotNegative(), link.NotNull().ToString()); // Компилируется в: // this.Name = Argument.NotNullOrWhiteSpace(name, "name"); // this.Age = Argument.NotNegative(age, "age"); // this.Link = Argument.NotNull(link, "link").ToString(); public string Name { get; } public int Age { get; } public string Link { get; } }
Имя аргумента генерируется во время компиляции, а во время выполнения кода нет вообще никакой дополнительной нагрузки на систему.
Проверка утверждений и логирование
Ещё один интересный сценарий применения новых возможностей открывается в сферах проверки утверждений и логирования:
[Conditional("DEBUG")] static void Assert(bool condition, [CallerArgumentExpression("condition")] string expression = "") { if (!condition) { Environment.FailFast($"'{expression}' is false and should be true."); } } Assert(y > TimeSpan.Zero); // Компилируется в: Assert(y > TimeSpan.Zero, "y > TimeSpan.Zero"); [Conditional("DEBUG")] static void Log<T>(T value, [CallerArgumentExpression("value")] string expression = "") { Trace.WriteLine($"'{expression}' has value '{value}'"); } Log(Math.Min(Environment.ProcessorCount, x)); // Компилируется в: Log(Math.Min(Environment.ProcessorCount, x), "Math.Min(Environment.ProcessorCount, x)");
Использование новых возможностей в старых проектах
Если на компьютере установлен .NET 6.0 SDK и доступен C# 10,
CallerArgumentExpression можно пользоваться в проектах, нацеленных на .NET 5 и .NET 6. В более старых .NET- или .NET Standard-проектах CallerArgumentExpressionAttribute недоступен. Но даже в таких проектах, при условии установленного .NET 6.0 SDK, можно воспользоваться этой возможностью. Достаточно вручную добавить в проект класс CallerArgumentExpressionAttribute и применить его как встроенный атрибут:#if !NET5_0 && !NET6_0 namespace System.Runtime.CompilerServices; /// <summary> /// Позволяет захватывать выражения, переданные методу. /// </summary> [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] internal sealed class CallerArgumentExpressionAttribute : Attribute { /// <summary> /// Инициализирует новый экземпляр <see cref="T:System.Runtime.CompilerServices.CallerArgumentExpressionAttribute" /> class. /// </summary> /// <param name="parameterName">Имя целевого параметра.</param> public CallerArgumentExpressionAttribute(string parameterName) => this.ParameterName = parameterName; /// <summary> /// Получает имя целевого параметра <c>CallerArgumentExpression</c>. /// </summary> /// <returns> /// Имя целевого параметра <c>CallerArgumentExpression</c>. /// </returns> public string ParameterName { get; } } #endif
Всё это должно быть представлено внутренними механизмами. В результате — когда на эту сборку сошлётся другая сборка, не будет конфликта со встроенной версией
[CallerArgumentExpression]. А компилятор C# 10 сам всё поймёт, после чего поведёт себя так, как мы уже видели в самом первом примере.Пользуетесь ли вы CallerArgumentExpression в своих C#-проектах?

