11.1 反射
程序是用来处理数据的,文本和特性都是数据,而我们程序本身(类的定义和BLC中的类)这些也是数据。有关程序及其类型的数据被称为元数据(metadata),它们保存在程序的程序集中。程序在运行时,可以查看其它程序集或其本身的元数据。一个运行的程序查看本身的元数据或者其他程序集的元数据的行为叫做反射。
下面介绍如何使用Type类来反射数据,以及如何使用特性来给类型添加元数据。Type位于System.Reflection命名空间下。
11.1.1 Type类
预定义类型(int long 和string等),BCL中的类型(Console,IEnumerable等)和程序员自定义类型(MyClass,MyDel等)。 每种类型都有自己的成员和特性。
BCL声明了一个叫做Type的抽象类,它被设计用来包含类型的特性。使用这个类的对象能让我们获取程序使用的类型的信息。
由于 Type是抽象类,因此不能利用它去实例化对象。关于Type的重要事项如下:
- 对于程序中用到的每一个类型,CLR都会创建一个包含这个类型信息的Type类型的对象。
- 程序中用到的每一个类型都会关联到独立的Type类的对象。
- 不管创建的类型有多少个示例,只有一个Type对象会关联到所有这些实例。
11.1.2 获取Type对象
namespace c_sharp_practice06
{
class Program
{
static void Main(string[] args)
{
//每一个类对应一个type对象,这个type对象存储了这个类有哪些方法、数据、成员
MyClass my = new MyClass { };//一个类的数据是存储在对象中的,但是type对象只存储类的成员
Type type = my.GetType();//通过对象获取这个对象所属类的Type对象
Console.WriteLine(type.Name);//获取类名
Console.WriteLine(type.Namespace);
Console.WriteLine(type.Assembly);
FieldInfo[] array1 = type.GetFields();//只能获取公有字段
foreach(var info in array1)
{
Console.Write(info + " ");
}
Console.WriteLine();
PropertyInfo[] array2 = type.GetProperties();
foreach (var info in array2)
{
Console.Write(info + " ");
}
Console.WriteLine();
MethodInfo[] array3 = type.GetMethods();
foreach (var info in array3)
{
Console.Write(info + " ");
}
Console.ReadKey();
}
}
}
11.1.3 Assembly类
Assembly类在System.Reflection命名空间中定义,它允许访问给定程序集的元数据,它也包含了可以加载和执行程序集。
加载程序集的两种方法:
- Assembly assembly1 = Assembly.Load(“SomeAssembly”);根据程序集的名字加载程序集,它会在本地目录和全局程序集缓存目录查找符合名字的程序集。
- Assembly assembly2 = Assembly.LoadFrom(@“c:\xx\xx\xx\SomeAssembly.dll”)//这里的参数是程序集的完整路径名,它不会在其他位置搜索。
11.1.4 Assembly对象的使用
namespace c_sharp_practice06
{
class Program
{
static void Main(string[] args)
{
MyClass my = new MyClass { };
Assembly assem = my.GetType().Assembly;//通过类的type对象获取它所在的程序集Assenbly
Console.WriteLine(assem.FullName);
Type[] types = assem.GetTypes();
foreach(var typ in types)
{
Console.WriteLine(typ);
}
Console.ReadKey();
}
}
}
11.2特性
特性(attribute)是一种允许我们向程序的程序集增加元数据的语言结构。它是用于保存程序结构信息的某种特殊类型的类。
将应用了特性的程序结构叫做目标。
设计用来获取和使用元数据的程序(对象浏览器)叫做特性的消费者(如编译器)。
.NET预定了很多特性,我们也可以声明自定义特性。
我们在源代码中将特性应用于程序结构;
编译器获取源代码并且从特性产生元数据,然后把元数据放到程序集中;
消费者程序可以获取特性的元数据以及程序中其他组件的元数据。注意,编译器同时生产和消费特性。
关于特性的命名规范,特性名使用Pascal命名法(首字母大写),并且以Attribute后缀结尾,当为目标应用特性时,我们可以不使用后缀。例如对于SerializableAttribute和MyAttributeAttribute这两个特性,我们把他们应用到结构是可以使用Serializable和MyAttribute。
特性的应用。特性的目的是告诉编译器把程序结构的某组元数据嵌入程序集。我们可以通过把特性应用到结构来实现。
- 在结构前放置特性片段来应用特性;
- 特性片段被方括号包围,特性片段包括特性名和特性的参数列表;
- 应用了特性的结构成为特性装饰。
11.2.1 .NET预定义特性
- Obsolete特性
一个程序可能在其生命周期中经历多次发布,而且很可能延续多年。在程序生命周期的后半部分,程序员经常需要编写类似功能的新方法替换老方法。处于多种原因,你可能不再使用哪些调用过时的旧方法的老代码。而只想用新编写的代码调用新方法。旧的方法不能删除,因为有些旧代码也使用的旧方法,那么如何提示程序员使用新代码呢?可以使用Obsolete特性将程序结构标注为过期的,并且在代码编译时,显示有用的警告信息。
namespace c_sharp_practice07
{
class Program
{
[Obsolete("请使用NewMethod代替")]//Obsolete特性用来表示一个方法被弃用了
static void OldMethod1()
{
Console.WriteLine("OldMethod1");
}
[Obsolete("请使用NewMethod代替", true)]//这个特性的第二个参数表示是是否应该标记为错误,而不仅仅是警告。
static void OldMethod2()
{
}
static void NewMethod()
{
Console.WriteLine("NewMethod");
}
static void Main(string[] args)
{
OldMethod1();
OldMethod2();
Console.ReadKey();
}
}
- Conditional特性
Conditional特性(System.Diagnostics)允许我们包括或取消特定方法的所有调用。为方法声明应用Conditional特性并把编译符作为参数来使用。定义方法的CIL代码本身总是会包含在程序集中,只是调用代码会被插入或忽略。
#define IsText1//定义一个宏,控制Text1是否被调用
namespace c_sharp_practice07
{
class Program
{
[Conditional("IsText1")]
static void Text1()
{
Console.WriteLine("Text1");
}
static void Text2()
{
Console.WriteLine("Text2");
}
static void Main(string[] args)
{
Text1();
Text2();
Text1();
Console.ReadKey();
}
}
}
- 调用者信息特性
调用者信息特性(System.Runtime.CompilerServices)可以访问文件路径,代码行数,调用成员的名称等源代码信息。这个三个特性名称为CallerFilePath,CallerLineNumber和CallerMemberName。这些特性只能用于方法中的可选参数。
namespace c_sharp_practice07
{
class Program
{
[DebuggerStepThrough]//可以跳过Debuger的单步调试,不进入该方法
static void PrintOut(string str,
[CallerFilePath] string filePathName="",[CallerLineNumber] int lineNumber=0,[CallerMemberName] string methodName="")//需赋初值
{
Console.WriteLine(str);
Console.WriteLine(filePathName);
Console.WriteLine(lineNumber);
Console.WriteLine(methodName);
}
static void Main(string[] args)
{
PrintOut("Lemon");
Console.ReadKey();
}
}
}
-
DebuggerStepThrough特性
我们在单步调试代码的时候,常常希望调试器不要进入某些方法。我们只想执行该方法,然后继续调试下一行。DebuggerStepThrough特性(System.Diagnostics)告诉调试器在执行目标代码时不要进入该方法调试。有些方法小并且毫无疑问是正确的,在调试时对其反复单步调试只能徒增烦恼。要小心使用该特性,不要排除了可能出现bug的代码。
该特性可用于类,结构,构造方法,方法或访问器。
示例见3 -
其它预定义特性
特性 | 意义 |
---|---|
CLSCompliant | 声明可公开的成员应该被编译器检查是否符合CLS。兼容的程序集可以被任何 |
Serializable | 声明结构可以被序列化 |
NonSerialized | 声明结构不可以被序列化 |
DLLImport | 声明是非托管代码实现的 |
WebMethod | 声明方法应该被作为XML Web服务的一部分暴露 |
AttributeUsage | 声明特性能应用到什么类型的程序结构。将这个特性应用到特性声明上 |
11.2.2 自定义特性
特性只是某个特殊结构的类。所有的特性类都派生自System.Attribute。
- 声明
声明一个特性类和声明其他类一样,有下面的注意事项:
声明一个派生自System.Attribute的类
给它起一个以后缀Attribute结尾的名字
(安全起见,一般我们声明一个sealed的特性类)
特性类声明如下:
public sealed class MyAttributeAttribute : System.Attribute{
…
特性类的公共成员可以是:
字段
属性
构造函数 - 构造函数
特性类的构造函数的声明跟普通类一样,如果不写系统会提供一个默认的,可以进行重载
构造函数的调用。
[MyAttribute(“a value”)] //调用特性类的带有一个字符串的构造函数
[MyAttribute(“Version 2.3”,“Mackel”)] //调用特性类带有两个字符串的构造函数
构造函数的实参,必须是在编译期间能确定值的常量表达式。如果调用的是无参的构造函数,那么后面的()可以不写。
构造函数中的位置参数和命名参数:位置参数(按照构造函数中参数的位置),命名参数(按照属性的名字进行赋值)。
[MyAttribute(“An excellent class”,Reviewer = “Amy McArthur”,Ver=“3.12.3”)] - 限定特性
有一个很重要的预定义特性可以用来应用到自定义特性上,那就是AttributeUsage特性,我们可以使用它来限制特性使用在某个目标类型上。例如,如果我们希望自定义特性MyAttribute只能应用到方法上,那么可以用如下形式使用AttributeUsate:
[AttributeUsage(AttributeTarget.Method)]
public sealed class MyAttributeAttribute : System.Attribute{
…
AttributeUsage三个重要的公共属性:
ValidOn:保存特性能应用到的目标类型的列表,构造函数的第一个参数就是。
Inherited:一个布尔值,它指示特性是否会被特性类所继承,默认为true。
AllowMutiple:是否可以多个特性的实例应用到一个目标上,默认为false。
AttributeTarget:类型的枚举值。
AttributeTarget枚举的成员:
All 、Assembly、Class、Constructor、Delegate、Enum、Event、Field、GenericParameter、Interface、Method、Module、Parameter、Property、ReturnValue Struct多个参数之间使用按位或|运算符来组合
AttributeTarget.Method|AttributeTarget.Constructor - 自定义特性一般遵守的规范
特性类应该表示目标结构的一些状态
如果特性需要某些字段,可以通过包含具有位置参数的构造函数来收集数据,可选字段可以采用命名参数按需初始化
除了属性之外,不要实现公共方法和其他函数成员
为了更安全,把特性类声明为sealed
在特性声明中使用AttributeUsage来指定特性目标组 - 访问(消费)特性
我们可以使用IsDefined方法来,通过Type对象可以调用这个方法,来判断这个类上是否应用了某个特性
bool isDefined = t.IsDefined( typeof(AttributeClass),false )
第一个参数是传递的需要检查的特性的Type对象
第二个参数是一个bool类型的,表示是否搜索AttributeClass的继承树
object[] attArray = t.GetCustomAttributes(false);
它返回的是一个object的数组,我们必须将它强制转换成相应类型的特性类型
bool的参数指定了它是否搜索继承树来查找特性
调用这个方法后,每一个与目标相关联的特性的实例就会被创建
namespace c_sharp_practice07
{
//1.特性类的后缀以Attribute结尾
//2.需要继承自System.Attribute
//3.一般声明为sealed,不需要被继承
//4.一般情况下特性类用来表示目标结构的状态(定义一些字段或者属性,一般不定义方法)
[AttributeUsage(AttributeTargets.)]//表示该特性类可以应用到程序结构有哪些
sealed class MyTextAttribute:System.Attribute
{
public string Description { get; set; }
public string VersionName { get; set; }
public int ID { get; set; }
public MyTextAttribute(string desc)
{
this.Description = desc;
}
}
}
namespace c_sharp_practice07
{
//当使用特性的时候后面的Attribute不需要写
[MyText("简单的特性类",ID=100)]//通过指定属性的名字给属性赋值,这种是命名参数
class Program
{
static void Main(string[] args)
{
Type type = typeof(Program);//创建Type对象的另一种方法
object[] array = type.GetCustomAttributes(false);//获得应用在Program上的特性
MyTextAttribute myText = array[0] as MyTextAttribute;
Console.WriteLine(myText.Description);
Console.WriteLine(myText.ID);
Console.ReadKey();
}
}
}