C#图解教程 第二十四章 反射和特性

反射和特性
元数据和反射
Type 类
获取Type对象
什么是特性
应用特性
预定义的保留的特性
Obsolete(废弃)特性
Conditional特性
调用者信息特性
DebuggerStepThrough 特性
其他预定义特性
有关应用特性的更多内容
多个特性
其他类型的目标
全局特性
自定义特性
声明自定义特性
使用特性的构造函数
指定构造函数
使用构造函数
构造函数中的位置参数和命名参数
限制特性的使用
自定义特性的最佳实践
访问特性
使用IsDefined方法
使用GetCustomAttributes方法

Note

  • 类的元数据包含该类的成员和特性
  • 程序的元数据可以理解为程序的结构信息
  • 反射(reflection)用来查看元数据
  • C#中通过Type类来反射
  • 特性(attribute)用来给类型添加元数据

PS:理解有待加强

反射和特性

元数据和反射



大多数程序都要处理数据,包括读、写、操作和显示数据。(图形也是一种数据的形式。)然而,对于某些程序来说,它们操作的数据不是数字、文本或图形,而是程序和程序类型本身的信息。

  • 有关程序及其类型的数据被称为元数据(metadata),它们保存在程序的程序集中
  • 程序在运行时,可以查看其他程序集或其本身的元数据。一个运行的程序査看本身的元数据或其他程序的元数据的行为叫做反射(reflection)

对象浏览器是显式元数据的程序的一个示例。它可以读取程序集,然后显示所包含的类型以及类型的所有特性和成员。

本章将介绍程序如何使用Type类来反射数据,以及程序员如何使用特性来给类型添加元数据。

要使用反射,我们必须使用System.Reflection命名空间。

Type 类



之前已经介绍了如何声明和使用C#中的类型。包括预定义类型(int、long和string等)、BCL中的类型(Console、IEnumerable等)以及用户自定义类型(MyClass、Mydel等)。每一种类型都有自己的成员和特性。

BCL声明了一个叫做Type的抽象类,它被设计用来包含类型的特性。使用这个类的对象能让我们获取程序使用的类型的信息。

由于Type是抽象类,因此它不能有实例。而是在运行时,CLR创建从Type(RuntimeType)派生的类的实例,Type包含了类型信息。当我们要访问这些实例时,CLR不会返回派生类的引用而是Type基类的引用。但是,为了简单起见,在本章剩余的篇幅中,我会把引用所指向的对象称为Type类型的对象(虽然从技术角度来说是一个BCL内部的派生类型的对象)。

需要了解的有关Type的重要事项如下:

  • 对于程序中用到的每一个类型,CLR都会创建一个包含这个类型信息的Type类型的对象
  • 程序中用到的每一个类型都会关联到独立的Type类的对象
  • 不管创建的类型有多少个实例,只有一个Type对象会关联到所有这些实例

下图显示了一个运行的程序,它有两个MyClass对象和一个OtherClass对象。注意,尽管有两个MyClass的实例,只会有一个Type对象来表示它。

我们可以从Type对象中获取需要了解的有关类型的几乎所有信息。下表列出了类中更有用的成员。

获取Type对象



本节学习使用GetType方法和typeof运算符来获取Type对象。object类型包含了一个叫做GetType的方法,它返回对实例的Type对象的引用。由于每一个类型最终都是从object继承的,所以我们可以在任何类型对象上使用GetType方法来获取它的Type对象,如下所示:

Type t = myInstance.GetType();

下面的代码演示了如何声明一个基类以及从它派生的子类。Main方法创建了每一个类的实例并且把这些引用放在了一个叫做bca的数组中以方便使用。在外层的foreach循环中,代码得到了Type对象并且输出类的名字,然后获取类的字段并输出。下图演示了内存中的对象。

using System;
using System.Reflection;
class BaseClass
{
    public int BaseField=0;
}
class DerivedClass:BaseClass
{
    public int DerivedField=0;
}
class Program
{
    static void Main()
    {
        var bc=new BaseClass();
        var dc=new DerivedClass();
        BaseClass[] bca=new BaseClass[]{bc,dc};

        foreach(var v in bca)
        {
            Type t=v.GetType();

            Console.WriteLine("Object type : {0}",t.Name);

            FieldInfo[] fi=t.GetFields();
            foreach(var f in fi)
            {
                Console.WriteLine("    Field : {0}",f.Name);
            }
            Console.WriteLine();
        }
    }
}

我们还可以使用typeof运算符来获取Type对象。只需要提供类型名作为操作数,它就会返回Type对象的引用,如下所示:

Type t = typeof(DerivedClass);
            ↑        ↑
         运算符 希望的Type对象的类型

下面的代码给出了一个使用typeof运算符的简单示例:

using System;
using System.Reflection;

namespace SimpleReflection
{
    class BaseClass
    {
        public int MyFieldBase;
    }
    class DerivedClass:BaseClass
    {
        public int MyFieldDerived;
    }
    class Program
    {
        static void Main()
        {
            Type tbc=typeof(DerivedClass);
            Console.WriteLine("Result is {0}.",tbc.Name);

            Console.WriteLine("It has the following fields:");
            FieldInfo[] fi=tbc.GetFields();
            foreach(var f in fi)
            {
                Console.WriteLine("    {0}",f.Name);
            }
        }
    }
}

什么是特性

特性(attribute)是一种允许我们向程序的程序集增加元数据的语言结构。它是用于保存程序结构信息的某种特殊类型的类。

  • 将应用了特性的程序结构(program construct)叫做目标(target)
  • 设计用来获取和使用元数据的程序(比如对象浏览器)叫做特性的消费者(consumer)
  • .NET预定了很多特性,我们也可以声明自定义特性

下图是使用特性中相关组件的概览,并且也演示了如下有关特性的要点。

  • 我们在源代码中将特性应用于程序结构
  • 编译器获取源代码并且从特性产生元数据,然后把元数据放到程序集中
  • 消费者程序可以获取特性的元数据以及程序中其他组件的元数据。注意,编译器同时生产和消费特性

根据惯例,特性名使用Pascal命名法并且以Attribute后缀结尾。当为目标应用特性时,我们可以不使用后缀。例如,对于SerializableAttribute和MyAttributeAttribute这两个特性,我们在把它们应用到结构时可以使用Serializable和MyAttribute短名称。

应用特性



我们先不讲解如何创建特性,而是看看如何使用已定义的特性。这样,你会对它们的使用情况有个大致了解。

特性的目的是告诉编译器把程序结构的某组元数据嵌入程序集。我们可以通过把特性应用到结构来实现。

  • 在结构前放置特性片段来应用特性
  • 特性片段被方括号包围,其中是特性名和特性的参数列表

例如,下面的代码演示了两个类的开始部分。最初的几行代码演示了把一个叫做Serializable的特性应用到MyClass。注意,Serializable没有参数列表。第二个类的声明有一个叫做MyAttribute的特性,它有一个带有两个string参数的参数列表。

[Serializable]
public class MyClass
{
    …
}
[MyAttribute("Simple class","Version 3.57")]
public class MyOtherClass
{
    …
}

有关特性需要了解的重要事项如下:

  • 大多数特性只针对直接跟随在一个或多个特性片段后的结构
  • 应用了特性的结构称为被特件装饰(decorated或adorned,两者都应用得很普遍)

预定义的保留的特性



在学习如何定义自己的特性之前,本小节会先介绍几个.NET预定义特性。

Obsolete(废弃)特性

一个程序可能在其生命周期中经历多次发布,而且很可能延续多年。在程序生命周期的后半部分,程序员经常需要编写类似功能的新方法替换老方法。出于多种原因,你可能不想再使用那些调用过时的旧方法的老代码,而只想用新编写的代码调用新方法。

如果出现这种情况,你肯定希望稍后操作代码的团队成员或程序员也只使用新代码。要警告他们不要使用旧方法,可以使用Obsolete特性将程序结构标注为过期的,并且在代码编译时显式有用的警告消息。以下代码给出了一个使用的示例:

class Program
{
    //应用特性
    [Obsolete("User method SuperPrintOut")]
    static void PrintOut(string str)
    {
        Console.WriteLine(str);
    }
    static void Main(string[] args)
    {
        PrintOut("Start of Main");
    }
}

注意,即使PrintOut被标注为过期,Main方法还是调用了它。代码编译也运行得很好并且产生了如下的输出:

不过,在编译的过程中,编译器产生了下面的CS0618警告消息来通知我们正在使用一个过期的结构:

另外一个Obsolete特性的重载接受了bool类型的第二个参数。这个参数指定目标是否应该被标记为错误而不仅仅是瞥告。以下代码指定了它需要被标记为错误:

                                   标记为错误
                                       ↓
[Obsolete("User method SuperPrintOut",true)]
static void PrintOut(string str)
{
    …
}

Conditional特性

Note

Conditional特性类似于C语言的条件编译

Conditional特性允许我们包括或排斥特定方法的所有调用。为方法声明应用Conditional特性并把编译符作为参数来使用。

  • 如果定义了编译符号,那么编译器会包含所有调用这个方法的代码,这和普通方法没有什么区别
  • 如果没有定义编译符号,那么编译器会忽略代码中这个方法的所有调用

定义方法的CIL代码本身总是会包含在程序集中。只是调用代码会被插入或忽略。

例如,在如下的代码中,把Conditional特性应用到对一个叫做TraceMessage的方法的声明上。特性只有一个参数,在这里是字符串DoTrace。

  • 当编译器编译这段代码时,它会检査是否有一个编译符号被定义为DoTrace
  • 如果DoTrace被定义,编译器就会像往常一样包含所有对TraceMessage方法的调用
  • 如果没有DoTrace这样的编译符号被定义,编译器就不会输出任何对TraceMessage的调用
[Conditional("DoTrace")]
static void TraceMessage(string str)
{
    Console.WriteLine(str);
}

Conditional特性的示例

以下代码演示了一个使用Conditional特性的完整示例。

  • Main方法包含了两个对TraceMessage方法的调用
  • TraceMessage方法的声明被用Conditional特性装饰,它带有DoTrace编译符号作为参数。因此,如果DoTrace被定义,那么编译器就会包舍所有对TraceMessage的调用代码
  • 由于代码的第一行定义了叫做DoTrace的编译符,编译器会包含两个对TraceMessage的调用
#define DoTrace
using System;
using System.Diagnostics;
namespace AttributeConditional
{
    class Program
    {
        [Conditional("DoTrace")]
        static void TraceMessage(string str)
        {
            Console.WriteLine(str);
        }
        static void Main()
        {
            TraceMessage("Start of Main");
            Console.WriteLine("Doing work in Main.");
            TraceMessage("End of Main");
        }
    }
}

如果注释掉第一行来取消DoTrace的定义,编译器就不再会插人两次对TraceMessage的调用代码。这次,如果我们运行程序,就会产生如下输出:

调用者信息特性

调用者信息特性可以访问文件路径、代码行数、调用成员的名称等源代码信息。

  • 这三个特性名称为CallerFilePathCallerLineNumberCallerMemberName
  • 这些特性只能用于方法中的可选参数

下面的代码声明了一个名为MyTrace的方法,它在三个可选参数上使用了这三个调用者信息特性。如果调用方法时显式指定了这些参数,则会使用真正的参数值。但在下面所示的Main方法中调用时,没有显式提供这些值,因此系统将会提供源代码的文件路径、调用该方法的代码行数和调用该方法的成员名称。

using System;
using System.Runtime.CompilerServices;

public static class Program
{
    public static void MyTrace(string message,
                                [CallerFilePath] string fileName="",
                                [CallerLineNumber] int lineNumber=0,
                                [CallerMemberName] string callingMember="")
    {
        Console.WriteLine("File:         {0}",fileName);
        Console.WriteLine("Line:         {0}",lineNumber);
        Console.WriteLine("Called From:  {0}",callingMember);
        Console.WriteLine("Message:      {0}",message);
    }
    public static void Main()
    {
        MyTrace("Simple message");
    }
}

DebuggerStepThrough 特性

我们在单步调试代码时,常常希望调试器不要进入某些方法。我们只想执行该方法,然后继续调试下一行。DebuggerStepThrough特性告诉调试器在执行目标代码时不要进入该方法调试。

在我自己的代码中,这是最常使用的特性。有些方法很小并且毫无疑问是正确的,在调试时对其反复单步调试只能徒增烦恼。但使用该特性时要十分小心,因为你并不想排除那些可能含有bug的代码。

关于DebuggerStepThrough要注意以下两点:

  • 该特性位于System.Diagnostics命名空间
  • 该特性可用于类、结构、构造函数、方法或访问器

下面这段随手编造的代码在一个访问器和一个方法上使用了该特性。你会发现,调试器调试这段代码时不会进入IncrementFields方法或X属性的set访问器。

using System;
using System.Diagnostics;

class Program
{
    int _x=1;
    int X
    {
        get{return _x;}
        [DebuggerStepThrough]
        set
        {
            _x=_x*2;
            _x+=value;
        }
    }

    public int Y{get;set;}

    public static void Main()
    {
        var p=new Program();
        p.IncrementFields();
        p.X=5;
        Console.WriteLine("X = {0}, Y = {1}",p.X,p.Y);
    }
    [DebuggerStepThrough]
    void IncrementFields()
    {
        X++;
        Y++;
    }
}

其他预定义特性

.NET框架预定义了很多编译器和CLR能理解和解释的特性,下表列出了一些。在表中使用了不带Attribute后缀的短名称。例如,CLSCompliant的全名是CLSCompliantAttribute。

有关应用特性的更多内容



至此,我们演示了特性的简单使用,都是为方法应用单个特性。这部分内容将会讲述其他特性的使用方式。

多个特性

我们可以为单个结构应用多个特性。

  • 多个特性可以使用下面列出的任何一种格式:

    • 独立的特性片段相互叠在一起
    • 单个特性片段,特性之间使用逗号分隔
  • 我们可以以任何次序列出特性

例如,下面的两个代码片段显示了应用多个特性的两种方式。两个片段的代码是等价的。

[Serializable ]                       //多层结构
[MyAttribute("Simple class", "Version 3.57")]

[MyAttribute("Simple class", "Version 3.57"),Serializable]    //逗号分隔

其他类型的目标

除了类,我们还可以将特性应用到诸如字段和属性等其他程序结构。以下的声明显示了字段上的特性以及方法上的多个特性:

[MyAttribute("Holds a value", "Version 3.2")]    //字段上的特性
public int MyField;

[Obsolete]                                       //方法上的特性
[MyAttribute("Prints out a message.", "Version 3.6")]
public void Printout()
{
    …
}

我们还可以显式地标注特性,从而将它应用到特殊的目标结构。要使用显式目标,在特性片段的开始处放置目标类型,后面跟冒号。例如,如下的代码用特性装饰方法,并且还把特性应用到返回值上。

显式目标说明符
   ↓
[method: MyAttribute("Prints out a message.", "Version 3.6")]
[return: MyAttribute("This value represents …", "Version 2.3")]
public long ReturnSetting()
{
    …
}

如下表所列,C#语言定义了10个标准的特性目标。大多数目标名可以自明(self-explanatory),而type覆盖了类、结构、委托、枚举和接口。 typevar目标名称指定使用泛型结构的类型参数。

全局特性

我们还可以通过使用assembly和module目标名称来使用显式目标说明符把特性设置在程序集或模块级別。(程序集和模块在第21章中解释过。)一些有关程序集级别的特性的要点如下:

  • 程序级级别的特性必须放置在任何命名空间之外,并且通常放置在AssemblyInfo.cs文件中
  • AssemblyInfo.cs文件通常包含有关公司、产品以及版权信息的元数据

如下的代码行摘自AssemblyInfo.cs文件:

[assembly: AssemblyTitle("SuperWidget")]
[assembly: AssemblyDescription("Implements the SuperWidget product.")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("McArthur Widgets, Inc.")]
[assembly: AssemblyProduct("Super Widget Deluxe")]
[assembly: AssemblyCopyright("Copyright ? McArthur Widgets 2012")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture(")]

自定义特性



你或许已经注意到了,应用特性的语法和之前见过的其他语法很不相同。你可能会觉得特性是和结构完全不同的类型,其实不是,特性只是某个特殊类型的类。

有关特性类的一些要点如下。

  • 用户自定义的特性类叫做自定义特性
  • 所有特性类都派生自System.Attribute

声明自定义特性

总体来说,声明一个特性类和声明其他类一样。然而,有一些事项值得注意,如下所示。

  • 要声明一个自定义特性,需要做如下工作

    • 声明一个派生自System.Attribute的类
    • 给它起一个以后缀Attribute结尾的名字
  • 安全起见,通常建议你声明一个sealed的特性类(sealed密封类,不能被继承)

例如,下面的代码显示了MyAttributeAttribute特性的声明的开始部分:

                         特性名                 基类
                           ↓                     ↓
public sealed class MyAttributeAttribute : System.Attribute
{
…
}

由于特性持有目标的信息,所有特性类的公共成员只能是:

  • 字段
  • 属性
  • 构造函数

使用特性的构造函数

特性和其他类一样,都有构造函数。每一个特性至少必须有一个公共构造函数。

  • 和其他类一样,如果你不声明构造函数,编译器会为我们产生一个隐式、公共且无参的构造函数
  • 特性的构造函数和其他构造函数一样,可以被重载
  • 声明构造函数时必须使用类全名,包括后缀。我们只可以在应用特性时使用短名称

例如,如果有如下的构造函数(名字没有包含后缀),编译器会产生一个错误消息:

public MyAttributeAttribute(string desc,string ver)
{
    Description=desc;
    VersionNumber=ver;
}

指定构造函数

当我们为目标应用特性时,其实是在指定应该使用哪个构造函数来创建特性的实例。列在特性应用中的参数其实就是构造函数的参数。

例如,在下面的代码中,MyAttribute被应用到一个字段和一个方法上。对于字段,声明指定了使用单个字符串的构造函数。对于方法,声明指定了使用两个字符串的构造函数。

[MyAttribute("Holds a value")]    //使用一个字符串的构造函数
public int MyField;

[MyAttribute("version 1.3", "Sal Martin")]    //使用两个字符串的构造函数
public void MyMethod()
{
    …
}

其他有关特性构造函数的要点如下。

  • 在应用特性时,构造函数的实参必须是在编译期能确定值的常量表达式
  • 如果应用的特性构造函数没有参数,可以省略圆括号。例如,如下代码的两个类都使用MyAttr特性的无参构造函数。两种形式的意义是相同的
[MyAttr]
class SomeClass …

[MyAttr()]
class OtherClass …

使用构造函数

和其他类一样,我们不能显式调用构造函数。特性的实例创建后,只有特性的消费者访问特性时才能调用构造函数。这一点与其他类的实例很不相同,这些实例都创建在使用对象创建表达式的位置。应用一个特性是一条声明语句,它不会决定什么时候构造特性类的对象。

下图比较了普通类构造函数的使用和特性的构造函数的使用。

  • 命令语句的实际意义是:"在这里创建新的类"
  • 声明语句的意义是:"这个特性和这个目标相关联,如果需要构造特性,使用这个构造函数"

构造函数中的位置参数和命名参数

和普通类的方法与构造方法相似,特性的构造方法同样可以使用位置参数和命名参数。如下代码显示了使用一个位置参数和两个命名参数来应用一个特性:

                   位置参数            命名参数              命名参数
                      ↓                   ↓                   ↓
[MyAttribute("An excellent class",Reviewer="Amy McArthur",Ver="0.7.15.33")]

下面的代码演示了特性类的声明以及为MyClass类应用特性。注意,构造函数的声明只列出了一个形参,但我们可通过命名参数给构造函数3个实参。两个命名参数设置了字段Ver和Reviewer的值。

public sealed class MyAttributeAttribute : System.Attribute
{
    public string Description;
    public string Ver;
    public string Reviewer;

    public MyAttributeAttribute(string desc) //一个形参
    {
        Description = desc;
    }
}
             //三个实参
[MyAttribute("An excellent class”, Reviewer="Amy McArthur", Ver="7.15.33")]
class MyClass
{
    …
}

构造函教需要的任何位置参数都必须放在命名参数之前。

限制特性的使用

我们已经看到了可以为类应用特性。而特性本身就是类,有一个很重要的预定义特性可以用来应用到自定义特性上,那就是AttributeUsage特性。我们可以使用它来限制特性使用在某个目标类型上。

例如,如果我们希望自定义特性MyAttribute只能应用到方法上,那么可以以如下形式使用AttributeUsage:

                   只针对方法
                       ↓
[AttributeUsage( AttributeTarget.Method )]
public sealed class MyAttributeAttribute : System.Attribute
{
    …
}

AttributeUsage有三个重要的公共属性,如下表所示。表中显示了属性名和属性的含义。对于后两个属性,还显示了它们的默认值。

AttributeUsage的构造函数

AttributeUsage的构造函数接受单个位置参数,该参数指定了特性允许的目标类型。它用这个参数来设置ValidOn属件,可接受目标类型是AttributeTarget枚举的成员。AttributeTarget枚举的完整成员列表如下表所示。

我们可以通过使用按位或运算符来组合使用类型。例如,在下面的代码中,被装饰的特性只能应用到方法和构造函数上。

                                     目标
                                       ↓
[AttributeUsage( AttributeTarget.Method| AttributeTarget.Constructor )]
public sealed class MyAttributeAttribute : System.Attribute

当我们为特性声明应用AttributeUsage时,构造函数至少需要一个参数,参数包含的目标类型会保存在ValidOn中。我们还可以通过使用命名参数有选择性地设置Inherited和AllowMultiple属性。如果我们不设置,它们会保持如表24-4所示的默认值。

作为示例,下面一段代码指定了MyAttribute的如下方面。

  • MyAttribute能且只能应用到类上
  • MyAttribute不会被应用它的派生类所继承
  • 不能有MyAttribute的多个实例应用到同一个目标上
[AttributeUsage( AttributeTarget.Class,     //必需的,位置参数
                Inherited = false,          //可选的,命名参数
                AllowMultiple = false )]    //可选的,命名参数
public sealed class MyAttributeAttribute : System.Attribute
{
    …
}

自定义特性的最佳实践

强烈推荐编写自定义特性时参考如下实践。

  • 特性类应该表示目标结构的一些状态
  • 如果特性需要某些字段,可以通过包含具有位置参数的构造函数来收集数据,可选字段可以采用命名参数按需初始化
  • 除了属性之外,不要实现公共方法或其他函数成员
  • 为了更安全,把特性类声明为sealed
  • 在特性声明中使用AttributeUsage来显式指定特性目标组

如下代码演示了这些准则:

[AttributeUsage( AttributeTargets.Class )]
public sealed class ReviewCommentAttribute : System.Attribute
{
    public    string    Description     {get;set;}
    public    string    VersionNumber   {get;set;}
    public    string    ReviewerID      {get;set;}

    public ReviewCommentAttribute(string desc, string ver)
    {
        Description = desc;
        VersionNumber = ver;
    }
}

访问特性



在本章开始处,我们已经看到了可以使用Type对象来获取类型信息。对于访问自定义特性来说,我们也可以这么做。Type的两个方法(IsDefined和GetCustomAttributes)在这里非常有用。

使用IsDefined方法

我们可以使用Type对象的IsDefined方法来检测某个特性是否应用到了某个类上。

例如,以下的代码声明了一个有特性的类MyClass,并且作为自己特性的消费者在程序中访问声明和被应用的特性。代码的开始处是MyAttribute特性和应用特性的MyClass类的声明。这段代码做了下面的事情。

  • 首先,Main创建了类的一个对象。然后通过使用从object基类继承的GetType方法获取了Type对象的一个引用
  • 有了Type对象的引用,就可以调用IsDefined方法来判断ReviewComment特性是否应用到了这个类
    • 第一个参数接受需要检査的特性的Type对象
    • 第二个参数是bool类型的,它指示是否搜索MyClass的继承树来查找这个特性
[AttributeUsage(AttributeTargets.Class)]
public sealed class ReviewCommentAttribute:System.Attribute
{…}

[ReviewComment("Check it out","2.4")]
class MyClass{}

class Program
{
    static void Main()
    {
        var mc=new MyClass();
        Type t=mc.GetType();
        bool isDefined=
            t.IsDefined(typeof(ReviewCommentAttribute),false);
        if(isDefined)
            Console.WriteLine("ReviewComment is applied to type {0}",t.Name);
    }
}

使用GetCustomAttributes方法

GetCustomAttributes方法返回应用到结构的特性的数组。

  • 实际返冋的对象是object的数组,因此我们必须将它强制转换为相应的特性类型
  • 布尔参数指定是否搜索继承树来査找特性
  • object[] AttArr = t.GetCustomAttributes(false);
  • 调用GetCustomAttributes方法后,每一个与目标相关联的特性的实例就会被创建

下面的代码使用了前面的示例中相同的特性和类声明。但是,在这种情况下,它不检测特性是否应用到了类,而是获取应用到类的特性的数组,然后遍历它们,输出它们的成员的值。

using System;

[AttributeUsage(AttributeTargets.Class)]
public sealed class MyAttributeAttribute:System.Attribute
{
    public  string  Description  {get;set;}
    public  string  VersionNumber{get;set;}
    public  string  ReviewerID   {get;set;}

    public MyAttributeAttribute(string desc,string ver)
    {
        Description=desc;
        VersionNumber=ver;
    }
}

[MyAttribute("Check it out","2.4")]
class MyClass
{
}
class Program
{
    static void Main()
    {
        Type t=typeof(MyClass);
        object[] AttArr=t.GetCustomAttributes(false);

        foreach(Attribute a in AttArr)
        {
            var attr=a as MyAttributeAttribute;
            if(null!=attr)
            {
                Console.WriteLine("Description    :{0}",attr.Description);
                Console.WriteLine("Version Number :{0}",attr.VersionNumber);
                Console.WriteLine("Reviewer ID    :{0}",attr.ReviewerID);
            }
        }
    }
}

时间: 2024-10-07 05:07:02

C#图解教程 第二十四章 反射和特性的相关文章

C#图解教程 第二十五章 其他主题

其他主题 概述字符串使用 StringBuilder类把字符串解析为数据值关于可空类型的更多内容 为可空类型赋值使用空接合运算符使用可空用户自定义类型 Main 方法文档注释 插入文档注释使用其他XML标签 嵌套类型 嵌套类的示例可见性和嵌套类型 析构函数和dispose模式 标准dispose模式比较构造函数和析构函数 和COM的互操作 其他主题 概述 在本章中,我会介绍使用C#时的一些重要而又不适合放到其他章节的主题,包括字符串操作.可空类型.Main方法.文档注释以及嵌套类型. 字符串 对

第二十四章 C++11特性之右值引用

右值引用,是 C++11 语言核心中最为重要的改进之一.右值引用给 C++ 带来了“Move语义”(“转移语义”),同时解决了模板编程中完美转发的问题(Perfect forwarding).右值引用使 C++ 对象有能力甄别什么是(可以看作)临时对象,对于临时对象的拷贝可以做某种特别的处理,一般来说主要是直接传递资源的所有权而不是像一般地进行拷贝,这就是所谓的 move 语义了.完美转发则是指在模板编程的时候,各层级函数参数传递时不会丢失参数的“属性”(lvalue/rvalue, const

Gradle 1.12用户指南翻译——第二十四章. Groovy 插件

其他章节的翻译请参见: http://blog.csdn.net/column/details/gradle-translation.html 翻译项目请关注Github上的地址: https://github.com/msdx/gradledoc/tree/1.12. 直接浏览双语版的文档请访问: http://gradledoc.qiniudn.com/1.12/userguide/userguide.html. 另外,Android 手机用户可通过我写的一个程序浏览文档,带缓存功能的,兼容

第二十四章

希言自然.飘风不终朝,骤雨不终日.孰为此?天地,天地而弗能久,又况于人乎?故从事而道者同于道,德者同于德,失者同于失.同于德者,道亦德之.同于失者,道亦失之. 第二十四章1 为何盛世的领导者很少有丰功伟绩? 各位朋友大家好,今天我们接着来聊<道德经>.前边大家的留言我都看了,写的感想我也看了,我真的没想到大家感想写的这么好.而且这个<道德经>给大家带来这么多的变化.这么多的提升,让我特别开心,真的非常感动.我自己在讲<道德经>的过程中,说实话我自己也在不断地提升.也在学

【WPF学习】第二十四章 基于范围的控件

原文:[WPF学习]第二十四章 基于范围的控件 WPF提供了三个使用范围概念的控件.这些控件使用在特定最小值和最大值之间的数值.这些控件--ScrollBar.ProgressBar以及Slider--都继承自RangeBase类(该类又继承自Control类).尽管它们使用相同的抽象概念(范围),但工作方式却又很大的区别. 下表显示了RangeBase类定义的属性: 表 RangeBase类的属性 通常不比直接使用ScrollBar控件.更高级的ScrollViewer控件(封装了两个Scro

Flask 教程 第十四章:Ajax

本文翻译自The Flask Mega-Tutorial Part XIV: Ajax 这是Flask Mega-Tutorial系列的第十四部分,我将使用Microsoft翻译服务和少许JavaScript来添加实时语言翻译功能. 在本章中,我将从服务器端开发的“安全区域”脱离,研究与服务器端同样重要的客户端组件的功能. 你是否看到过某些网站在用户生成的内容旁边显示的“翻译”链接? 这些链接会触发非用户本地语言内容的实时自动翻译. 翻译的内容通常插入原始版本的下方. Google将其显示为外语

C#图解教程 第十九章 LINQ

LINQ 什么是LINQLINQ提供程序 匿名类型 方法语法和查询语法查询变量查询表达式的结构 from子句join子句什么是联结查询主体中的from-let-where片段 from子句let子句where子句 orderby子句select-group子句查询中的匿名类型group子句查询延续:into子句 标准查询运算符 标准查询运算符的签名查询表达式和标准查询运算符将委托作为参数LINQ预定义的委托类型使用委托参数的示例使用Lamba表达式参数的示例 LINQ to XML 标记语言XM

【读书笔记】C#高级编程 第二十四章 文件和注册表操作

(一)文件和注册表 对于文件系统操作,相关的类几乎都在System.IO名称空间中,而注册表操作由System.Win32名称空间中的类来处理. (二)管理文件系统 System.MarshalByRefObject--这是.NET类中用于远程操作的基对象类,它允许在应用程序域之间编组数据. FileSystemInfo--这是表示任何文件系统对象的基类. FileInfo和File--这些类表示文件系统上的文件. DirectoryInfo和Directory--这些类表示文件系统上的文件夹.

C#图解教程 第十五章 接口

接口 什么是接口 使用IComparable接口的示例 声明接口实现接口 简单接口示例 接口是引用类型接口和as运算符实现多个接口实现具有重复成员的接口多个接口的引用派生成员作为实现显式接口成员实现 访问显式接口成员实现 接口可以继承接口不同类实现一个接口的示例 接口 什么是接口 接口是指定一组函数成员而不实现它们的引用类型.所以只能类和结构来实现接口. 这种描述比较抽象,直接来看个示例. 下例中,Main方法创建并初始化了一个CA类的对象,并将该对象传递给PrintInfo方法. class