c# 高级编程 16章327页 【反射、元数据、动态编程】

227 阅读5分钟

用户自定义特性

用户自定义特性,把 自定义元数据程序元素 关联起来

反射

在运行过程中,检查和处理 程序元素。例如:

  • 枚举 类型的 成员
  • 实例化 新对象
  • 执行 对象的 成员
  • 查找 类型的 信息
  • 查找 程序集的 信息
  • 检查 应用于某种类型的 自定义特性
  • 创建和编译 新程序集

用户自定义特性

.NET Framework 中定义的特性,编译器 能够 以特殊方式 定制 编译过程

  • 例如:可以根据StructLayout特性中的信息,在内存中 布置结构

用户自定义的特性,不能够 影响 编译过程。

  • 因为 编译器 不能识别 用户自定义特性
  • 用户自定义特性, 应用于 程序元素时,可以在 编译好的程序集中 用作元数据
  • 这些元数据 在文档说明中 非常有用
  • 但 使用户自定义特性 非常强大的,是使用反射
  • 代码可以 读取这些元数据使用它们 在运行期间 做出决策
  • 用户自定义特性 可以直接影响代码运行的方式。例如:
    • 对自定义许可类 进行 声明性的 代码访问 安全检查
    • 测试工具 使用 程序元素, 用户自定义特性 把信息与程序元素 关联起来
    • 开发 可扩展的架构 时,允许加载 插件或模块

当编译器 遇上 用户自定义特性

[FieldName("SocialSecurityNumber")]
public string SocialSecurityNumber
{
    get {
        //...
    }
}
  1. C# 编译器 发现 程序元素 应用了一个特性XX时,会把Attribute追加到 这个特性名称后面。e.g. XXAttribute
    • 如果特性XX已经以Attribute结尾了,那么就 不再追加Attribute
  2. 搜索 using语句提及的 所有命名空间,找 名为 XXAttribute的特性类
    • 这个类 派生自System.Attribute
    • 这个类 定义了 如何使用特性:
      • 可以用到 哪些 程序元素上? (类、结构、属性、方法 等)
      • 是否 可以多次应用到 同一个 程序元素
      • 应用到 类或接口 上时,是否由 派生类和接口 继承
      • 有哪些 必选参数
      • 有哪些 可选参数
    • 如果找不到类,或者使用特性的方式与特性类中的信息不匹配,会产生一个编译错误

如何定义 特性类

特性类 本身 用一个特性 System.AttributeUsage 来标记。

1. AttributeUsage

  • 是一个 Microsoft 定义的特性
  • C# 编译器为它提供了特殊的支持
  • 它更像一个元特性
  • 它能用到其他特性上,但不能应用到类上
[AttributeUsage(AttributeTargets.Property, AllowMultiple=false, Inherited=false)]
public class FieldNameAttribute : Attribute
{
    private string _name;
    public FieldNameAttribute(string name)
    {
        _name = name;
    }
}

AttributeTargets 枚举 (必选参数)

  • All: 应用到所有类型的程序元素上
  • Assembly
  • Class
  • Constructor
  • Delegate
  • Enum
  • Event
  • Field
  • GenericParameter
  • Interface
  • Method
  • Module
  • Parameter
  • Property
  • Return Value
  • Struct

特性 放置在?

  • 一般,将特性 应用到 程序元素上时,应把 特性 放在 元素前面的方括号中
  • 例外是,对于Assembly(程序集)和Module(模块),特性可以放在 源代码的任何地方
[assembly:SomeAssemblyAttribute(Parameters)]
[module:SomeAssemblyAttribute(Parameters)]

特性 应用到 多种程序元素上

用OR运算符 将这些 枚举值 组合起来

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple=false, Inherited=false)]
public class FieldNameAttribute : Attribute

AllowMultiple (可选参数)

一个特性是否可以多次应用到同一项上。false时,如下会报编译错误

[FieldName("SocialSecurityNumber")]
[FieldName("NationalInsuranceNumber")]
public string SocialSecurityNumber

Inherited

类或接口 上的特性,可以 自动应用到 所有 派生类或接口

方法或属性 上的特性,可以 自动应用到 该方法或属性 的重写版本

2. 如何定义 自定义特性 接收的参数

编译器 会检查 传递给特性的 参数,并在特性类中 寻找 带有这些参数的 构造函数

  • 如果找不到这样的构造函数,就抛出一个编译错误
  • 因为:
    • 反射 会从程序集中 读取 元数据(特性)
    • 并实例化 它们 表示的 特性类
    • 编译器 需要 确保 存在 这样的构造函数
    • 才能在 运行期间 实例化 特性
  • 可以 提供 构造函数 的 不同重载方法
  • 一般只提供一个 构造函数,然后使用 属性 来定义 其他任何 可选参数

3. 用(AttributeUsage + 公共属性/公共字段) 定义 可选参数

AttributeUsage 有另外一种语法,可以 给特性 传递 可选参数

//定义特性类, 如下:
[AttributeUsage(AttributeTargets.Property, AllowMultiple=false, Inherited=false)]
public class FieldNameAttribute : Attribute
{
    public string Comment { get; set; }
    private string _name;
    public FieldNameAttribute(string name)
    {
        _name = name;
    }
}
// 使用特性
[FieldName("SocialSecurityNumber", Comment="This is the primary key field")]
public string SocialSecurityNumber { get; set; }
  • 编译器 识别 第二个参数 的语法 <ParameterName>=<ParameterValue>
  • 这个参数 ParameterName 不会被传递给构造函数
  • 而是查找名为 ParameterName公共属性 或 公共字段
    • 最好不要用公共字段

示例

using System;

namespace WhatsNewAttributes
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Constructor, AllowMultiple = true, Inherited = false)]
    public class LastModifiedAttribute : Attribute
    {
        private readonly DateTime _dateModified;
        private readonly string _changes;

        public LastModifiedAttribute(string dateModified, string changes)
        {
            _dateModified = DateTime.Parse(dateModified);
            _changes = changes;
        }

        public DateTime DateModified => _dateModified;

        public string Changes => _changes;

        public string Issues { get; set; }
    }

    [AttributeUsage(AttributeTargets.Assembly)]
    public class SupportsWhatsNewAttribute : Attribute
    {
    }
}

using System;
using System.Collections;
using System.Collections.Generic;
using WhatsNewAttributes;

[assembly: SupportsWhatsNew]

namespace VectorClass
{
    [LastModified("19 Jul 2017", "updated for C# 7 and .NET Core 2")]
    [LastModified("6 Jun 2015", "updated for C# 6 and .NET Core")]
    [LastModified("14 Dec 2010", "IEnumerable interface implemented: " +
        "Vector can be treated as a collection")]
    [LastModified("10 Feb 2010", "IFormattable interface implemented " +
        "Vector accepts N and VE format specifiers")]
    public class Vector : IFormattable, IEnumerable<double>
    {
        public Vector(double x, double y, double z)
        {
            X = x;

            Y = y;
            Z = z;
        }

        [LastModified("19 Jul 2017", "Reduced the number of code lines")]
        public Vector(Vector vector)
            : this (vector.X, vector.Y, vector.Z) { }

        public double X { get;  }
        public double Y { get; }
        public double Z { get; }

        public override bool Equals(object obj) => this == obj as Vector;

        public override int GetHashCode() =>  (int)X | (int)Y | (int)Z;

        [LastModified("19 Jul 2017",
              "changed ijk format from StringBuilder to format string")]
        public string ToString(string format, IFormatProvider formatProvider)
        {
            if (format == null)
            {
                return ToString();
            }

            switch (format.ToUpper())
            {
                case "N":
                    return "|| " + Norm().ToString() + " ||";
                case "VE":
                    return $"( {X:E}, {Y:E}, {Z:E} )";
                case "IJK":
                    return $"{X} i + {Y} j + {Z} k";
                default:
                    return ToString();
            }
        }

        [LastModified("6 Jun 2015", "added to implement IEnumerable<T>")]
        public IEnumerator<double> GetEnumerator() => new VectorEnumerator(this);

        IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

        public override string ToString() => $"({X} , {Y}, {Z}";

        public double this[uint i]
        {
            get
            {
                switch (i)
                {
                    case 0:
                        return X;
                    case 1:
                        return Y;
                    case 2:
                        return Z;
                    default:
                        throw new IndexOutOfRangeException(
                            "Attempt to retrieve Vector element" + i);
                }
            }
        }

        public static bool operator == (Vector left, Vector right) =>
            Math.Abs(left.X - right.X) < double.Epsilon &&
            Math.Abs(left.Y - right.Y) < double.Epsilon &&
            Math.Abs(left.Z - right.Z) < double.Epsilon;
    

        public static bool operator != (Vector left, Vector right) => !(left == right);

        public static Vector operator + (Vector left, Vector right) =>  new Vector(left.X + right.X, left.Y + right.Y, left.Z + right.Z);

        public static Vector operator * (double left, Vector right) =>
            new Vector(left * right.X, left * right.Y, left * right.Z);

        public static Vector operator * (Vector left, double right) => left * right;

        public static double operator * (Vector left, Vector right) =>
            left.X * right.X + left.Y + right.Y + left.Z * right.Z;

        public double Norm() => X * X + Y * Y + Z * Z;

        #region enumerator class
        [LastModified("6 Jun 2015", "Change to implement the generic version IEnumerator<T>")]
        [LastModified("14 Feb 2010", "Class created as part of collection support for Vector")]
        private class VectorEnumerator : IEnumerator<double>
        {
            readonly Vector _theVector;      // Vector object that this enumerato refers to 
            int _location;   // which element of _theVector the enumerator is currently referring to 

            public VectorEnumerator(Vector theVector)
            {
                _theVector = theVector;
                _location = -1;
            }

            public bool MoveNext()
            {
                ++_location;
                return (_location <= 2);
            }

            public object Current => Current;

            double IEnumerator<double>.Current
            {
                get
                {
                    if (_location < 0 || _location > 2)
                        throw new InvalidOperationException(
                            "The enumerator is either before the first element or " +
                            "after the last element of the Vector");
                    return _theVector[(uint)_location];
                }
            }

            public void Reset()
            {
                _location = -1;
            }

            public void Dispose()
            {
                // nothing to cleanup
            }
        }
        #endregion
    }
}


using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using WhatsNewAttributes;

namespace LookupWhatsNew
{
    class Program
    {
        private static readonly StringBuilder outputText = new StringBuilder(1000);
        private static DateTime backDateTo = new DateTime(2017, 2, 1);

        static void Main()
        {
            Assembly theAssembly = Assembly.Load(new AssemblyName("VectorClass"));
            Attribute supportsAttribute = theAssembly.GetCustomAttribute(typeof(SupportsWhatsNewAttribute));

            AddToOutput($"Assembly: {theAssembly.FullName}");

            if (supportsAttribute == null)
            {
                AddToOutput("This assembly does not support WhatsNew attributes");
                return;
            }
            else
            {
                AddToOutput("Defined Types:");
            }

            foreach (Type definedType in theAssembly.ExportedTypes)
            {
                DisplayTypeInfo(definedType);
            }

            Console.WriteLine($"What\'s New since {backDateTo:D}");
            Console.WriteLine(outputText.ToString());

            Console.ReadLine();
        }

        static void AddToOutput(string text) =>
            outputText.Append($"{Environment.NewLine}{text}");

        private static void DisplayTypeInfo(Type type)
        {
            if (!type.GetTypeInfo().IsClass)
            {
                return;
            }

            AddToOutput($"{Environment.NewLine}class {type.Name}");

            IEnumerable<LastModifiedAttribute> lastModifiedAttributes = type.GetTypeInfo().GetCustomAttributes().OfType<LastModifiedAttribute>().Where(a => a.DateModified >= backDateTo).ToArray();
            if (lastModifiedAttributes.Count() == 0)
            {
                AddToOutput($"\tNo changes to the class {type.Name}{Environment.NewLine}");
            }
            else
            {
                foreach (LastModifiedAttribute attribute in lastModifiedAttributes)
                {
                    WriteAttributeInfo(attribute);
                }
            }

            AddToOutput("changes to methods of this class:");

            foreach (MethodInfo method in type.GetTypeInfo().DeclaredMembers.OfType<MethodInfo>())
            {
                IEnumerable<LastModifiedAttribute> attributesToMethods = method.GetCustomAttributes()
                    .OfType<LastModifiedAttribute>().Where(a => a.DateModified >= backDateTo).ToArray();

                if (attributesToMethods.Count() > 0)
                {
                    AddToOutput($"{method.ReturnType} {method.Name}()");

                    foreach (Attribute attribute in attributesToMethods)
                    {
                        WriteAttributeInfo(attribute);
                    }
                }
            }
        }

        private static void WriteAttributeInfo(Attribute attribute)
        {
            if (attribute is LastModifiedAttribute lastModifiedAttribute)
            {
                AddToOutput($"\tmodified: {lastModifiedAttribute.DateModified:D}: {lastModifiedAttribute.Changes}");

                if (lastModifiedAttribute.Issues != null)
                {
                    AddToOutput($"\tOutstanding issues: {lastModifiedAttribute.Issues}");
                }
            }      
        }
    }
}