• 友链

  • 首页

  • 文章归档
h u a n b l o g
h u a n b l o g

欢

HI,Friend

04月
16
C#

特性

发表于 2022-04-16 • 字数统计 14345 • 被 1,412 人看爆

元数据和反射

  • 元数据:有关程序及其类型的数据,保存在程序的程序集中
  • 反射:一个运行的程序查看本身的元数据及类型的所有特性和成员

Type类

Type是个抽象类,用来包含类型的特性,以及获取程序使用类型的信息

System.Type类部分成员

成员成员类型描述
Name属性返回类型的名称
Namespace属性返回包含类型声明的命名空间
Assembly属性返回声明类型的程序集。如果类型是泛型的,返回定义这个类型的程序集
GetFields方法返回类型的字段列表
GetProperties方法返回类型的属性列表
GetMethods方法返回类型的方法列表

获取Tyep对象
通过GetType和typeof运算符来获取对象

Type t = myInstance.GetType();

示例

class BaseClass
{
    public int BaseField = 0;
}

class DerivedClass:BaseClass
{
    public int DerivedField = 0;
}

class Program
{
    static void Main(string[] args)
    {
        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();     //对字段元数据的访问权限 引入命名空间:using System.Reflection;
            foreach(var f in fi)
            {
                Console.WriteLine("        field: {0}", f.Name);
            }

        }

        Console.ReadKey();
    }
}

结果

Object type: BaseClass
field: BaseField
Object type: DerivedClass
field: DerivedField
field: BaseField

特性

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

使用特性相关组件.png

  • 将应用了特性的程序结构叫做目标。
  • 设计用来获取和使用元数据的程序(比如对象浏览器)叫做特性的消费者。

过程

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

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

应用特性

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

例

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

[ Serializable ]        //特性

public class MyClass
{
    ....
}
[MyAtttribute("Simple class", "Version 3.57")]      //带参数的特性
public class MyOtherClass
{
    ...
}


注意

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

预定义的保留特性

Obsolete特性

用于将程序结构(如调用的方法)标注为过期,并且在代码编译时显示有用的警告消息

例

[Obsolete("请使用SuperPrintOut方法")]        //将特性应用到方法上
static void PrintOut(string str)
{
    Console.WriteLine(str);
}

static void Main(string[] args)
{
    PrintOut("Main方法"); //调用Obsolete方法

}

这样调用Obsolete方法时,编辑器会给出警告,但不会影响运行

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

[Obsolete("请使用SuperPrintOut方法", true)]        //将特性应用到方法上
static void PrintOut(string str)
{
    Console.WriteLine(str);
}

static void Main(string[] args)
{
    PrintOut("Main方法"); //调用Obsolete方法

}

这样程序就会报错

Conditional特性

Conditional特性允许我们包括或排斥特定方法的所有调用。为方法声明应用Conditional特性并把编译符作为参数来使用。
命名空间:using System.Diagnostics;

注意

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

例1
  在如下的代码中,把Conditional特性应用到对一个叫做TraceMessage的方法的声明上特性只有一个参数,在这里是字符串DoTrace。
  当编译器编译这段代码时,它会检查是否有一个编译符号被定义为DoTrace。
  如果DoTrace被定义,编译器就会像往常一样包含所有对TraceHessage方法的调用。
  如果没有DoTrace这样的编译符号被定义,编译器就不会输出任何对TraceMessage的调用代码。


[Conditional("DoTrace")]
static void TraceMessage(string str)
{
    Console.WriteLine(str);
}

static void Main(string[] args)
{
    TraceMessage("Hello");
    Console.ReadKey();

}

未打印,由于DoTrace未定义,所以不会调用TraceMessage方法

例2

#define DoTrace

[Conditional("DoTrace")]
static void TraceMessage(string str)
{
    Console.WriteLine(str);
}

static void Main(string[] args)
{
    TraceMessage("Hello");
    Console.ReadKey();

}

结果

Hello

分析

1.Main方法包含了两个对TraceMessage方法的调用。
2.TraceMessage方法的声明被用Conditional特性装饰,它带有DoTrace编译符号作为参数。因此,如果DoTrace被定义,那么编译器就会包含所有对TraceNessage的调用代码。
3.由于代码的第一行定义了叫做DoTrace的编译符,编译器会包含两个对TraceMessage的调用。

调用者信息特性

调用者信息特性可以访问文件路径、代码行数、调用成员的名称等源代码信息。
命名空间:using System.Runtime.CompilerServices;

规则

  • 这三个特性名称为CallerFilePath、CallerLineNumber和CallerMemberName。
  • 这些特性只能用于方法中的可选参数。

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

public static void MyTrace(string message, [CallerFilePath] string fileName = "", 
                                        [CallerLineNumber] int lineNumer = 0,
                                        [CallerMemberName] string callingMember = "")
{
    Console.WriteLine("文件路径:{0}", fileName);
    Console.WriteLine("行数:{0}", lineNumer);
    Console.WriteLine("调用函数:{0}", callingMember);
    Console.WriteLine("message:{0}", message);
}



static void Main(string[] args)
{
    MyTrace("Simple Message");
    Console.ReadKey();

}

结果

文件路径:X:\XXX\XXX\XXXX\Program.cs
行数:42
调用函数:Main
message:Simple Message

DebuggerStepThrough特性

DebuggerstepThrough特性告诉调试器在执行目标代码时不要进人该方法调试。

注意

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

示例
下列代码展示,调式器(断点)不会进入IncrementFields方或X属性的set访问器

int _x = 1;
int X
{
    get { return _x; }

    [DebuggerStepThrough]       //编辑器调试不进入set访问器
    set
    {
        _x = _x * 2;
        _x += value;
    }
}

public int Y { get; set; }

[DebuggerStepThrough]       //编辑器调试不进入该方法
void IncrementFields()
{
    X++;
    Y++;
}

static void Main(string[] args)
{
    Program p = new Program();
    p.IncrementFields();
    p.X = 5;
    Console.WriteLine("X = {0}, Y = {1}", p.X, p.Y);
    Console.ReadKey();

}

其它预定义特性

特性意义
CLSCompliant声明可公开的成员应该被编译器检测是否符合CLS。兼容的程序集可以被任何.NET兼容的语言使用
Serializable声明结构可以被序列化
NonSerialized声明结构不能被序列化
DLLImport声明是非托管代码实现的
WebMethod声明方法应该被作为XML Web服务的一部分暴露
AttributeUsage声明特性能应用到什么类型的程序结构。将这个特性应用到特性声明上

特性的其它内容

多个特性

  • 多个特性可以使用下面列出的任何一种格式:
    • 独立的特性片段相互叠在一起;
    • 单个特性片段,特性之间使用逗号分隔。
  • 我们可以以任何次序列出特性。
[Serializable]      //多层结构
[MyAttribute("Simple class", "Version 3.57")]
[MyAttribute("Simple class", "Version 3.57"), Serializable]     //逗号分隔

其它类型目标

将特性应用到诸如字段和属性等其他程序结构。

[MyAttribute("Hold 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 represets...", "Version 3.6")]
public long ReturnSetting(){
    ...
}

特性目标

event field
method param
property return
type typevar
assembly module

全局特性

通过使用assembly和module目标名称来使用显式目标说明符把特性设置在程序集或模块级别

要点

  • 程序级级别的特性必须放置在任何命名空间之外,并且通常放置在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 0 McArthur widgets 2012")]
[assembly: AssemblyTrademark("")]
[assembly: Assemblyculture("")]

自定义特性

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

声明自定义特性

规则

  • 要声明一个自定义特性,需要做如下工作。
    • 声明一个派生自System.Attribute的类。
    • 给它起一个以后缀Attribute结尾的名字。
  • 安全起见,通常建议你声明一个sealed的特性类。

例

//MyAttributeAttribute特性名    Attribute后缀
//System.Attribute 基类
public sealed class MyAttributeAttribute: System.Attribute 
{
    ...
} 

特性类的公共成员只能是:字段、属性、构造函数

使用特性的构造函数

注意

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

指定构造函数

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

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

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

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

要点

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

[MyAttr()]
class OtherClass...

使用构造函数

//命令语句
MyClass mc = new MyClass("Hello", 15);

//特性声明语句
[MyAttribute("Hello")]

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

和普通类的方法与构造方法相似,特性的构造方法同样可以使用位置参数和命名参数。
//位置参数部分是""
//命名参数部分是=
[MyAttribute("An excellent class",Reviewer="Amy McArthur",Ver="0.7.15.33")]

例

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")]
static void Main(string[] args)
{
    ...
}

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

限制特性的使用

特性本身是类,用AttributeUsage特性来限制某个目标类型上

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

[AttributeUsage(AttributeTarget.Method)]
public seald class MyAttributeAttribute: System.Attibute
{...}

AttributeUsage的公共属性

名字意义默认值
ValidOn保存特性能应用到的目标类型的列表。构造函数的第一个参数必须是AttributeTarget类型的枚举值
Inherited一个布尔值,它指示特性是否会被装饰类型的派生类所继承true
AllowMutiple一个指示目标是否被应用多个特性的实例的布尔值false

AttributeUsage的构造函数

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

例

[AttributeUsage(AttributeTarget.Method | AttributeTarget.Constructor)]
public seald class MyAttributeAttribute: System.Attibute
{...}

AttributeTarget枚举成员

All Assembly Class Constructor
Delegate Enum Event Field
GenericParameter Interface Method Module
Parameter Property ReturnValue Struct

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

例
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;
    }
}

可访问特性

使用IsDefined方法

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

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

  • (1)首先,Main创建了类的一个对象。然后通过使用从object基类继承的GetType方法获取了Type对象的一个引用。
  • (2)有了Type对象的引用,就可以调用IsDefined方法来判断ReviewComment特性是否应用到了这个类。
    • 第一个参数接受需要检查的特性的Type对象。
    • 第二个参数是bool类型的,它指示是否搜索MyClass的继承树来查找这个特性。
[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;
    }
}

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

}

class Program
{
    static void Main(string[] args)
    {
        MyClass 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);

        Console.ReadKey();
    }
}

结果

ReviewComment is applied to type: MyClass

使用GetCustomAttributes方法

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

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

[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(string[] args)
    {
        Type t = typeof(MyClass);
        object[] AttAr = t.GetCustomAttributes(false);
        foreach(Attribute a in AttAr)
        {
            MyAttributeAttribute 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);
            }
        }

        Console.ReadKey();
    }
}

结果

Description:Check it Out
Version Number:2.4
Reviewer ID:

分享到:
C#字符串
预处理指令
  • 文章目录
  • 站点概览
欢

网红 欢

你能抓到我么?

Email RSS
看爆 Top5
  • mac系统版本与Xcode版本有冲突 4,084次看爆
  • JAVA_HOME环境配置问题 3,734次看爆
  • AssetBundle使用 3,503次看爆
  • VSCode配置C++开发环境 3,260次看爆
  • Lua反射 3,136次看爆

Copyright © 2025 欢 粤ICP备2020105803号-1

由 Halo 强力驱动 · Theme by Sagiri · 站点地图