用户自定义特性
用户自定义特性,把 自定义元数据 和 程序元素 关联起来
反射
在运行过程中,检查和处理 程序元素。例如:
- 枚举 类型的 成员
- 实例化 新对象
- 执行 对象的 成员
- 查找 类型的 信息
- 查找 程序集的 信息
- 检查 应用于某种类型的 自定义特性
- 创建和编译 新程序集
用户自定义特性
.NET Framework 中定义的特性,编译器 能够 以特殊方式 定制 编译过程
- 例如:可以根据
StructLayout特性中的信息,在内存中 布置结构
用户自定义的特性,不能够 影响 编译过程。
- 因为 编译器 不能识别 用户自定义特性
- 但 用户自定义特性, 应用于 程序元素时,可以在 编译好的程序集中 用作元数据
- 这些元数据 在文档说明中 非常有用
- 但 使用户自定义特性 非常强大的,是使用反射
- 代码可以 读取这些元数据,使用它们 在运行期间 做出决策
- 用户自定义特性 可以直接影响代码运行的方式。例如:
- 对自定义许可类 进行 声明性的 代码访问 安全检查
- 测试工具 使用 程序元素, 用户自定义特性 把信息与程序元素 关联起来
- 开发 可扩展的架构 时,允许加载 插件或模块
当编译器 遇上 用户自定义特性
[FieldName("SocialSecurityNumber")]
public string SocialSecurityNumber
{
get {
//...
}
}
- C# 编译器 发现 程序元素 应用了一个特性XX时,会把
Attribute追加到 这个特性名称后面。e.g.XXAttribute- 如果特性XX已经以
Attribute结尾了,那么就 不再追加Attribute
- 如果特性XX已经以
- 搜索
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: 应用到所有类型的程序元素上AssemblyClassConstructorDelegateEnumEventFieldGenericParameterInterfaceMethodModuleParameterPropertyReturn ValueStruct
特性 放置在?
- 一般,将特性 应用到 程序元素上时,应把 特性 放在 元素前面的方括号中
- 例外是,对于
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}");
}
}
}
}
}