这次实验是单纯的对C#反射特性的学习,没什么特别的。

前期准备

实验要求

3.2 构建一个components.txt文件,每行代表一个类(比如形状类Shape)的名字(这些类可以来自系统库,或者自己构造);程序逐行扫描该文件,构建对应的类的对象。要求:

1)并把这些对象放到数组中;

  1. 列出每个类的字段、方法;

3)让用户选择,使得用户可以调用所需要的方法(操作)

4)系统随机选择对象(比如形状),随机的执行其操作。从而看系统演化。可能的话,进行界面展示

前置知识

实验要求中需要我们通过类名字实例化对象,且列出其字段与方法,并尝试调用方法。以上这些操作都指向了C#中很重要的一个知识点——反射。

反射提供描述程序集、模块和类型的对象(Type 类型)。 可以使用反射动态地创建类型的实例,将类型绑定到现有对象,或从现有对象中获取类型,然后调用其方法或访问器字段和属性。 如果代码中使用了特性,可以利用反射来访问它们。

如果对反射还不太理解,建议参阅C#官方文档

而从文件中读取的操作则需要System.IO中File类的操作。

对于调用形状库,则需要使用System.Reflection中的Assembly.LoadFrom方法。

有了以上基础知识的积累,就可以开始编写程序了。

实验过程

构建形状库

首先创建一个cs文件,写入形状类代码:

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
73
74
75
76
77
78
79
using System;

namespace ShapeRefl
{
public class Shape //形状基类
{
public const double PI = Math.PI;
protected double x, y;
public Shape() { }
public Shape(double x,double y)
{
this.x = x;
this.y = y;
}
public virtual double GetArea() { return 0; }
public virtual double GetPerimeter() { return 0; }
}

public class Square : Shape //正方形
{
public Square(double x,double y) : base(x, 0) { }
public override double GetArea()
{
return x * x;
}
public override double GetPerimeter()
{
return 4 * x;
}
}
public class Rectangle : Shape //矩形
{
public Rectangle(double x,double y) : base(x, y) { }
public override double GetArea()
{
return x * y;
}
public override double GetPerimeter()
{
return 2 * (x + y);
}
}
public class Triangle : Shape //等腰三角形
{
public Triangle(double x,double y) : base(x, y) { }
public override double GetArea()
{
return 0.5 * x * y;
}
public override double GetPerimeter()
{
return Math.Sqrt(x * x * 0.25 + y * y) * 2 + x;
}
}
public class Circle : Shape //圆形
{
public Circle(double x,double y) : base(x, 0) { }
public override double GetArea()
{
return PI * x * x;
}
public override double GetPerimeter()
{
return 2 * PI * x;
}
}
public class Ellipse : Shape //椭圆形
{
public Ellipse(double x,double y) : base(x, y) { }
public override double GetArea()
{
return PI * x * y;
}
public override double GetPerimeter()
{
return 2 * PI * y + (x - y) * 4;
}
}
}

但设计完了这些类,却不能直接被另一个文件所引用。我们需要将其打包成一个dll库文件。

打开解决方案资源管理器 ,右键项目 ,选择属性 ,打开项目的属性设置界面。

将项目的输出类型设为类库

最后回到解决方案资源管理器 ,右键解决方案 ,点击重新生成解决方案

稍作等待,就能在下方给出的地址中获得生成的dll类库文件了。

先将这个类库文件存下来,取个好记的名字,比如我这里改成了Shape.dll,后续在代码中会用到。

主体代码

调用类库

新创建一个项目,作为我们的代码主体。先随便写点东西生成一次解决方案。此时我们可以在Debug文件夹下找到项目的exe文件,记住这个目录,并将之前那个dll文件移动到此处,同时创建components.txt文件,作为读取文件。

此时回到代码,开始程序的编写:

1
2
Assembly assembly = Assembly.LoadFrom("Shape.dll");
string fileName = "components.txt";

创建一个字符串变量fileName作为读取文件路径,方便后续调用。再创建一个Assembly变量加载之前编写的类库。

同时在Main函数外创建一个全局变量,作为实例化的类对象的数组:

1
static ArrayList clsList = new ArrayList();
读取文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void ReadFromFile(string fileName,Assembly assembly)
{
/*文件流*/
FileStream file = File.OpenRead(fileName);
StreamReader stream = new StreamReader(file, System.Text.Encoding.Default);
file.Seek(0, SeekOrigin.Begin);

string nspName = "ShapeRefl.";
string clsName = "";
object[] ars = { 3, 5 };

while (stream.Peek() > -1)
{
clsName = stream.ReadLine();
object obj = assembly.CreateInstance(nspName + clsName, true, BindingFlags.Default, null, ars, null, null);
clsList.Add(obj);
}
}

首先创建一个读取文件流与一个文件流指针,并从文件头开始读取:

1
2
3
FileStream file = File.OpenRead(fileName);
StreamReader stream = new StreamReader(file, System.Text.Encoding.Default);
file.Seek(0, SeekOrigin.Begin);

接下来开始逐行读入文件名,并由读入的文件名实例化对象以及将其加入数组:

1
object obj = assembly.CreateInstance(nspName + clsName, true, BindingFlags.Default, null, ars, null, null);

这部分的核心是实例化对象的函数CreateInstance()。对于需要写入参数的函数要传入多个参数。大部分参数不用修改,只需特别关注第一与第五个参数:第一个参数为完整的类名,即需要以“命名空间.类名”的形式写入,由于我们的类都在同一个命名空间中,所以无需额外写入,只需修改类名即可。而第五个参数为传入构造函数的参数,用一个object[]来储存,如用ars={3,5}来实例化Square对象,其实就相当于new Square(3,5)。

列出信息

这部分函数可谓展示了反射的大部分内容,是该程序的核心:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void ShowAll()
{
for(int i=0;i<clsList.Count;i++)
{
Type type = clsList[i].GetType();
/*输出类名*/
if (type != null)
Console.WriteLine("- {0}: <{1}>", i+1, type.FullName);
/*输出方法*/
MethodInfo[] methods = type.GetMethods();
Console.WriteLine("\t方法({0})", methods.Length);
foreach (MethodInfo method in methods)
Console.WriteLine("\t|- {0}", method.Name);
/*输出字段*/
FieldInfo[] fields = type.GetFields();
Console.WriteLine("\t字段({0})", fields.Length);
foreach (FieldInfo field in fields)
Console.WriteLine("\t|- {0}", field.Name);
}
}

GetType()函数可以获得当前类的类型,将其存在type变量中。对该变量,可以进行FullName,GetMethods()与GetFields()等操作,分别为获得其完整的名字、获得其所有方法以及获得其所有字段。使用这几个方法,即可完成对其方法与字段的打印。

选择调用
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
static void UserChoose()
{
for (int i = 0; i < clsList.Count; i++)
{
Type type = clsList[i].GetType();
/*输出类名*/
if (type != null)
Console.WriteLine("- {0}: <{1}>", i + 1, type.FullName);
}
Console.WriteLine("请选择要访问的类(1~{0})", clsList.Count);
int clsChoice;
clsChoice = int.Parse(Console.ReadLine())-1;
if (clsChoice >= 0 && clsChoice < clsList.Count)
{
Type type = clsList[clsChoice].GetType();
MethodInfo[] methods = type.GetMethods();
Console.WriteLine("方法({0})", methods.Length);
for (int i = 0; i < methods.Length; i++)
Console.WriteLine("|{0}. {1}", i + 1, methods[i].Name);
Console.WriteLine("请选择要访问的方法(1~{0})", methods.Length);
int mtdChoice;
mtdChoice= int.Parse(Console.ReadLine())-1;
if (mtdChoice >= 0 && mtdChoice < methods.Length && mtdChoice != 4)
Console.WriteLine(methods[mtdChoice].Name + ":" + methods[mtdChoice].Invoke(clsList[clsChoice], null));
}
}

已经实现了以上列出信息的部分,选择调用的部分就不再复杂,无非是做点基础的语法调整。但这里需要额外注意一下:第4号方法(从0开始计数)为Equal方法,为二元方法,无法被直接调用打印,所以需要跳过,否则程序会崩溃。

随机调用

该部分的代码实现也与上述过程如出一辙:

1
2
3
4
5
6
7
8
9
10
11
12
static void SystemRand()
{
Random random = new Random((int)DateTime.Now.Ticks);
int clsChoice = random.Next(0, clsList.Count - 1);
Type type = clsList[clsChoice].GetType();
Console.WriteLine(type.Name);
MethodInfo[] methods = type.GetMethods();
int mtdChoice = random.Next(0, 5);
while (mtdChoice == 4)
mtdChoice = random.Next(0, 5);
Console.WriteLine(methods[mtdChoice].Name + ":" + methods[mtdChoice].Invoke(clsList[clsChoice], null));
}

但需要引入一个随机数生成器Random,其构造函数可以引入一个参数作为种子 ,一般情况下为了接近真正的随机,我们选用当前时间tick数(DateTime.Now.Ticks作为种子。有了这个生成器,我们就可以用Next()方法获取下一个随机数了。值得注意的是,这里也需要避开Equal方法的直接调用。

结尾

完成了以上函数,我们就只需要稍微润色一下Main函数,用文字实现简单的操作交互即可:

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
static void Main(string[] args)
{
Assembly assembly = Assembly.LoadFrom("Shape.dll");
string fileName = "components.txt";
ReadFromFile(fileName, assembly);
string action = "9";
while (action != "0")
{
Console.Clear();
switch (action)
{
case "1":
ShowAll();
break;
case "2":
UserChoose();
break;
case "3":
SystemRand();
break;
default:break;
}
Console.WriteLine("请输入操作代码:");
Console.WriteLine("1.列出所有类的字段与方法");
Console.WriteLine("2.由用户选择操作");
Console.WriteLine("3.系统随机选择操作");
Console.WriteLine("0.退出程序");
action = Console.ReadLine();
}
}

完整代码

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
using System;
using System.IO;
using System.Reflection;
using System.Collections;

namespace testConsole
{
class Program
{
static ArrayList clsList = new ArrayList();
static void Main(string[] args)
{
Assembly assembly = Assembly.LoadFrom("Shape.dll");
string fileName = "components.txt";
ReadFromFile(fileName, assembly);
string action = "9";
while (action != "0")
{
Console.Clear();
switch (action)
{
case "1":
ShowAll();
break;
case "2":
UserChoose();
break;
case "3":
SystemRand();
break;
default:break;
}
Console.WriteLine("请输入操作代码:");
Console.WriteLine("1.列出所有类的字段与方法");
Console.WriteLine("2.由用户选择操作");
Console.WriteLine("3.系统随机选择操作");
Console.WriteLine("0.退出程序");
action = Console.ReadLine();
}
}
static void ReadFromFile(string fileName,Assembly assembly)
{
/*文件流*/
FileStream file = File.OpenRead(fileName);
StreamReader stream = new StreamReader(file, System.Text.Encoding.Default);
file.Seek(0, SeekOrigin.Begin);

string nspName = "ShapeRefl.";
string clsName = "";
object[] ars = { 3, 5 };

while (stream.Peek() > -1)
{
clsName = stream.ReadLine();
object obj = assembly.CreateInstance(nspName + clsName, true, BindingFlags.Default, null, ars, null, null);
clsList.Add(obj);
}
}
static void ShowAll()
{
for(int i=0;i<clsList.Count;i++)
{
Type type = clsList[i].GetType();
/*输出类名*/
if (type != null)
Console.WriteLine("- {0}: <{1}>", i+1, type.FullName);
/*输出方法*/
MethodInfo[] methods = type.GetMethods();
Console.WriteLine("\t方法({0})", methods.Length);
foreach (MethodInfo method in methods)
Console.WriteLine("\t|- {0}", method.Name);
/*输出字段*/
FieldInfo[] fields = type.GetFields();
Console.WriteLine("\t字段({0})", fields.Length);
foreach (FieldInfo field in fields)
Console.WriteLine("\t|- {0}", field.Name);
}
}
static void UserChoose()
{
for (int i = 0; i < clsList.Count; i++)
{
Type type = clsList[i].GetType();
/*输出类名*/
if (type != null)
Console.WriteLine("- {0}: <{1}>", i + 1, type.FullName);
}
Console.WriteLine("请选择要访问的类(1~{0})", clsList.Count);
int clsChoice;
clsChoice = int.Parse(Console.ReadLine())-1;
if (clsChoice >= 0 && clsChoice < clsList.Count)
{
Type type = clsList[clsChoice].GetType();
MethodInfo[] methods = type.GetMethods();
Console.WriteLine("方法({0})", methods.Length);
for (int i = 0; i < methods.Length; i++)
Console.WriteLine("|{0}. {1}", i + 1, methods[i].Name);
Console.WriteLine("请选择要访问的方法(1~{0})", methods.Length);
int mtdChoice;
mtdChoice= int.Parse(Console.ReadLine())-1;
if (mtdChoice >= 0 && mtdChoice < methods.Length && mtdChoice != 4)
Console.WriteLine(methods[mtdChoice].Name + ":" + methods[mtdChoice].Invoke(clsList[clsChoice], null));
}
}
static void SystemRand()
{
Random random = new Random((int)DateTime.Now.Ticks);
int clsChoice = random.Next(0, clsList.Count - 1);
Type type = clsList[clsChoice].GetType();
Console.WriteLine(type.Name);
MethodInfo[] methods = type.GetMethods();
int mtdChoice = random.Next(0, 5);
while (mtdChoice == 4)
mtdChoice = random.Next(0, 5);
Console.WriteLine(methods[mtdChoice].Name + ":" + methods[mtdChoice].Invoke(clsList[clsChoice], null));
}
}
}