注释
本文是功能规格说明。 此规范是功能的设计文档。 它包括建议的规范变更,以及功能设计和开发过程中所需的信息。 这些文章将持续发布,直至建议的规范变更最终确定并纳入当前的 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" 断言消息有助于开发人员确定失败的内容。 Enter 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中引用的参数关联的字符串。 当任一断言失败时,开发人员将显示为 false 的条件,并知道哪一个失败。
对于参数验证,该属性不能直接使用,但可以通过帮助程序类使用:
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只采用 APIbool的 API。 即使采用消息的重载具有使用此属性标记的第二个参数,并且是可选的,编译器仍会选取重载解析中的无消息参数。 因此,必须删除无消息重载才能利用此功能,这是二进制(尽管不是源)中断性变更。
替代方案
- 如果能够在调用站点查看使用此属性的方法的源代码被证明是一个问题,我们可以使属性的效果选择加入。 开发人员将通过它们放入
[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
此方法有一些缺点:
尽管通过允许你指定所需的属性来为游戏付费友好,但它仍然可以通过为表达式/调用
MethodBase.GetCurrentMethod分配数组来显著伤害性能,即使断言传递也是如此。此外,虽然将新标志传递给
CallerInfo属性不会是中断性变更,Debug.Assert但不能保证实际从针对旧版方法编译的调用站点接收该新参数。
未解决的问题
待定
设计会议
无