c#特性与反射

特性

定义:在程序运行时传递程序中各种元素(比如类、方法、结构、枚举、组件等)行为信息的声明性标签

看定义,一脸懵逼,那这玩意究竟有什么用呢?
举个例子:比如我们有一个开发平台,向外部提供了API;然后,当我们更新时,想使用一个新的方法代替其中一个方法;这时我们总不能直接把老方法删了吧,但是我们又不想让人家用老方法了,怎么办呢?这时候,特性就可以帮助我们在人家调用老方法时提醒人。怎么用呢?呐,示例代码:

1
2
3
4
5
6
7
8
9
[Obsolete("Don't use OldMethod(), use NewMethod()", false)]
public void OldMethod()
{
Console.WriteLine("老方法");
}
public void NewMethod()
{
Console.WriteLine("新方法");
}

Obsolete是.Net提供的三个预定义特性之一,这个预定义特性标记了不应该被使用的程序实体。它可以让您通知编译器丢弃某个特定的目标元素。语法为:[Obsolete(message,iserror)]

  • message:字符串参数,描述项目实体过时的原因以及该使用什么替代。
  • iserror,布尔参数。为 true 时,编译器把对该实体的使用当作一个错误。默认值是 false(当做警告)。

.Net提供了两种类型的特性:预定义特性,自定义特性。
个人理解:预定义特性是在预处理阶段起作用的特性。

预定义特性

除了Obsolete,另外两个预定义特性是AttributeUsage与Conditional。
AttributeUsage
语法如下:

1
2
3
4
5
[AttributeUsage(
validon,
AllowMultiple=allowmultiple,
Inherited=inherited
)]

这位的作用是描述了如何使用一个自定义特性。可以理解成用它来约束我们编写的自定义特性。看参数就知道了:

  • validon:规定特性可“修饰”的语言元素(类、方法…)。它是枚举器 AttributeTargets 的值的组合,使用OR(|)运算法组合;默认值是 AttributeTargets.All。
  • allowmultiple:为该特性的 AllowMultiple 属性提供一个布尔值。为 true 时该特性是多用的( 即我们的定制特性能被重复放置在同一个程序实体(语言元素)前多次。)。默认值是 false(单用的)
  • inherited:为该特性的 Inherited 属性提供一个布尔值。为 true 时该特性可被派生类继承。默认值是 false(不被继承);这里的继承指当被这个特性“修饰”的类被继承时,其子类是否也继承了此特性。
    示例代码:
1
2
3
4
5
6
7
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method,
AllowMultiple = true,
Inherited = true)]
class MyHelpAttribure : Attribute
{
......
}

上述代码中,我们自定义了一个特性MyHelpAttribure,并用预定义特性AttributeUsage约束它只能放置在类和方法前面,可重复放置,可被继承。(自定义特性其实就是一个继承了System.Attribute类的类)

Conditional
Conditional 在命名空间 System.Diagnostics 下,语法如下:[Conditional(conditionalSymbol)]

  • conditionalSymbol:字符串类型,为预处理标识符的名称。
    这个特性只能放置在特性方法之前。 这个预定义特性标记了一个条件方法,其执行依赖于指定的预处理标识符,它会引起方法调用的条件编译。 和预编译指令 #if、#endif 作用相同。不同的是#if、#endif作用于代码段,Conditional作用于方法。
    示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;
namespace cSharpReview
{
class Program
{
[System.Diagnostics.Conditional("DEBUG")]
void Method1()
{
Console.WriteLine("Method1");
}
void Method2()
{
Console.WriteLine("Method2");
}
static void Main(string[] args)
{
Program pro = new Program();
pro.Method1();
pro.Method2();
Console.ReadKey();
}
}
}

上面的代码在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. 获取对象的三种方式
1
2
3
4
Type t = typeof(String);
string str = string.Empty;
Type t = str.GetType();
Type t = Type.GetType("System.String");

使用 Assembly.GetType 或 Assembly.GetTypes 从尚未加载的程序集中获取 Type 对象,传入所需类型的名称。 使用 Type.GetType 从已加载的程序集中获取 Type 对象。 使用 Module.GetType 和 Module.GetTypes 获取模块 Type 对象。

  1. 如何使用反射访问特性

    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();
    }
    }
  2. 使用反射查看类型信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Program
{
static void Main(string[] args)
{
Type t = Type.GetType("System.String");
///获取所有成员(构造函数、 事件、 字段、 方法和属性)信息
foreach (MemberInfo mi in t.GetMembers())
{
Console.WriteLine(mi.Name);
}
///获取所有成员方法信息
foreach (MethodInfo mi in t.GetMethods())
{
Console.WriteLine(mi.Name);
}
//......
Console.ReadKey();
}
}
  1. 使用反射获取及调用构造方法
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
class Test
{
string str = string.Empty;
private Test()
{
}
public Test(string str)
{
this.str = str;
}
public void Method()
{
Console.WriteLine(str);
}
}
class Program
{
static void Main(string[] args)
{
Type t = typeof(Test);
//根据构造函数的参数类型获取构造函数,为空的话传入 Type.EmptyTypes;
Type[] para = new Type[1];
para[0] = typeof(string);
//获取有一个string参数的公有构造函数
ConstructorInfo ct = t.GetConstructor(para);
//构造Object数组,作为传入的参数组合(无参设置为null)
object[] ojbs = new object[1] { "fx" };
//调用构造函数创建对象
if(ct != null)
{
object test = ct.Invoke(ojbs);
((Test)test).Method();
}

//获取无参数的私有构造函数
ConstructorInfo _ct = t.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic,
null, Type.EmptyTypes, null);
if(_ct != null)
{
object test = _ct.Invoke(null);
((Test)test).Method();
}

///使用Activator生成对象
Object theObj2 = Activator.CreateInstance(t, "fx"); //调用有参构造函数
((Test)theObj2).Method();
//调用 私有 无参构造函数
Object theObj3 = Activator.CreateInstance(t, true);
((Test)theObj3).Method();
Console.ReadKey();
}
}

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了解参数的名称、数据类型、是输入参数还是输出参数,以及参数在方法签名中的位置等。

反射的优缺点:

  • 优点:

    1. 反射提高了程序的灵活性和扩展性。
    2. 降低耦合性,提高自适应能力。
    3. 它允许程序创建和控制任何类的对象,无需提前硬编码目标类。
  • 缺点:

    1. 性能问题:使用反射基本上是一种解释操作,用于字段和方法接入时要远慢于直接代码。因此反射机制主要应用在对灵活性和拓展性要求很高的系统框架上,普通程序不建议使用。
    2. 使用反射会模糊程序内部逻辑;程序员希望在源代码中看到程序的逻辑,反射却绕过了源代码的技术,因而会带来维护的问题,反射代码比相应的直接代码更复杂。

关于反射的使用,这里只做简单的介绍,深入学习的话请访问反射(c#)编程指南.

c#属性(Property)

定义:属性是一种成员,它提供灵活的机制来读取、写入或计算私有字段的值。 属性可用作公共数据成员,但它们实际上是称为访问器的特殊方法。 这使得可以轻松访问数据,还有助于提高方法的安全性和灵活性。

访问器包括get和set,通过get来返回某个字段的值,在set中,使用关键字value为某个字段进行赋值。属性中至少有一个访问器,可以只有一个get或set访问器,或者既有get又有set访问器。
可以用访问限制修饰符修饰访问器,不过限制级别要大于属性的限制修饰符。

  • 限制级别:private > internal和protected > public
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Test
{
//字段
private int field;
//属性
public int Field {
get { return field; }
set { field = value; }
}
}
class Program
{
static void Main(string[] args)
{
Test test = new Test();
//通过属性为field赋值
test.Field = 66;
//通过属性获取字段field的值
Console.WriteLine("Test.field:{0}", test.Field);
Console.ReadKey();
}
}

在一些情况下,属性 get 和 set 访问器仅为支持字段赋值或仅从其中检索值,而没有其它附加逻辑。 这是,通过使用自动实现的属性,既能简化代码,还能让 C# 编译器透明地提供支持字段(即我们不用显示声明字段了)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Test
{
//自动属性
public int Pro { get; set; }
}
class Program
{
static void Main(string[] args)
{
Test test = new Test();
test.Pro = 66;
Console.WriteLine("Test.Pro:{0}", test.Pro);
Console.ReadKey();
}
}

深入学习请访问:属性(c#)

索引器

定义:索引器允许类或结构的实例像数组一样进行索引。 无需显式指定类型或实例成员,即可设置或检索索引值。 类似于属性,不同之处在于索引器的的访问器需要使用参数。

下面是一个简单索引器的使用案例:

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
class collection
{
static int Count = 100;
private string[] arr = new string[Count];

public string this[int i]
{
get {
if(i >= 0 && i < 100)
return arr[i];
return null;
}
set {
if (i >= 0 && i < 100)
arr[i] = value;
}
}
}

class Program
{
static void Main()
{
var stringCollection = new collection();
stringCollection[0] = "fx";
Console.WriteLine(stringCollection[0]);
Console.ReadKey();
}
}

当然,我们可以在泛型类中来使用索引器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class SampleCollection<T>
{
private T[] arr = new T[100];

public T this[int i]
{
get { return arr[i]; }
set { arr[i] = value; }
}
}

class Program
{
static void Main()
{
var stringCollection = new SampleCollection<string>();
stringCollection[0] = "Hello, World";
Console.WriteLine(stringCollection[0]);
Console.ReadKey();
}
}

索引器也可以被重载。

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
class collection
{
private string[] arr = new string[100];

public string this[int i]
{
get { return arr[i]; }
set { arr[i] = value; }
}
//重载索引器
public int this[string val]
{
get
{
int index = 0;
while (index < 100)
{
if (arr[index] == val)
{
return index;
}
index++;
}
return -1;
}
}
}

class Program
{
static void Main()
{
var stringCollection = new collection();
stringCollection[0] = "fx";
Console.WriteLine(stringCollection[0]);
Console.WriteLine(stringCollection["fx"]);
Console.ReadKey();
}
}

关于更多请访问索引器(c#)

c#委托与事件

委托

定义:委托是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用。 在实例化委托时,你可以将其实例与任何具有兼容参数和返回类型的方法相关联。 并能通过委托实例调用方法。

委托具有以下属性:

  • 委托类似于 C++ 函数指针,但委托完全面向对象,不像 C++ 指针会记住函数,委托会同时封装对象 + 实例和方法。
  • 委托允许将方法作为参数进行传递。
  • 委托可用于定义回调方法。
  • 委托可以链接在一起;例如,可以对一个事件调用多个方法。

声明一个委托:

1
public delegate int DgName(int para1, string para2);

实例化委托并简单使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Program
{
public delegate int DgName(int para1);
public int Method(int para1)
{
return para1 / 2;
}
static void Main()
{
Program pro = new Program();
//实例化一个委托myDg
DgName myDg = pro.Method;
//通过委托调用pro.Method
Console.WriteLine(myDg(66)); //33
Console.ReadKey();
}
}

上面说过,委托允许将方法作为参数进行传递,因此,我们还可以这样使用委托。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class PrintString
{
// 委托声明
public delegate void DgName(string s);

public static void WriteToScreen(string str)
{
Console.WriteLine("The String is: {0}", str);
}
// 该方法把委托作为参数,并使用它调用方法
public static void sendString(DgName ps)
{
ps("Hello World");
}
static void Main(string[] args)
{
//可以这样实例化委托变量
DgName ps1 = new DgName(WriteToScreen);
sendString(ps1);
Console.ReadKey();
}
}

委托对象可使用加法赋值运算符(“+”或“+=”)进行合并。一个合并委托调用它所合并的两个委托。只有相同类型的委托可被合并。
使用委托的这个有用的特点,您可以创建一个委托被调用时要调用的方法的调用列表。这被称为委托的多播(multicasting),也叫组播。 若要删除调用列表中的方法,使用减法运算符或减法赋值运算符(“-”或“-=”)。

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
class Program
{
public delegate void Dlg();

public static void DogCry()
{
Console.WriteLine("汪汪汪~");
}
public static void CatCry()
{
Console.WriteLine("喵喵~");
}
public static void SheepCry()
{
Console.WriteLine("咩 ~");
}
public static void AnimalCry(Dlg dlg)
{
dlg();
}
static void Main()
{
Dlg dlg =DogCry;
dlg += CatCry;
Dlg anoDlg = new Dlg(SheepCry);
dlg = dlg + anoDlg;
AnimalCry(dlg);
/**
* 输出:
* 汪汪汪~
* 喵喵~
* 咩 ~
*/
Console.ReadKey();
}
}

更多请访问委托(c#)

事件

定义:类或对象可以通过事件向其他类或对象通知发生的相关事情。 发送(或 引发)事件的类称为“发行者(发布器)” ,接收(或 处理)事件的类称为“订户(订阅器)” 。

  • 发布器(publisher) 是一个包含事件和委托定义的对象。事件和委托之间的联系也定义在这个对象中。发布器类的对象调用这个事件,并通知其他的对象。
  • 订阅器(subscriber) 是一个接受事件并提供事件处理程序的对象。在发布器(publisher)类中的委托调用订阅器类中的方法(事件处理程序)。

由上面的定义可以看出,事件,实际上就是.Net对观察这模式的应用。

在类中声明事件之前,必须要先声明用于该事件的委托。例:

1
2
3
public delegate void Cry();
//基于委托声明事件
public event Cry MouseRun;

编写一个实例:当老鼠跑时,狗叫,猫叫并追赶老鼠;因为在这个设计模型中,当老鼠跑时要通知猫追赶它,通知狗叫,因此老鼠类就是“发布器”,狗类和猫类就是“订阅器”。

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
using System;

namespace StudyNotes
{
//;老鼠类
class Mouse
{
public delegate void Cry();
//基于委托声明事件
public event Cry MouseRun;

//老鼠跑
public void Run()
{
//向“订阅器”发送消息
MouseRun();
}
}
//狗类
class Dog
{
public void Cry()
{
Console.WriteLine("狗:猫赶紧抓老鼠去!");
}
}
//猫类
class Cat
{
public void RunAndCry()
{
Console.WriteLine("猫:小老鼠,哪里跑!");
}
}
class Program
{
static void Main()
{
Mouse mouse = new Mouse();
Dog dog = new Dog();
Cat cat = new Cat();
//猫和狗“订阅”老鼠的跑事件
mouse.MouseRun += dog.Cry;
mouse.MouseRun += cat.RunAndCry;
//老鼠跑事件发生
mouse.Run();
/**
* 输出:
* 狗:猫赶紧抓老鼠去!
* 猫:小老鼠,哪里跑!
* */
Console.ReadKey();
}
}
}

跟多关于事件(c#)


 评论