第7章 C#5 附加特性
第7章 C#5 附加特性
7.1 在 foreach 循环中捕获变量
在 C#5 之前,根据语言规范中对 foreach 循环的描述,每个 foreach 循环都只会声明一个迭代变量,该变量在原始代码中是只读的,但之后每次迭代都会赋一个新值。即如下两段代码等价:
foreach (string name in names)
{
Console.WriteLine(name);
}
string name; // 声明单个迭代变量
using (var iterator = names.GetEnumerator()) // 不可见的迭代变量
{
while (iterator.MoveNext())
{
name = iterator.Current; // 每次迭代将新值赋给迭代变量
Console.WriteLine(name); // 原始的 foreach 循环体
}
}
这样的设计自 C#2 引入匿名方法(同时引入了闭包)后出了问题:迭代变量可以被捕获,变量的生命周期发生了颠覆性变化。
以如下代码为例,在 C#5 之前它将输出“ z z z ”:
List<string> names = new List<string> { "x", "y", "z" };
var actions = new List<Action>();
foreach (string name in names)
{
actions.Add(() => Console.WriteLine(name));
}
foreach (Action action in actions)
{
action();
}
在 C#5,语言规范修正了关于 foreach 循环的表述,这样每次循环都会引入新的变量。上述代码将输出“ x y z ”。
Notice
这项修正只影响 foreach 循环。普通的 for 循环被捕获的依然是一个变量。
7.2 调用方信息 attribute
7.2.1 基本行为
.NET 4.5 引入了 3 个新的 attribute,都只能应用于方法形参,而且只有在特定类型的可选形参中才能发挥作用。以调用方没有提供实参为前提,这 3 个特性有:
-
CallerFilePathAttribute
:使用当前 文件 作为实参 -
CallerLineNumberAttribute
:使用当前 行数 作为实参 -
CallerMemberNameAttribute
:使用当前 成员名 作为实参
用例如下:
static void ShowInfo(
[CallerFilePath] string file = null,
[CallerLineNumber] int line = 0,
[CallerMemberName] string member = null)
{
Console.WriteLine("{0}:{1} - {2}", file, line, member);
}
static void Main()
{
ShowInfo();
ShowInfo("LiesAndDamnedLies.java", -10);
}
一般来说,不应为此类实参使用虚构值,参数名通常需要与其含义相对应。
上述特性最常用的场景有二,接下来依次介绍。
7.2.2 日志
调用方信息的常见使用场景是记录日志文件。在该特性出现之前,当需要记录日志时,一般要构建一个调用栈(例如使用 System.Diagnostics.StackTrace
)来找出调用方信息。这些信息一般隐藏在日志框架背后,方法虽不甚优雅,但终归可用。此外,还可能造成潜在的性能问题,对于内联的 JIT 编译来说也是十分脆弱的。
日志框架使用新特性来更方便地记录调用方信息。即便删除了调试信息和进行代码混淆之后,依然能够将行号以成员名保留下来。虽然这项特性不能用来记录整个调用栈信息,但是我们可以通过其他方式实现这样的需求。
7.2.3 简化 INotifyPropertyChanged
的实现
该接口常见于富客户端的使用。该接口包含一个事件,该事件的 PropertyChangedEventArgs
类型参数需要调用成员的名称作为入参。我们一般这样使用:
class OldPropertyNotifier : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int firstValue;
public int FirstValue
{
get { return firstValue; }
set
{
if (value != firstValue)
{
firstValue = value;
NotifyPropertyChanged("FirstValue");
}
}
}
// (相同模式的其他属性)
private void NotifyPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
有了调用方信息 attribute 后,这段代码可以简化成如下形式:
if (value != firstValue)
{
firstValue = value;
NotifyPropertyChanged();
}
private void NotifyPropertyChanged([CallerMemberName]string propertyName = null)
{
}
7.2.4 调用方信息 attribute 的小众使用场景
7.2.4.1 动态调用成员
动态类型会使调用方信息 attribute 失效,毕竟调用信息是在编译时就决定好的。当然调用方信息也不是完全无法起作用:我们可以将动态类型转换为合适的类型以在编译时确定其信息,或通过一个辅助方法返回调用方信息。
以如下代码为例,它将分别输出行号“ 0 、 17 、 18 ”
static void ShowLine(string message, //
[CallerLineNumber] int line = 0) //
{ // 即将调用的、使用
Console.WriteLine("{0}: {1}", line, message); // 行号的方法
} //
static int GetLineNumber( //
[CallerLineNumber] int line = 0) //
{ // 第 2 种迂回方案的
return line; // 帮助方法
} //
static void Main()
{
dynamic message = "Some message";
ShowLine(message); // 简单的动态调用
ShowLine((string) message); // 第 1 种迂回方案:使用类型转换消除动态类型
ShowLine(message, GetLineNumber()); // 第 2 种迂回方案:使用帮助方法显式地提供行号信息
}
7.2.4.2 非“显著”成员名称
当调用方是一个方法,该方法由编译器提供名称,那么这是一个“显著”名称,即方法的名称。以下方法调用方名称分别为:
-
实例构造器中调用: .ctor
-
静态构造器中调用: .cctor
-
终结器中调用: Finalize
-
运算符中调用: op_Operator (如 op_Addition)
-
字段、事件或属性初始化器的部分中调用: 成员名称
-
索引器中调用: Item
前提是未附加
IndexerNameAttribute
特性
下面是我测试上述方法使用的 Demo:
Demo demo = new Demo();
Demo result = demo + demo;
var value = demo.IntValue;
value = demo[0];
class Demo
{
private int _intValue = GetMemberName();
public int IntValue
{
get
{
return GetMemberName();
}
}
[System.Runtime.CompilerServices.IndexerName("DemoIndexer")]
public int this[int index] => GetMemberName();
static Demo()
{
GetMemberName();
}
public Demo()
{
GetMemberName();
}
~Demo()
{
GetMemberName();
}
public static Demo operator + (Demo left, Demo right)
{
GetMemberName();
return null;
}
private static int GetMemberName([CallerMemberName] string memberName = null)
{
memberName.Dump();
return default;
}
}
7.2.4.3 隐式构造器调用
C#5 的语言规范中要求,只有当函数在源码中被显式调用时才能使用调用方信息(扩展方法除外)。尤其是对于构造器初始化器。以如下代码为例,Derived1
、Derived2
都是 隐 式调用的 BaseClass
的构造函数,它们都无法正确输出调用方信息;只有 Derived3
可以正确输出:
Derived1 value1 = new Derived1();
Derived2 value2 = new Derived2();
Derived3 value3 = new Derived3();
public abstract class BaseClass
{
protected BaseClass(
[CallerFilePath] string file = "Unspecified file",
[CallerLineNumber] int line = -1,
[CallerMemberName] string member = "Unspecified member")
{
Console.WriteLine("{0}:{1} - {2}", file, line, member);
}
}
public class Derived1 : BaseClass { }
public class Derived2 : BaseClass
{
public Derived2() { }
}
public class Derived3 : BaseClass
{
public Derived3() : base() { }
}
Info
作者认为这是一个设计缺陷。经测试,截至到 C#13、.NET9,Roslyn 编译器仍遵循上述规范。作者提出 Mono 编译器则不然,三种方式都可以正确输出调用方信息。
7.2.4.4 查询表达式的调用
查询表达式虽然是隐式调用的扩展方法,编译器仍然会提供调用方信息。
以下是随书资料中的 LINQ 查询代码,它支持获取调用方信息:
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
namespace Chapter07
{
// Note: this code uses C# 6 features for brevity.
public class CallerInfo
{
public string File { get; }
public int Line { get; }
public string Member { get; }
public CallerInfo(string file, int line, string member)
{
File = file;
Line = line;
Member = member;
}
public override string ToString() => $"{Path.GetFileName(File)}:{Line} - {Member}";
}
public static class CallerInfoEnumerableExtensions
{
public static CallerInfoEnumerable<T> Where<T>(
this IEnumerable<T> source,
Func<T, bool> predicate,
[CallerFilePath] string file = "Unspecified file",
[CallerLineNumber] int line = -1,
[CallerMemberName] string member = "Unspecified member") =>
CombineInfo(source, Enumerable.Where(source, predicate), file, line, member);
public static CallerInfoEnumerable<TResult> Select<TSource, TResult>(
this IEnumerable<TSource> source,
Func<TSource, TResult> selector,
[CallerFilePath] string file = "Unspecified file",
[CallerLineNumber] int line = -1,
[CallerMemberName] string member = "Unspecified member") =>
CombineInfo(source, Enumerable.Select(source, selector), file, line, member);
private static CallerInfoEnumerable<TResult> CombineInfo<TOriginal, TResult>(
IEnumerable<TOriginal> original,
IEnumerable<TResult> result,
string file,
int line,
string member)
{
var previousInfo =
original is CallerInfoEnumerable<TOriginal> infoOriginal
? infoOriginal.CallerInfo
: ImmutableList.Create<CallerInfo>();
var info = previousInfo.Add(new CallerInfo(file, line, member));
return new CallerInfoEnumerable<TResult>(result, info);
}
}
public class CallerInfoEnumerable<T> : IEnumerable<T>
{
public IEnumerable<T> Data { get; }
public ImmutableList<CallerInfo> CallerInfo { get; }
internal CallerInfoEnumerable(IEnumerable<T> data, ImmutableList<CallerInfo> callerInfo)
{
Data = data;
CallerInfo = callerInfo;
}
public IEnumerator<T> GetEnumerator() => Data.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
[Description("Listing 7.8")]
class CallerInfoLinq
{
static void Main()
{
string[] source =
{
"the", "quick", "brown", "fox",
"jumped", "over", "the", "lazy", "dog"
};
var query = from word in source
where word.Length > 3
select word.ToUpperInvariant();
Console.WriteLine("Data:");
Console.WriteLine(string.Join(", ", query));
Console.WriteLine("CallerInfo:");
Console.WriteLine(
string.Join(Environment.NewLine, query.CallerInfo));
}
}
}
7.2.4.5 使用调用方信息 attribute 的 attribute
在 Attribute 中使用调用方信息,会发生什么?下面我们进行测试。这里已经定义好了一个使用调用方信息的特性:
[AttributeUsage(AttributeTargets.All)]
public class MemberDescriptionAttribute : Attribute
{
public MemberDescriptionAttribute(
[CallerFilePath] string file = "Unspecified file",
[CallerLineNumber] int line = 0,
[CallerMemberName] string member = "Unspecified member")
{
File = file;
Line = line;
Member = member;
}
public string File { get; }
public int Line { get; }
public string Member { get; }
public override string ToString() =>
$"{Path.GetFileName(File)}:{Line} - {Member}";
}
我们分别应用在类型、方法、类型形参、方法形参上:
[MemberDescription]
class CallerNameInAttribute
{
[MemberDescription]
public void Method<[MemberDescription] T>(
[MemberDescription] int parameter)
{ }
static void Main()
{
var typeInfo = typeof(CallerNameInAttribute).GetTypeInfo();
var methodInfo = typeInfo.GetDeclaredMethod("Method");
var paramInfo = methodInfo.GetParameters()[0];
var typeParamInfo =
methodInfo.GetGenericArguments()[0].GetTypeInfo();
Console.WriteLine(typeInfo.GetCustomAttribute<MemberDescriptionAttribute>());
Console.WriteLine(methodInfo.GetCustomAttribute<MemberDescriptionAttribute>());
Console.WriteLine(paramInfo.GetCustomAttribute<MemberDescriptionAttribute>());
Console.WriteLine(typeParamInfo.GetCustomAttribute<MemberDescriptionAttribute>());
}
}
执行的结果如下:
CallerNameInAttribute.cs:22 - Unspecified member
CallerNameInAttribute.cs:25 - Method
CallerNameInAttribute.cs:27 - Method
CallerNameInAttribute.cs:26 - Method
可以看到,调用方信息记录的是 Attribute 所在 cs 路径、Attribute 使用时的行号、Attribute 所应用的成员。
Info
这部分内容仅作了解即可。
7.2.5 旧版本 .NET 使用调用方信息 attribute
在 .NET4.5、.NET Standard1.0 之前的版本中,尚未纳入调用方信息 attribute。不过这不意味着不能使用。我们只需要编译器能认出这些 attribute。方法有二:
- 使用 NuGet 库 |Microsoft.Bcl 1.1.10 Nuget 包
- 自定义这些 attribute,令其位于 System.Runtime.CompilerServices 命名空间下