c#特性与反射
特性
定义:在程序运行时传递程序中各种元素(比如类、方法、结构、枚举、组件等)行为信息的声明性标签。
看定义,一脸懵逼,那这玩意究竟有什么用呢?
举个例子:比如我们有一个开发平台,向外部提供了API;然后,当我们更新时,想使用一个新的方法代替其中一个方法;这时我们总不能直接把老方法删了吧,但是我们又不想让人家用老方法了,怎么办呢?这时候,特性就可以帮助我们在人家调用老方法时提醒人。怎么用呢?呐,示例代码:
1 | [Obsolete("Don't use OldMethod(), use NewMethod()", false)] |
Obsolete是.Net提供的三个预定义特性之一,这个预定义特性标记了不应该被使用的程序实体。它可以让您通知编译器丢弃某个特定的目标元素。语法为:[Obsolete(message,iserror)]
- message:字符串参数,描述项目实体过时的原因以及该使用什么替代。
- iserror,布尔参数。为 true 时,编译器把对该实体的使用当作一个错误。默认值是 false(当做警告)。
.Net提供了两种类型的特性:预定义特性,自定义特性。
个人理解:预定义特性是在预处理阶段起作用的特性。
预定义特性
除了Obsolete,另外两个预定义特性是AttributeUsage与Conditional。
AttributeUsage
语法如下:
1 | [AttributeUsage( |
这位的作用是描述了如何使用一个自定义特性。可以理解成用它来约束我们编写的自定义特性。看参数就知道了:
- validon:规定特性可“修饰”的语言元素(类、方法…)。它是枚举器 AttributeTargets 的值的组合,使用OR(|)运算法组合;默认值是 AttributeTargets.All。
- allowmultiple:为该特性的 AllowMultiple 属性提供一个布尔值。为 true 时该特性是多用的( 即我们的定制特性能被重复放置在同一个程序实体(语言元素)前多次。)。默认值是 false(单用的)
- inherited:为该特性的 Inherited 属性提供一个布尔值。为 true 时该特性可被派生类继承。默认值是 false(不被继承);这里的继承指当被这个特性“修饰”的类被继承时,其子类是否也继承了此特性。
示例代码:
1 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, |
上述代码中,我们自定义了一个特性MyHelpAttribure,并用预定义特性AttributeUsage约束它只能放置在类和方法前面,可重复放置,可被继承。(自定义特性其实就是一个继承了System.Attribute类的类)
Conditional
Conditional 在命名空间 System.Diagnostics 下,语法如下:[Conditional(conditionalSymbol)]
- conditionalSymbol:字符串类型,为预处理标识符的名称。
这个特性只能放置在特性和方法之前。 这个预定义特性标记了一个条件方法,其执行依赖于指定的预处理标识符,它会引起方法调用的条件编译。 和预编译指令 #if、#endif 作用相同。不同的是#if、#endif作用于代码段,Conditional作用于方法。
示例代码:
1 | using System; |
上面的代码在Debug环境里执行结果为:
Method1
Method2
在Release环境里执行结果为:
Method2
因为在Debug环境下定义的有 预处理标识符 DEBUG。
自定义特性
前面说过,自定义特性就是一个继承了System.Attribute的类。一般我们将自定义特性用于存储声明性的信息,可在运行时通过反射技术检索这些信息。
下面,通过一个示例来了解一下自定义特性。
反射
定义:反射提供描述程序集、模块和类型的对象(Type 类型)。 可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型,然后调用其方法或访问其字段和属性。 如果代码中使用了特性,可以利用反射来访问它们。
通俗点讲,反射能够在获得对象的内部结构。可以理解成“给一个对象做b超”(这个对象是 Type 类型的对象实例,不是人啊,别想多了)。
在此之前,要了解一些名词。
- 程序集、模块、与类型。
.Net应用程序由“程序集”,“模块”,“类型”组成,程序集包含模块,模块包含类型。 - 命名空间与装配件的关系。
名空间类似与Java的包,但又不完全等同,因为Java的包必须按照目录结构来放置,命名空间则不需要。
装配件,是.Net应用程序执行的最小单位,编译出来的.dll、.exe都是一个装配件。一个程序集中可以有无数个名称不同的命名空间,不同程序集之间可以有名字相同的命名空间。
关于装配件与程序集,是同一个概念(都是翻译惹得祸吧,英文:Assembly),这个不用纠结ヽ(ー_ー)ノ。
以上名词来自网络资料,如有不对,再做更改
System.Type 类是反射的中心。 当反射提出请求时,公共语言运行时为已加载的类型创建 Type。 可使用 Type 对象的方法、字段、属性和嵌套类来查找该类型的任何信息。
继承关系:Object->MemberInfo->Type
- 获取对象的三种方式
1 | Type t = typeof(String); |
使用 Assembly.GetType 或 Assembly.GetTypes 从尚未加载的程序集中获取 Type 对象,传入所需类型的名称。 使用 Type.GetType 从已加载的程序集中获取 Type 对象。 使用 Module.GetType 和 Module.GetTypes 获取模块 Type 对象。
如何使用反射访问特性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72//自定义特性类
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
class DebugInfo : Attribute
{
private int bugNo; //调错编号
private string lastReview; //最后一次排错时间
private string message; //描述
public DebugInfo(int iNo, String sLR, String sMsg)
{
bugNo = iNo;
lastReview = sLR;
message = sMsg;
}
public int BugNo
{
get { return bugNo; }
}
public String LastReview
{
get { return lastReview; }
}
public String Message
{
get { return message; }
}
}
[DebugInfo(17, "22/04/2019", "fx")]
[DebugInfo(16, "18/04/2019", "fx")]
class Test
{
[DebugInfo(17, "22/04/2019", "fx")]
void Method1()
{
}
[DebugInfo(16, "18/04/2019", "fx")]
public void Method2()
{
}
}
class Program
{
static void Main(string[] args)
{
Type type = typeof(Test);
///1、遍历类Test的特性
foreach (Object obj in type.GetCustomAttributes(false))
{
DebugInfo dbInfo = (DebugInfo)obj;
if(dbInfo != null)
{
Console.WriteLine("bug no: {0}", dbInfo.BugNo);
Console.WriteLine("last review: {0}", dbInfo.LastReview);
Console.WriteLine("message: {0}", dbInfo.Message);
}
}
///2、遍历Test类中方法的特性
foreach(MethodInfo mi in type.GetMethods())
{
foreach (Object obj in mi.GetCustomAttributes(false))
{
DebugInfo dbInfo = (DebugInfo)obj;
if (dbInfo != null)
{
Console.WriteLine("bug no: {0}", dbInfo.BugNo);
Console.WriteLine("last review: {0}", dbInfo.LastReview);
Console.WriteLine("message: {0}", dbInfo.Message);
}
}
}
Console.ReadKey();
}
}使用反射查看类型信息
1 | class Program |
- 使用反射获取及调用构造方法
1 | class Test |
BindingFlags是一个枚举类型,常用枚举值如下:
- BindingFlags.Public : 指定公开成员将被包含在搜索中
- BindingFlags.Static :指定静态成员将被包含在搜索中
- BindingFlags.NonPublic:指定非公开成员将被包含在搜索中
- BindingFlags.Instance :指定实例成员将被包含在搜索中
其它(方法、字段、属性等)的操作与此类似:
- MemberInfo类是用来获取所有成员 (构造函数、 事件、 字段、 方法和属性) 的类的相关信息的类的抽象基类。
- 使用Assembly定义和加载程序集,加载在程序集清单中列出模块,以及从此程序集中查找类型并创建该类型的实例。
- 使用Module了解包含模块的程序集以及模块中的类等,还可以获取在模块上定义的所有全局方法或其他特定的非全局方法。
- 使用ConstructorInfo了解构造函数的名称、参数、访问修饰符(如pulic 或private)和实现详细信息(如abstract或virtual)等。
- 使用MethodInfo了解方法的名称、返回类型、参数、访问修饰符(如pulic 或private)和实现详细信息(如abstract或virtual)等。
- 使用FiedInfo了解字段的名称、访问修饰符(如public或private)和实现详细信息(如static)等,并获取或设置字段值。
- 使用EventInfo了解事件的名称、事件处理程序数据类型、自定义属性、声明类型和反射类型等,添加或移除事件处理程序。
- 使用PropertyInfo了解属性的名称、数据类型、声明类型、反射类型和只读或可写状态等,获取或设置属性值。
- 使用ParameterInfo了解参数的名称、数据类型、是输入参数还是输出参数,以及参数在方法签名中的位置等。
反射的优缺点:
优点:
- 反射提高了程序的灵活性和扩展性。
- 降低耦合性,提高自适应能力。
- 它允许程序创建和控制任何类的对象,无需提前硬编码目标类。
缺点:
- 性能问题:使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此反射机制主要应用在对灵活性和拓展性要求很高的系统框架上,普通程序不建议使用。
- 使用反射会模糊程序内部逻辑;程序员希望在源代码中看到程序的逻辑,反射却绕过了源代码的技术,因而会带来维护的问题,反射代码比相应的直接代码更复杂。
关于反射的使用,这里只做简单的介绍,深入学习的话请访问反射(c#)编程指南.
c#属性(Property)
定义:属性是一种成员,它提供灵活的机制来读取、写入或计算私有字段的值。 属性可用作公共数据成员,但它们实际上是称为访问器的特殊方法。 这使得可以轻松访问数据,还有助于提高方法的安全性和灵活性。
访问器包括get和set,通过get来返回某个字段的值,在set中,使用关键字value为某个字段进行赋值。属性中至少有一个访问器,可以只有一个get或set访问器,或者既有get又有set访问器。
可以用访问限制修饰符修饰访问器,不过限制级别要大于属性的限制修饰符。
- 限制级别:private > internal和protected > public
1 | class Test |
在一些情况下,属性 get 和 set 访问器仅为支持字段赋值或仅从其中检索值,而没有其它附加逻辑。 这是,通过使用自动实现的属性,既能简化代码,还能让 C# 编译器透明地提供支持字段(即我们不用显示声明字段了)。
1 | class Test |
深入学习请访问:属性(c#)
索引器
定义:索引器允许类或结构的实例像数组一样进行索引。 无需显式指定类型或实例成员,即可设置或检索索引值。 类似于属性,不同之处在于索引器的的访问器需要使用参数。
下面是一个简单索引器的使用案例:
1 | class collection |
当然,我们可以在泛型类中来使用索引器。
1 | class SampleCollection<T> |
索引器也可以被重载。
1 | class collection |
关于更多请访问索引器(c#)
c#委托与事件
委托
定义:委托是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用。 在实例化委托时,你可以将其实例与任何具有兼容参数和返回类型的方法相关联。 并能通过委托实例调用方法。
委托具有以下属性:
- 委托类似于 C++ 函数指针,但委托完全面向对象,不像 C++ 指针会记住函数,委托会同时封装对象 + 实例和方法。
- 委托允许将方法作为参数进行传递。
- 委托可用于定义回调方法。
- 委托可以链接在一起;例如,可以对一个事件调用多个方法。
声明一个委托:
1 | public delegate int DgName(int para1, string para2); |
实例化委托并简单使用:
1 | class Program |
上面说过,委托允许将方法作为参数进行传递,因此,我们还可以这样使用委托。
1 | class PrintString |
委托对象可使用加法赋值运算符(“+”或“+=”)进行合并。一个合并委托调用它所合并的两个委托。只有相同类型的委托可被合并。
使用委托的这个有用的特点,您可以创建一个委托被调用时要调用的方法的调用列表。这被称为委托的多播(multicasting),也叫组播。 若要删除调用列表中的方法,使用减法运算符或减法赋值运算符(“-”或“-=”)。
1 | class Program |
更多请访问委托(c#)
事件
定义:类或对象可以通过事件向其他类或对象通知发生的相关事情。 发送(或 引发)事件的类称为“发行者(发布器)” ,接收(或 处理)事件的类称为“订户(订阅器)” 。
- 发布器(publisher) 是一个包含事件和委托定义的对象。事件和委托之间的联系也定义在这个对象中。发布器类的对象调用这个事件,并通知其他的对象。
- 订阅器(subscriber) 是一个接受事件并提供事件处理程序的对象。在发布器(publisher)类中的委托调用订阅器类中的方法(事件处理程序)。
由上面的定义可以看出,事件,实际上就是.Net对观察这模式的应用。
在类中声明事件之前,必须要先声明用于该事件的委托。例:
1 | public delegate void Cry(); |
编写一个实例:当老鼠跑时,狗叫,猫叫并追赶老鼠;因为在这个设计模型中,当老鼠跑时要通知猫追赶它,通知狗叫,因此老鼠类就是“发布器”,狗类和猫类就是“订阅器”。
1 | using System; |
跟多关于事件(c#)