反射、特性和动态编程
1、访问元数据
2、成员调用
3、泛型上的反射
4、自定义特性
5、特性构造器
6、具名参数
7、预定义特性
8、动态编程
特性(attribute)是在一个程序集中插入额外的元数据,
并将这些元数据同一个编程构造(比如类、方法或者属性)关联起来的一种方式。
反射
动态编程
一、反射
可以利用反射做下面这些事情。
a、访问程序集中类型的元数据。其中包括像完整类型名称和成员名这样的构造,
以及对一个构造进行修饰的任何特性。
b、使用元数据,在运行时动态调用一个类型的成员,而不是执行编译时绑定。
反射是指对一个程序集中的元数据进行检查的过程。在以前,当代码编译成一种机器
语言时,关于代码的所有元数据(比如类型和方法名)都会被丢弃。
与此相反,当C#编译成CIL时,它会维持有关代码的大部分元数据。
除此之外,利用反射,可以枚举程序集中的所有类型,并搜索满足特定条件的那些类型。
可以通过System.Type的实例来访问一个类型的元数据,该对象包含了对类型实例的成员
进行枚举的方法。
利用反射,允许你枚举一个程序集中的所有类型及其成员。在这个过程中,可以创建对程序集
API进行编档所需要的存根。然后,可以将通过反射获取的元数据与通过XML注释创建的XML文档
合并起来,从而创建API文档。
类似地,程序员可以利用反射元数据来生成代码,从而将业务对象持久化(序列化)到一个数据库中。
.NET Framework所提供的XmlSerializer、ValueType和DataBinder类均在其实现中利用了反射技术。
1、利用System.Type访问元数据
读取一个类型的元数据,关键在于获得System.Type的一个实例,它代表了目标类型实例。
System.Type提供了获取类型信息的所有方法。
你可以用它回答以下总是
类型的名称是什么(Type.Name)?
类型是public的吗(Type.IsPublic)?
类型的基类型是什么(Type.BaseType)?
类型支持任何接口吗(Type.GetInterfaces)?
类型是在哪个程序集中定义的(Type.Assembly)?
类型的属性、方法、字段是什么(Type.GetProperties()、Type.GetMethods()、Type.GetFields())?
都有什么特性在修饰一个类型(Type.GetCustomAttributes())?
还有其他成员未能一一列出。
现在的关键是获得对一个类型的Type对象的引用。主要通过object.GetType()和typeof()来达到这个目的。
注意,GetMethods()调用不能返回扩展方法,它们只能作为实现类型的静态成员使用。
1.1 GetType()
object包含一个GetType()成员。因此,所有类型都包含这个函数。调用GetType(),即可获得与原始对象
对应的System.Type的一个实例。
1 DateTime dt = new DateTime(); 2 Type type = dt.GetType(); 3 foreach (System.Reflection.PropertyInfo property in type.GetProperties()) 4 { 5 Console.WriteLine(property.Name); 6 } 7 Console.ReadLine();
输出:
Date
Day
DayOfWeek
DayOfYear
Hour
Kind
Millisecond
Minute
Month
Now
UtcNow
Second
Ticks
TimeOfDay
Today
Year
在调用了GetType()之后,程序遍历从Type.GetProperties()返回的每个
System.Reflection.PropertyInfo实例,并显示属性名。
成功调用GetType()的关键在于获得一个对象实例。
例如,静态类是无法实例化的,所以没有办法调用GetType()
1.2 typeof()
typeof 在编译时绑定到一个特定的Type实例,并直接获取一个类型作为参数。
1 using System.Diagnostics; 2 3 ThreadPriorityLevel priority = 4 (ThreadPriorityLevel)Enum.Parse(typeof(ThreadPriorityLevel), "Idle");
Enum.Parse()获取标识了一个枚举的Type对象,然后将一个字符串转换成特定的枚举值。
// 摘要:
// 指定线程的优先级别。
public enum ThreadPriorityLevel
{
// 摘要:
// 指定空闲优先级。 它是所有线程的可能的最低优先级值,与关联的 System.Diagnostics.ProcessPriorityClass 的值无关。
Idle = -15,
//
// 摘要:
// 指定最低优先级。 这比关联的 System.Diagnostics.ProcessPriorityClass 的普通优先级低两级。
Lowest = -2,
//
// 摘要:
// 指定的优先级比关联的 System.Diagnostics.ProcessPriorityClass 的普通优先级低一级。
BelowNormal = -1,
//
// 摘要:
// 指定关联的 System.Diagnostics.ProcessPriorityClass 的普通优先级。
Normal = 0,
//
// 摘要:
// 指定的优先级比关联的 System.Diagnostics.ProcessPriorityClass 的普通优先级高一级。
AboveNormal = 1,
//
// 摘要:
// 指定最高优先级。 这比关联的 System.Diagnostics.ProcessPriorityClass 的普通优先级高两级。
Highest = 2,
//
// 摘要:
// 指定时间关键优先级。 这是所有线程中的最高优先级,与关联的 System.Diagnostics.ProcessPriorityClass 的值无关。
TimeCritical = 15,
}
2、成员调用
反射并非仅可以用来获取元数据。
我们的下一步是获取元数据,并动态调用它引用的成员。
假如现在定义一个类,代表一个应用程序的命令行,并把它命名为CommandLineInfo。
对于这个类来说,最困难的地方在于如何在类中填充启动应用程序时的实际命令数据。
然而,利用反射,你可以将命令行选项映射到属性名,并在运行时动态设置属性。
1 using System; 2 using System.Diagnostics; 3 using System.Reflection; 4 5 namespace ConsoleApplication2 6 { 7 class Program 8 { 9 static void Main(string[] args) 10 { 11 12 13 string[] commadnstr = new string[] { "Comparess.exe", "/Out:newFile", ": <file name>", "/Help:true", "/test:testquestion" }; 14 string errorMessage; 15 CommandLineInfo commandLine = new CommandLineInfo(); 16 17 if (!CommandLinHanlder.TryParse(commadnstr, commandLine, out errorMessage)) 18 { 19 Console.WriteLine(errorMessage); 20 DisplayHelp(); 21 22 } 23 if (commandLine.Help) 24 { 25 DisplayHelp(); 26 } 27 else 28 { 29 if (commandLine.Proiroity != ProcessPriorityClass.Normal) 30 { 31 //Change thread priority 32 } 33 } 34 Console.ReadLine(); 35 } 36 private static void DisplayHelp() 37 { 38 //Display the command-Line help. 39 } 40 41 private class CommandLineInfo 42 { 43 public bool Help { get; set; } 44 public string Out { get; set; } 45 private ProcessPriorityClass _priority = ProcessPriorityClass.Normal; 46 public ProcessPriorityClass Proiroity 47 { 48 get { return _priority; } 49 set { _priority = value; } 50 } 51 52 } 53 } 54 55 public class CommandLinHanlder 56 { 57 public static void Parse(string[] args, object commandLine) 58 { 59 string errorMessage; 60 if (!TryParse(args, commandLine, out errorMessage)) 61 { 62 throw new ApplicationException(errorMessage); 63 } 64 65 66 } 67 public static bool TryParse(string[] args, object commandLine, out string errorMessage) 68 { 69 bool success = false; 70 errorMessage = null; 71 foreach (string arg in args) 72 { 73 string option; 74 if (arg[0] == '/' || arg[0] == '-') 75 { 76 string[] optionParts = arg.Split(new char[] { ':' }, 2); 77 //Remove the slash/dash 除去 / 和 - 78 option = optionParts[0].Remove(0, 1).Trim(); 79 optionParts[1] = optionParts[1].Trim(); 80 81 PropertyInfo property = 82 commandLine.GetType().GetProperty(option, 83 BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public);//根据输入的字符串,获取对应的属性 84 85 if (property != null)//判断输入的有无 86 { 87 //根据属性数据类型 调用SetValue为属性set设置值 88 if (property.PropertyType == typeof(bool)) 89 { 90 property.SetValue(commandLine, true, null); 91 success = true; 92 } 93 else if (property.PropertyType == typeof(string)) 94 { 95 property.SetValue(commandLine, optionParts[1], null); 96 success = true; 97 } 98 else if (property.PropertyType.IsEnum) 99 { 100 //枚举类型的设置,以及报错处理 101 try 102 { 103 property.SetValue(commandLine, 104 Enum.Parse( 105 typeof(ProcessPriorityClass), optionParts[1], true) 106 , null); 107 success = true; 108 } 109 catch (ArgumentException) 110 { 111 success = false; 112 errorMessage = string.Format("The option '{0}' is invalid for '{1}'" 113 , optionParts[1], option); 114 } 115 } 116 else 117 { 118 //不被支持的属性数据类型 119 success = false; 120 errorMessage = string.Format("Data type '{0}' on {1} is not supported", 121 property.PropertyType.ToString(), 122 commandLine.GetType().ToString()); 123 } 124 } 125 else 126 { 127 //不支持的命令行选项,没有此相关属性 128 success = false; 129 errorMessage = string.Format("Option '{0}' is not supported.", option); 130 } 131 132 133 } 134 135 136 } 137 return success; 138 } 139 } 140 141 }
输出:
Option 'test' is not supported.
注:虽然CommandLineInfo是嵌套在Program中的一个private类,但ComamdnLineHandler在它上面执行反射没有任何问题,
甚至可以调用它的成员。如果不使用反射,这个类是不允许其它外部类访问的。
换言之,只要设置了恰当的代码访问安全性(Code Access Security,CAS 参见第21章)权限,反射就可以绕过可访问性规则
。
例如,假定Out是private的,TryParese()方法仍然可以向其赋值。
由于这一点,所以可以将CommandLineHandler转移到一个单独的程序集中,并在多个程序之间共享它,
而且每个程序都有它们自己的CommandLineInfor类。
对于方法,有一个MethodInfo类可供使用,该类提供了一个Invoke()成员来调用方法。
MethodInfo和PropertyInfo都是从MemberInfo继承的(虽然并非直接)。
在这个例子中,之所以设置CAS权限来允许私有成员调用,是因为程序是从本地电脑中运行的。
默认情况下,本地安装的程序是受信任区域的一部分,已被授予了恰当的权限。
然而,从一个远程位置运行的程序就需要被显式地授予这样的权限。
3、泛型类型上的反射
从2.0开始,.NET Framework提供了在泛型类型上反射的能力。
在泛型类型上执行运行时反射,将判断出一个类或方法是否包含泛型类型,并确定它包含的任何类型参数
3.1、判断类型参数的类型
对泛型类型或者泛型方法中的类型参数使用typeof运算符。
1 public class Stack<T> 2 { 3 public void Add(T i) 4 { 5 Type t = typeof(T); 6 } 7 }
获得类型参数的Type对象实例以后,就可以在类型参数上执行反射,从而判断它的行为,
并针对具体类型来调整Add方法,使其能更有效地支持这种类型。
3.2、判断类或方法是否支持泛型
在CLI 2.0的System.Type类中,新增了一系列方法来判断某个给定的类型是否支持泛型参数。
所谓泛型实参,就是在实例化一个泛型类的时候,实际提供的一个类型参数。
1 Type type; 2 type = typeof(System.Nullable<>); 3 Console.WriteLine(type.ContainsGenericParameters); 4 Console.WriteLine(type.IsGenericType); 5 6 type = typeof(System.Nullable<DateTime>); 7 Console.WriteLine(type.ContainsGenericParameters); 8 Console.WriteLine(type.IsGenericType);
输出:
True
True
False
True
3.3 为泛型类或方法获取类型参数
可以调用GetGenericArguments()方法,从一个泛型类获取泛型实参(或类型参数)的一个列表。
这样得到的是由System.Type实例构成的一个数组,这些System.Type实例的顺序就是它们声明为泛型类的
类型参数的顺序。
1 Stack<int> s = new Stack<int>(); 2 Type t = s.GetType(); 3 4 foreach (Type type in t.GetGenericArguments()) 5 { 6 Console.WriteLine("Type parameter:" + type.FullName); 7 }
输出:
Type parameter:System.Int32
二、特性
特性是将额外数据关联到一个属性(以及其他构造)的一种方式。
特性要放到它们修饰的那个构造之前的一对方括号中。
如修改以上代码,设置某些属性必须输入(必选参数)。
用特性来修饰属性。
1 class CommandLineInfo 2 { 3 [CommandLineSwitchAlias("?")] 4 public bool Help 5 { 6 get { return _Help; } 7 set { _Help = value; } 8 } 9 private bool _Help; 10 11 [CommandLineSwitchRequired] 12 public string Out 13 { 14 get { return _Out; } 15 set { _Out = value; } 16 } 17 private string _Out; 18 19 public System.Diagnostics.ProcessPriorityClass Priority 20 { 21 get { return _Priority; } 22 set { _Priority = value; } 23 } 24 private System.Diagnostics.ProcessPriorityClass _Priority = 25 System.Diagnostics.ProcessPriorityClass.Normal; 26 } 27 28 internal class CommandLineSwitchRequiredAttribute : Attribute 29 { 30 //not implimented 31 } 32 33 internal class CommandLineSwitchAliasAttribute : Attribute 34 { 35 public CommandLineSwitchAliasAttribute(string s) 36 { 37 //not implimented 38 } 39 }
除了对属性进行修饰,开发者还可以使用特性来修饰类、接口、struct、枚举
、委托、事件、方法、构造器、字段、参数、返回值、程序集、类型参数和模块。
有两个办法可以在同一个构造上合并多个特性,既可以在同一对方括号中,以逗号分隔多个特性,
也可以将每个特性都放在它自己的一对方括号中。
1 //Two Ways to do it 2 //[CommandLineSwitchRequired] 3 //[CommandLineSwitchAlias("FileName")] 4 [CommandLineSwitchRequired, 5 CommandLineSwitchAlias("FileName")] 6 public string Out 7 { 8 get { return _Out; } 9 set { _Out = value; } 10 }
一系列程序集特性(它们附加了assembly:前缀)定义了像公司、产品和程序集版本号这样的东西。
1 using System.Reflection; 2 using System.Runtime.CompilerServices; 3 using System.Runtime.InteropServices; 4 5 // General Information about an assembly is controlled through the following 6 // set of attributes. Change these attribute values to modify the information 7 // associated with an assembly. 8 [assembly: AssemblyTitle("Chapter17")] 9 [assembly: AssemblyDescription("")] 10 [assembly: AssemblyConfiguration("")] 11 [assembly: AssemblyCompany("")] 12 [assembly: AssemblyProduct("Chapter17")] 13 [assembly: AssemblyCopyright("Copyright ? 2010")] 14 [assembly: AssemblyTrademark("")] 15 [assembly: AssemblyCulture("")] 16 17 // Setting ComVisible to false makes the types in this assembly not visible 18 // to COM components. If you need to access a type in this assembly from 19 // COM, set the ComVisible attribute to true on that type. 20 [assembly: ComVisible(false)] 21 22 // The following GUID is for the ID of the typelib if this project is exposed to COM 23 [assembly: Guid("9adaf385-f1a3-474f-8a36-5c059a7a7e22")] 24 25 // Version information for an assembly consists of the following four values: 26 // 27 // Major Version 28 // Minor Version 29 // Build Number 30 // Revision 31 // 32 // You can specify all the values or you can default the Build and Revision Numbers 33 // by using the '*' as shown below: 34 // [assembly: AssemblyVersion("1.0.*")] 35 [assembly: AssemblyVersion("1.0.0.0")] 36 [assembly: AssemblyFileVersion("1.0.0.0")] 37 38 return:特性,出现在一个方法声明之前。 39 using System.ComponentModel; 40 41 public class Program 42 { 43 [return: Description( 44 "Returns true if the object is in a valid state.")] 45 public bool IsValid() 46 { 47 // ... 48 return true; 49 } 50 }
assembly:
return:
module:
class:
method:
分别对应程序集、返回值、模块、类和方法进行修饰的特性。
assembly:与module:特性的限制在于,它人必须出现在using指令之后,同时在任何命名空间或类声明之前。
使用特性的一个方便之处在于,语言会自动考虑特性的命名规范问题,也就是在名称末尾添加Attribute后缀。
虽然在应用一个特性时允许使用命名,但C#规定后缀是可选的,而且在应用一个特性的时候,通常不会出现此类后缀,
只有在自己定义的一个或者以内联方式使用一个特性的时候才会出现。
比如typeof(DescriptionAttribute)。
1、自定义特性
特性是对象,所以要定义一个特性,你需要定义一个类。
从System.Attribute派生之后,一个普通的类就成为了一个特性。
如:创建一个CommandLineSwitchRequiredAttribute
1 public class CommandLineSwitchRequiredAttribute : System.Attribute 2 { 3 4 } 5
有了这个简单的定义之后,就可以可以这个特性,但目前还没有代码与这个特性对应。
2、查找特性
除了提供属性来返回类型成员之外,Type还提供了一些方法来获取对某个类型进行修饰的特性。
类似地,所有反射类型(比如PropertyInfo和MethodInfo)都包含了一些成员,它们可以用来获取对一个类型进行
修饰的特性列表。
1 public class CommandLineSwitchRequiredAttribute : Attribute 2 { 3 public static string[] GetMissingRequiredOptions( 4 object commandLine) 5 { 6 List<string> missingOptions = new List<string>(); 7 PropertyInfo[] properties = 8 commandLine.GetType().GetProperties(); 9 10 foreach (PropertyInfo property in properties) 11 { 12 Attribute[] attributes = 13 (Attribute[])property.GetCustomAttributes( 14 typeof(CommandLineSwitchRequiredAttribute), 15 false); 16 if ((attributes.Length > 0) && 17 (property.GetValue(commandLine, null) == null)) 18 { 19 missingOptions.Add(property.Name); 20 } 21 } 22 return missingOptions.ToArray(); 23 } 24 }
用于查找特性的代码是相当简单的。给定一个PropertyInfo对象(通过反射来获取),
调用GetCustomAttributes(),指定要查找的特性,并指定是否检查任何重载的方法。
也可以返回所有特性。对于查找一个特性的代码来说,还有什么位置比特性类的静态方法中
更好呢。
3、使用构造器来初始化特性
调用GetCustomAttributes(),返回的是一个object数组,该数组能成功转型为一个Attribute数组。
然而,由于配合中的特性没有任何实例成员,所以在返回的特性中,提供的唯一元数据就是它是否出现。
在之前的代码中定义了一个CommandLineSwitchAliasAttribute 特性。这是一个自定义的特性,它为命令行选项提供了别名。
如,你可以支持用户输入/Help或者/?作为缩写。
为了支持这个设计,需要为特性提供一个构造器。
具体地说,针对别名,你需要提供一个构造器来获取一个string类型,类似地,如果希望允许多个别名,需要让构造器获取一个params
string数组作为参数。
1 public class CommandLineSwitchAliasAttribute : Attribute 2 { 3 public CommandLineSwitchAliasAttribute(string alias) 4 { 5 Alias = alias; 6 } 7 public string Alias 8 { 9 get { return _Alias; } 10 set { _Alias = value; } 11 } 12 private string _Alias; 13 } 14 class CommandLineInfo 15 { 16 [CommandLineSwitchAlias("?")] 17 public bool Help 18 { 19 get { return _Help; } 20 set { _Help = value; } 21 } 22 private bool _Help; 23 24 // ... 25 }
构造器唯一的限制在于,向某种构造应用一个特性的时候,只有字面值和类型(比如typeof(int))
才允许作为参数使用。这是为了确保它们能序列化到最终的CIL中。
因此,你无法在应用一个特性的时候调用静态方法。
除此之外,提供构造器来获取System.DateTime类型的参数是没有多大意义的,
因为没有System.DateTime字面量。
有了构造器调用之后,从PropertyInfo.GetCustomAttributes()返回的对象会使用指定的构造器
参数来初始化。
1 PropertyInfo property = 2 typeof(CommandLineInfo).GetProperty("Help"); 3 CommandLineSwitchAliasAttribute attribute = 4 (CommandLineSwitchAliasAttribute) 5 property.GetCustomAttributes( 6 typeof(CommandLineSwitchAliasAttribute), false)[0]; 7 if (attribute.Alias == "?") 8 { 9 Console.WriteLine("Help(?)"); 10 }; 11 }
除此之外,可以在GetSwitches()方法中,为(CommandLineSwitchAliasAttribute)使用类似的代码,
从而返回由所有开关构成的一个字典集合,并将每个名称同命令行对象的对应特性关联起来(<别名,属性> )。
1 public class CommandLineSwitchAliasAttribute : Attribute 2 { 3 public CommandLineSwitchAliasAttribute(string alias) 4 { 5 Alias = alias; 6 } 7 8 public string Alias 9 { 10 get { return _Alias; } 11 set { _Alias = value; } 12 } 13 private string _Alias; 14 15 public static Dictionary<string, PropertyInfo> GetSwitches( 16 object commandLine) 17 { 18 PropertyInfo[] properties = null; 19 Dictionary<string, PropertyInfo> options = 20 new Dictionary<string, PropertyInfo>(); 21 22 properties = commandLine.GetType().GetProperties( 23 BindingFlags.Public | BindingFlags.NonPublic | 24 BindingFlags.Instance); 25 foreach (PropertyInfo property in properties) 26 { 27 options.Add(property.Name.ToLower(), property); 28 foreach (CommandLineSwitchAliasAttribute attribute in 29 property.GetCustomAttributes( 30 typeof(CommandLineSwitchAliasAttribute), false)) 31 { 32 options.Add(attribute.Alias.ToLower(), property); 33 } 34 } 35 return options; 36 } 37 }
现在来更新CommandLineHandler.TryParse()以处理别名
1 public class CommandLineHandler 2 { 3 // ... 4 public static bool TryParse( 5 string[] args, object commandLine, 6 out string errorMessage) 7 { 8 bool success = false; 9 errorMessage = null; 10 11 Dictionary<string, PropertyInfo> options = 12 CommandLineSwitchAliasAttribute.GetSwitches( 13 commandLine); 14 15 foreach (string arg in args) 16 { 17 PropertyInfo property; 18 string option; 19 if (arg[0] == '/' || arg[0] == '-') 20 { 21 string[] optionParts = arg.Split( 22 new char[] { ':' }, 2); 23 option = optionParts[0].Remove(0, 1).ToLower(); 24 25 if (options.TryGetValue(option, out property)) 26 { 27 success = SetOption( 28 commandLine, property, 29 optionParts, ref errorMessage); 30 } 31 else 32 { 33 success = false; 34 errorMessage = string.Format( 35 "Option '{0}' is not supported.", 36 option); 37 } 38 } 39 } 40 return success; 41 } 42 43 private static bool SetOption( 44 object commandLine, PropertyInfo property, 45 string[] optionParts, ref string errorMessage) 46 { 47 bool success; 48 49 if (property.PropertyType == typeof(bool)) 50 { 51 // Last parameters for handling indexers 52 property.SetValue( 53 commandLine, true, null); 54 success = true; 55 } 56 else 57 { 58 59 if ((optionParts.Length < 2) 60 || optionParts[1] == "" 61 || optionParts[1] == ":") 62 { 63 // No setting was provided for the switch. 64 success = false; 65 errorMessage = string.Format( 66 "You must specify the value for the {0} option.", 67 property.Name); 68 } 69 else if ( 70 property.PropertyType == typeof(string)) 71 { 72 property.SetValue( 73 commandLine, optionParts[1], null); 74 success = true; 75 } 76 else if (property.PropertyType.IsEnum) 77 { 78 success = TryParseEnumSwitch( 79 commandLine, optionParts, 80 property, ref errorMessage); 81 } 82 else 83 { 84 success = false; 85 errorMessage = string.Format( 86 "Data type '{0}' on {1} is not supported.", 87 property.PropertyType.ToString(), 88 commandLine.GetType().ToString()); 89 } 90 } 91 return success; 92 } 93 94 private static bool TryParseEnumSwitch(object commandLine, string[] optionParts, PropertyInfo property, ref string errorMessage) 95 { 96 throw new NotImplementedException(); 97 } 98 }
4、System.AttributeUsageAttribute 设置特性应用的 Target
大多数特性都只对特定的构造进行修饰。
例如,让CommandLineOptionAttribute去修饰一个类或者程序集是无意义的。
为了避免不恰当地使用特性,可以使用System.AttributeUsageAttribute来修饰一个自定义特性。
(限制特性只能应用的构造类型)
1 [AttributeUsage(AttributeTargets.Property)] 2 public class CommandLineSwitchAliasAttribute : Attribute 3 { 4 // ... 5 }
如果想要修饰一个字段,可以如此修饰
1 // Restrict the attribute to properties and methods 2 [AttributeUsage( 3 AttributeTargets.Field | AttributeTargets.Property)] 4 public class CommandLineSwitchAliasAttribute : Attribute 5 { 6 // ... 7 }
AttributeTargets这是一个枚举。
5、具名参数
使用AttributeUsageAttribute除了限制一个特性能够修饰的东西,还可以用它指定是否允许特性在一个构造上进行多次复制。
1 [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] 2 public class CommandLineSwitchAliasAttribute : Attribute 3 { 4 // ... 5 }
AllowMultiple参数是一个具名参数(namedparameter),
它类似于C#4.0为方法参数添加的"命名参数“语法。
具名参数用于在特性构造器调用中设置特定的公共属性和字段,即使构造器不包括对应的参数。
具名参数虽然是可选的,但它允许对特性的额外实例数据进行设置,同时无需提供一个对应的构造器参数。
所以,在使用特性的时候,可以次具名参数赋值来设置该成员,对具名参数的赋值只能放到构造器的最后一部分进行。
任何显式 声明的构造器参数都必须在它之前完成赋值。
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]就已经利用了具名参数这一功能。它的构造函数其实只有一个参数。
<<<<<<<<<
初学者主题:FlagsAttribute
这是由.NET Framework定义的一个特性,它指示可以将枚举作为一组标志(flag)类型的值来处理。
//由.NET Framework定义的一个特性
namespace System.IO
{
// 摘要:
// 提供文件和目录的属性。
[Serializable]
[ComVisible(true)]
[Flags]
public enum FileAttributes
{
// 摘要:
// 文件为只读。
ReadOnly = 1,
//
// 摘要:
// 文件是隐藏的,因此没有包括在普通的目录列表中。
Hidden = 2,
//
// 摘要:
// 文件为系统文件。文件是操作系统的一部分或由操作系统以独占方式使用。
System = 4,
//
// 摘要:
// 文件为一个目录。
Directory = 16,
//
// 摘要:
// 文件的存档状态。应用程序使用此属性为文件加上备份或移除标记。
Archive = 32,
//
// 摘要:
// 保留供将来使用。
Device = 64,
//
// 摘要:
// 文件正常,没有设置其他的属性。此属性仅在单独使用时有效。
Normal = 128,
//
// 摘要:
// 文件是临时文件。文件系统试图将所有数据保留在内存中以便更快地访问,而不是将数据刷新回大容量存储器中。不再需要临时文件时,应用程序会立即将其删除。
Temporary = 256,
//
// 摘要:
// 文件为稀疏文件。稀疏文件一般是数据通常为零的大文件。
SparseFile = 512,
//
// 摘要:
// 文件包含一个重新分析点,它是一个与文件或目录关联的用户定义的数据块。
ReparsePoint = 1024,
//
// 摘要:
// 文件已压缩。
Compressed = 2048,
//
// 摘要:
// 文件已脱机。文件数据不能立即供使用。
Offline = 4096,
//
// 摘要:
// 操作系统的内容索引服务不会创建此文件的索引。
NotContentIndexed = 8192,
//
// 摘要:
// 该文件或目录是加密的。对于文件来说,表示文件中的所有数据都是加密的。对于目录来说,表示新创建的文件和目录在默认情况下是加密的。
Encrypted = 16384,
}
}
使用:
1 using System; 2 using System.IO; 3 4 class Program 5 { 6 public static void Main() 7 { 8 // ... 9 string fileName = @"enumtest.txt"; 10 FileInfo file = new FileInfo(fileName); 11 12 file.Attributes = FileAttributes.Hidden | 13 FileAttributes.ReadOnly; 14 15 Console.WriteLine(""{0}" outputs as "{1}"", 16 file.Attributes.ToString().Replace(",", " |"), 17 file.Attributes); 18 19 FileAttributes attributes = 20 (FileAttributes)Enum.Parse(typeof(FileAttributes), 21 file.Attributes.ToString()); 22 23 Console.WriteLine(attributes); 24 25 // ... 26 } 27 }
标志记录了多个枚举值可以组合使用。
此外,这改变了ToString()和Parse()方法的行为。
例如,为一个已用FlagAttribute修饰了的枚举调用ToString()方法,
会为已设置的每个枚举标志输出对应的字符串。
将一个值从字符串解析成每句也是可行的,只要每个枚举标识符都以一个逗号分隔即可。
需要注意的一个要点是,FlagsAttribute并不会自动指派唯一的标志值,或检查它们是否具有
唯一值,你必须显式指派每个枚举项的值。
<<<<<<<<<
1、预定义特性
AttributeUsageAttribute是一个预定义特性。
AttributeUsageAttribute、FlagsAttribute、ObsoleteAttribute和ConditionalAttribute等
都是预定义特性。
它们都包含了只有CLI提供才或编译器才提供的特定行为,因为没有可用的扩展点用于额外的非
自定义特性,而自定义特性又是完全被动的。
2、System.ConditionAttribte
在一个程序集中,System.Diagnostics.ConditinalAttribute特性的行为有点儿像
#if /#endif等预处理器标识符。
利用它为一个调用赋予无操作行为,换言之,使其成为一个什么都不做的指令。
1 #define CONDITION_A 2 using System; 3 using System.Diagnostics; 4 using System.Reflection; 5 using System.Collections.Generic; 6 7 8 namespace ConsoleApplication2 9 { 10 public class Program 11 { 12 public static void Main() 13 { 14 Console.WriteLine("Begin..."); 15 MethodA(); 16 MethodB(); 17 Console.WriteLine("End..."); 18 } 19 20 [Conditional("CONDITION_A")] 21 static void MethodA() 22 { 23 Console.WriteLine("MethodA() executing..."); 24 } 25 26 [Conditional("CONDITION_B")] 27 static void MethodB() 28 { 29 Console.WriteLine("MethodB() executing..."); 30 } 31 } 32 }
输出:
Begin...
MethodA() executing...
End...
假如一个方法包含了out参数,或者返回类型为void,就不能使用此属性,否则
会造成一个编译时错误。
因为不执行,就不知道该将什么返回给调用者。
属性也能用此特性。
此属性的只能用于方法或类,但将它用于类时比较特殊,因为只允许派生
于System.Attribute的类使用。
用此特性修饰一个自定义特性时,只有在调用程序集中定义了条件字符串的前提下,
才能通过反射来获取那个自定义特性。
3、System.ObsoleteAttribute
用于编制代码的版本,向调用者指定一个特定的成员或类型已过时。
一个成员在使用此特性进行修饰之后,对调用它的代码进行编译,会造成编译器显示一条
警告消息(也可以强制报错)。
1 class Program 2 { 3 public static void Main() 4 { 5 ObsoleteMethod(); 6 } 7 8 [Obsolete] 9 public static void ObsoleteMethod()//if you look through the warnings in the error list the warning will also show up in that list 10 { 11 } 12 }
在这个例子中,只显示一个警告。
然而,该特性还提供了另外两个构造器。第一个是只有一个参数string,显示附加的额外的消息。
第二个是bool,指定是否强制将警告视为错误
1 [Obsolete("建议使用NewMethod()")] 2 public static void ObsoleteMethod()//if you look through the warnings in the error list the warning will also show up in that list 3 { 4 }
编译警告提示:
警告 1 “ConsoleApplication2.Program.ObsoleteMethod()”已过时:“建议使用NewMethod()”
[Obsolete("请使用NewMethod()",true)]
public static void ObsoleteMethod()//if you look through the warnings in the error list the warning will also show up in that list
{
}
编译错误提示:
错误 1 “ConsoleApplication2.Program.ObsoleteMethod()”已过时:“请使用NewMethod()”
此特性允许第方向开发者通知已过时的API。
4、与序列化相关的特性
通过预定义特性,.NET Framework允许将对象序列化成一个流,使它们以后能反序列化为对象。
这样一来,可以在关闭一个应用程序之前,方便地将一个文档类型的对象存盘。之后,文档可以反序列化,
便于用户继续对它进行处理。
虽然对象可能相当复杂,而且可能链接到其他许多也需要序列化的对象类型,但序列化框架其实是很容易使用的。
要想序列化一个对象,唯一需要的就是包含一个System.SerializableAttribute。
有了这个特性,一个formatter(格式化程序)类就可以对该序列化对象执行反射,
并把它复制到一个流中。
1 using System; 2 using System.IO; 3 using System.Runtime.Serialization.Formatters.Binary; 4 5 6 namespace ConsoleApplication2 7 { 8 class Program 9 { 10 public static void Main() 11 { 12 Stream stream; 13 Document documentBefore = new Document(); 14 documentBefore.Title = "before"; 15 documentBefore.Data = "this is a data"; 16 Document documentAfter; 17 //序列化 18 using (stream = File.Create(Environment.CurrentDirectory+"\"+ documentBefore.Title + ".bin")) 19 { 20 BinaryFormatter formatter = new BinaryFormatter(); 21 formatter.Serialize(stream, documentBefore); 22 } 23 24 //反序列化 25 using (stream = File.Open(Environment.CurrentDirectory + "\" + documentBefore.Title + ".bin", FileMode.Open)) 26 { 27 BinaryFormatter formatter = new BinaryFormatter(); 28 documentAfter = (Document)formatter.Deserialize(stream); 29 } 30 } 31 32 33 34 } 35 [Serializable] 36 class Document 37 { 38 public string Title = null; 39 public string Data = null; 40 41 [NonSerialized] 42 public long _WindowHandle = 0; 43 class Image 44 { 45 } 46 [NonSerialized] 47 private Image Picture = new Image(); 48 } 49 }
注意:序列化是针对整个对象图发生的。因此,对象图中的所有字段也必须是可序列化的。
System.NonSerializable
不可序列化的字段就使用System.NonSerializable这个特性来修饰。
它告诉序列化框架忽略这些字段。
不应持久化的字段也就使用这个特性来修饰。
如密码和Windows句柄(handle)。
提供自定义序列化:
添加加密功能的一个办法是提供自定义序列化。
为了提供自定义序列化,除了使用SerializableAttribute之外,还要实现
ISerializable接口。这个接口只要求实现GetObjetData()方法。
然后,这仅足以支持序列化。为了同时支持反序列化,需要提供一个构造器来获取
System.Runtime.Serialization.SerializationInfo与
System.Runtime.Serialization.StreamingContext类型的参数。
1 using System; 2 using System.Runtime.Serialization; 3 [Serializable] 4 class EncryptableDocument : ISerializable 5 { 6 enum Field 7 { 8 Title, 9 Data 10 } 11 public string Title; 12 public string Data; 13 14 public static string Ecrypt(string data) 15 { 16 string encryptedData = data; 17 //加密算法 18 return encryptedData; 19 } 20 public static string Decrypt(string encryptedData) 21 { 22 string data = encryptedData; 23 //解密算法 24 return data; 25 } 26 27 #region 序列化与反序列化 28 public void GetObjectData(SerializationInfo info, StreamingContext context) 29 { 30 info.AddValue(Field.Title.ToString(), Title);//name value 键值对 31 info.AddValue(Field.Data.ToString(), Ecrypt(Data)); 32 } 33 34 //用序列化后的数据来构造 35 public EncryptableDocument(SerializationInfo info, StreamingContext context) 36 { 37 Title = info.GetString(Field.Title.ToString()); 38 Data = Decrypt(info.GetString(Field.Data.ToString())); 39 40 } 41 #endregion 42 }
本质上讲SerializationInfo 对象是由"名称/值"对构成的一个集合。
在序列化的时候,GetObjectData()实现会调用AddValue()。
为了反转,需要调用某个Get*()成员。
在本例中,序列化和反序列化之前分别进行加密和解密。
序列化的版本控制:
像文档这样的对象可能用程序集的一个版本序列化,以后用一个新版本来反序列化(如类中添加了一个新的字段)。
用可选特性来修饰,或者在反序列化的时候,迭代(SerializationInfo 中的值,而不是直接用。只反序列化其中存在的值。
高级主题:System.SerializableAttribute和CIL
三、使用动态对象进行编程
随着动态对象在C#4.0中的引入,许多编程情形都得到了简化,以前无法实现的一些编程情形现在也能实现了。
从根本上说,使用动态对象进行编程,开发人员可以用一个动态调度机制对设想的操作进行编码。
"运行时"会在程序执行时对这个机制进行解析,而不是由编译器在编译时验证和绑定。
为什么要推出动态对象?从较高的级别上说,经常都有对象天生就不适合赋予一个静态类型。
例如包括从一个XML/CSV文件、一个数据库表、DOM或者COM的IDispatch接口加载数据,
或者调用以动态语言写的代码(比如调用IronPython对象中的代码)。
C#4.0的动态对象提供了一个通用的解决方案与"运行时"环境对话。
这种对象在编译时不一定有一个定义好的结构。
在C#4.0 的动态对象的初始实现中,提供了如下4个绑定方法。
(1)、针对一个底层CLR类型使用反射
(2)、调用一个自定义IDynamicMetaObjectProvider,它使一个DynamicMetaObject变得可用。
(3)、通过COM的IUnknown和IDispatch接口来调用。
(4)、调用由动态语言(比如IronPython)定义的类型。
1、使用dynamic调用反射
反射的关键功能之一就是动态查找和调用特定类型的一个成员。
这需要在执行时识别成员名或其他特征,比如一个特性。
然而,C#4.0新增的动态对象提供了一个更简单的办法来通过反射调用成员。
这个技术的限制在于、编译时需要知道成员名和签名(参数个数,参数类型是否兼容)。
有点类似弱类型(JavaScript)。
dynamic data = "Hello World"; Console.WriteLine(data); data = 2 * 100+data.Length; Console.WriteLine(data); data = true; Console.WriteLine(data);
输出:
Hello World
211
True
在这个例子中,没有显式的代码来判断对象类型,查找一个特定的MemberInfo实例并调用它。
相反,data声明为dynamic类型,并直接在它上面调用方法。
在编译时,不会检查指定的成员是否可用,甚至不会检查dynamic对象的基础类型是什么。
所以,只要语法是有效的,编译时就可以发出任何调用。
在编译时,是否有一个对应的成员是无关紧要的。
然后,类型安全性并没有被完全放弃。
对于标准CLR类型,一般在编译时用于非dynamic类型的类型检查器会在执行时为dynamic类型调用。
所以,如果在执行时发现事实上没有这个成员,调用就会引发一个错误。
2、dynamic的原则和行为
dynamic数据类型的几个特征
a、dynamic是告诉编译器生成代码的指令:
当"运行时"遇到一个dynamic调用时,它可以将请求编译成CIL,然后调用新编译的调用
ddynamic对象上调用GetType(),可提示出dynamic实例的基础类型---这个方法不会返回dynamic作为类型
b、任何类型都可以转换成dynamic。
c、从dynamic到一个替代类型的成员转换要依赖于基础类型的支持。
d、dynamic类型的基础类型在每次赋值时都可能改变
e、验证基础类型上是否存在指定的签名要推迟到运行时才进行
f、任何dynamic成员调用返回的都是一个dynamic对象
如:data.ToString() 返回的是一个dynamic对象,而不是string类型。
g、如果指定在成员在运行时不存在,"运行时(VFS)"会引发异常。
h、用dynamic来实现的反射不支持扩展方法
i、究其根本,dynamic是一个System.Object
3、为什么需要动态绑定
除了反射,还可以定义自定义类型来进行动态调用。
假如,假定需要通过动态调用来获取一个XML元素的值。
使用动态对象和不使用动态对象有如下两种写法
1 XElement person = XElement.Parse( 2 @"<Person> 3 <FirstName>Inigo</FirstName> 4 <LastName>Montoya</LastName> 5 </Person>"); 6 7 Console.WriteLine("{0} {1}", 8 person.Element("FirstName").Value, 9 person.Element("LastName").Value);
用以来代码替代:
1 dynamic person = DynamicXml.Parse( 2 @"<Person> 3 <FirstName>Inigo</FirstName> 4 <LastName>Montoya</LastName> 5 </Person>"); 6 7 Console.WriteLine("{0} {1}", 8 person.FirstName, person.LastName);
DynamicXml是待会自定义动态对象定义的。
4、静态编译与动态编程的比较
如果能在编译时验证成员是否属于对象,静态类型的编程是首选的,因为这时也许能
使用一些易读的、简洁的API。
然后,当它的作用不大的时候,C#4.0就允许你写更简单的代码,而不必刻意追求编译时的类型安全性。
5、实现自定义动态对象
定义自定义动态类型的关键是实现
System.Dynamic.IDynamicMetaObjectProvider接口。
但是,不必从头实现接口。
相反,首先的方案是从System.Dynamic.DynamicObject派生出自定义的动态类型。
这样会为众多成员提供默认的实现,你只需要重写那些不合适的。
1 public class DynamicXml : DynamicObject 2 { 3 private XElement Element { get; set; } 4 5 public DynamicXml(System.Xml.Linq.XElement element) 6 { 7 Element = element; 8 } 9 10 public static DynamicXml Parse(string text) 11 { 12 return new DynamicXml(XElement.Parse(text)); 13 } 14 //调用成员时会调用此方法,如: person.FirstName 15 public override bool TryGetMember( 16 GetMemberBinder binder, out object result) 17 { 18 bool success = false; 19 result = null; 20 XElement firstDescendant = 21 Element.Descendants(binder.Name).FirstOrDefault(); 22 if (firstDescendant != null) 23 { 24 if (firstDescendant.Descendants().Count() > 0) 25 { 26 result = new DynamicXml(firstDescendant); 27 } 28 else 29 { 30 result = firstDescendant.Value; 31 } 32 success = true; 33 } 34 return success; 35 } 36 //设置成员值时会调用,如: person.FirstName = "text"; 37 public override bool TrySetMember( 38 SetMemberBinder binder, object value) 39 { 40 bool success = false; 41 XElement firstDescendant = 42 Element.Descendants(binder.Name).FirstOrDefault(); 43 if (firstDescendant != null) 44 { 45 if (value.GetType() == typeof(XElement)) 46 { 47 firstDescendant.ReplaceWith(value); 48 } 49 else 50 { 51 firstDescendant.Value = value.ToString(); 52 } 53 success = true; 54 } 55 return success; 56 } 57 }
如果需要其他动态调用,可以利用System.Dynamic.DynamicObject支持的其他虚
方法。
具体参考MSDN
几乎一切都有相应的成员实现---从转型和各种运算,一直到索引调用。
除此之外,还有一个方法用于获取所有可能的成员名,这方法是GetDynamicMemberNames()。