CallerArgumentExpression
Заметка
Эта статья является спецификацией компонентов. Спецификация служит проектным документом для функции. Она включает предлагаемые изменения спецификации, а также информацию, необходимую во время проектирования и разработки функции. Эти статьи публикуются до тех пор, пока предложенные изменения спецификации не будут завершены и включены в текущую спецификацию ECMA.
Может возникнуть некоторое несоответствие между спецификацией компонентов и завершенной реализацией. Эти различия фиксируются в соответствующих собраниях по проектированию языка (LDM).
Дополнительные сведения о процессе внедрения спецификаций функций в стандарт языка C# см. в статье о спецификациях .
Вопрос чемпиона: https://github.com/dotnet/csharplang/issues/287
Сводка
Разрешите разработчикам записывать выражения, передаваемые методу, чтобы улучшить сообщения об ошибках в API диагностики и тестирования и сократить нажатия клавиш.
Мотивация
Если утверждение или проверка аргумента завершается ошибкой, разработчик хочет знать как можно больше о том, где и почему он завершился ошибкой. Однако сегодня api диагностики не полностью упрощают эту задачу. Рассмотрим следующий метод:
T Single<T>(this T[] array)
{
Debug.Assert(array != null);
Debug.Assert(array.Length == 1);
return array[0];
}
Если одно из утверждений завершается ошибкой, в трассировке стека будет указано только имя файла, номер строки и имя метода. Разработчик не сможет определить, какое утверждение завершилось ошибкой из этой информации — им придется открыть файл и перейти к указанному конкретному номеру строки, чтобы увидеть, что пошло не так.
Это также причина, по которой платформы тестирования должны предоставлять различные методы утверждения. При использовании xUnit Assert.True
и Assert.False
часто не используются, так как они не предоставляют достаточно контекста о том, что завершилось сбоем.
Хотя ситуация немного лучше для проверки аргументов, так как имена недопустимых аргументов отображаются разработчику, разработчик должен передать эти имена исключениям вручную. Если приведенный выше пример был перезаписан для использования традиционной проверки аргументов вместо Debug.Assert
, он будет выглядеть следующим образом.
T Single<T>(this T[] array)
{
if (array == null)
{
throw new ArgumentNullException(nameof(array));
}
if (array.Length != 1)
{
throw new ArgumentException("Array must contain a single element.", nameof(array));
}
return array[0];
}
Обратите внимание, что nameof(array)
необходимо передать каждому исключению, хотя из контекста уже очевидно, какой аргумент недопустим.
Подробный дизайн
В приведенных выше примерах добавление строки "array != null"
или "array.Length == 1"
в сообщение проверки assert помогло бы разработчику определить, что именно не удалось. Введите CallerArgumentExpression
: это атрибут, который платформа может использовать для получения строки, связанной с определенным аргументом метода. Мы добавим его в Debug.Assert
вот так
public static class Debug
{
public static void Assert(bool condition, [CallerArgumentExpression("condition")] string message = null);
}
Исходный код в приведенном выше примере будет оставаться неизменным. Однако код, который компилятор фактически выдает, будет соответствовать
T Single<T>(this T[] array)
{
Debug.Assert(array != null, "array != null");
Debug.Assert(array.Length == 1, "array.Length == 1");
return array[0];
}
Компилятор специально распознает атрибут в Debug.Assert
. Он передает строку, связанную с аргументом, на который ссылается в конструкторе атрибута (в данном случае condition
) в месте вызова. При сбое любого утверждения разработчик будет отображать условие, которое было ложным и будет знать, какой из них произошел сбой.
Для проверки аргументов атрибут нельзя использовать напрямую, но его можно использовать с помощью вспомогательного класса:
public static class Verify
{
public static void Argument(bool condition, string message, [CallerArgumentExpression("condition")] string conditionExpression = null)
{
if (!condition) throw new ArgumentException(message: message, paramName: conditionExpression);
}
public static void InRange(int argument, int low, int high,
[CallerArgumentExpression("argument")] string argumentExpression = null,
[CallerArgumentExpression("low")] string lowExpression = null,
[CallerArgumentExpression("high")] string highExpression = null)
{
if (argument < low)
{
throw new ArgumentOutOfRangeException(paramName: argumentExpression,
message: $"{argumentExpression} ({argument}) cannot be less than {lowExpression} ({low}).");
}
if (argument > high)
{
throw new ArgumentOutOfRangeException(paramName: argumentExpression,
message: $"{argumentExpression} ({argument}) cannot be greater than {highExpression} ({high}).");
}
}
public static void NotNull<T>(T argument, [CallerArgumentExpression("argument")] string argumentExpression = null)
where T : class
{
if (argument == null) throw new ArgumentNullException(paramName: argumentExpression);
}
}
static T Single<T>(this T[] array)
{
Verify.NotNull(array); // paramName: "array"
Verify.Argument(array.Length == 1, "Array must contain a single element."); // paramName: "array.Length == 1"
return array[0];
}
static T ElementAt<T>(this T[] array, int index)
{
Verify.NotNull(array); // paramName: "array"
// paramName: "index"
// message: "index (-1) cannot be less than 0 (0).", or
// "index (6) cannot be greater than array.Length - 1 (5)."
Verify.InRange(index, 0, array.Length - 1);
return array[index];
}
Предложение добавить такой вспомогательный класс в платформу ведется на https://github.com/dotnet/corefx/issues/17068. Если эта функция языка реализована, можно обновить предложение, чтобы воспользоваться этой функцией.
Методы расширения
Параметр this
в методе расширения может ссылаться на CallerArgumentExpression
. Например:
public static void ShouldBe<T>(this T @this, T expected, [CallerArgumentExpression("this")] string thisExpression = null) {}
contestant.Points.ShouldBe(1337); // thisExpression: "contestant.Points"
thisExpression
получит выражение, соответствующее объекту перед точкой. Если он вызывается с синтаксисом статических методов, например Ext.ShouldBe(contestant.Points, 1337)
, он будет вести себя так, как если бы первый параметр не был помечен this
.
Всегда должно быть выражение, соответствующее параметру this
. Даже если экземпляр класса вызывает сам метод расширения, например this.Single()
из типа коллекции, this
требуется компилятором, поэтому "this"
будет передано. Если это правило изменится в будущем, мы можем рассмотреть возможность передачи null
или пустой строки.
Дополнительные сведения
- Как и другие атрибуты
Caller*
, напримерCallerMemberName
, этот атрибут может использоваться только для параметров со значениями по умолчанию. - Разрешены несколько параметров, помеченных
CallerArgumentExpression
, как показано выше. - Пространство имен атрибута будет
System.Runtime.CompilerServices
. - Если указана
null
или строка, которая не является именем параметра (например,"notAParameterName"
), компилятор будет передавать пустую строку. - Тип, к которому применяется параметр
CallerArgumentExpressionAttribute
, должен иметь стандартное преобразование изstring
. Это означает, что пользовательские преобразования изstring
не разрешены, и на практике тип такого параметра должен бытьstring
,object
или интерфейс, реализованныйstring
.
Недостатки
Пользователи, которые знают, как использовать декомпилаторы, смогут видеть некоторые из исходного кода на сайтах вызовов для методов, помеченных этим атрибутом. Это может быть нежелательно или неожиданно для программного обеспечения с закрытым исходным кодом.
Хотя это не недостаток самой функции, источником беспокойства может быть то, что сегодня существует
Debug.Assert
API, который принимает толькоbool
. Даже если перегрузка, принимающая сообщение, имеет второй параметр, помеченный этим атрибутом и сделанный необязательным, компилятор все равно выберет перегрузку без сообщения при разрешении перегрузки. Таким образом, для использования этой функции необходимо удалить перегрузку без сообщений, что приведет к изменению, нарушающему двоичную, но не исходную совместимость.
Альтернативы
- Если возможность видеть исходный код в местах вызова для методов, использующих этот атрибут, вызывает проблему, мы можем сделать эффекты атрибута по желанию. Разработчики будут активировать его с помощью атрибута
[assembly: EnableCallerArgumentExpression]
на уровне сборки, который они добавят вAssemblyInfo.cs
.- В случае, если эффекты атрибута не включены, вызов методов, помеченных атрибутом, не будет ошибкой, чтобы разрешить существующим методам использовать атрибут и поддерживать совместимость источников. Однако атрибут будет игнорироваться, и метод будет вызываться с любым значением по умолчанию.
// Assembly1
void Foo(string bar); // V1
void Foo(string bar, string barExpression = "not provided"); // V2
void Foo(string bar, [CallerArgumentExpression("bar")] string barExpression = "not provided"); // V3
// Assembly2
Foo(a); // V1: Compiles to Foo(a), V2, V3: Compiles to Foo(a, "not provided")
Foo(a, "provided"); // V2, V3: Compiles to Foo(a, "provided")
// Assembly3
[assembly: EnableCallerArgumentExpression]
Foo(a); // V1: Compiles to Foo(a), V2: Compiles to Foo(a, "not provided"), V3: Compiles to Foo(a, "a")
Foo(a, "provided"); // V2, V3: Compiles to Foo(a, "provided")
- Чтобы избежать проблемы двоичной совместимости при каждом добавлении новой информации о вызывающем в
Debug.Assert
, альтернативным решением может стать добавление структурыCallerInfo
в фреймворк, содержащей все необходимые сведения о вызывающем.
struct CallerInfo
{
public string MemberName { get; set; }
public string TypeName { get; set; }
public string Namespace { get; set; }
public string FullTypeName { get; set; }
public string FilePath { get; set; }
public int LineNumber { get; set; }
public int ColumnNumber { get; set; }
public Type Type { get; set; }
public MethodBase Method { get; set; }
public string[] ArgumentExpressions { get; set; }
}
[Flags]
enum CallerInfoOptions
{
MemberName = 1, TypeName = 2, ...
}
public static class Debug
{
public static void Assert(bool condition,
// If a flag is not set here, the corresponding CallerInfo member is not populated by the caller, so it's
// pay-for-play friendly.
[CallerInfo(CallerInfoOptions.FilePath | CallerInfoOptions.Method | CallerInfoOptions.ArgumentExpressions)] CallerInfo callerInfo = default(CallerInfo))
{
string filePath = callerInfo.FilePath;
MethodBase method = callerInfo.Method;
string conditionExpression = callerInfo.ArgumentExpressions[0];
//...
}
}
class Bar
{
void Foo()
{
Debug.Assert(false);
// Translates to:
var callerInfo = new CallerInfo();
callerInfo.FilePath = @"C:\Bar.cs";
callerInfo.Method = MethodBase.GetCurrentMethod();
callerInfo.ArgumentExpressions = new string[] { "false" };
Debug.Assert(false, callerInfo);
}
}
Первоначально это было предложено в https://github.com/dotnet/csharplang/issues/87.
Существует несколько недостатков этого подхода:
Несмотря на то, что вы платите за игру, позволяя указать необходимые свойства, он по-прежнему может значительно повредить perf путем выделения массива для выражений или вызовов
MethodBase.GetCurrentMethod
даже при прохождении утверждения.Кроме того, передача нового флага в атрибут
CallerInfo
не приведет к нарушению совместимости, но дляDebug.Assert
не гарантируется получение этого нового параметра из мест вызова, скомпилированных с использованием старой версии метода.
Неразрешенные вопросы
подлежит уточнению
Планирование встреч
N/A
C# feature specifications