第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 失效,毕竟调用信息是在编译时就决定好的。当然调用方信息也不是完全无法起作用:我们可以将动态类型转换为合适的类型以在编译时确定其信息,或通过一个辅助方法返回调用方信息。

以如下代码为例,它将分别输出行号“ 01718

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。方法有二:

posted @ 2025-04-02 21:44  hihaojie  阅读(34)  评论(0)    收藏  举报