C#实际案例分析(第三弹)——By 流星
实验三
题目要求
构建一个components.txt文件,每行代表一个类(比如形状类Shape)的名字(这些类可以来自系统库,或者自己构造);程序逐行扫描该文件,构建对应的类的对象。要求:
把这些对象放到数组中;
列出每个类的字段、方法;
让用户选择,使得用户可以调用所需要的方法或者操作
系统随机选择对象(比如形状),随机的执行其操作,从而看系统演化。可能的话,进行界面展示
环境设置
操作系统: Windows 10 x64
SDK: .NET Framework 4.7.2
IDE: Visual Studio 2019

题意分析
本次实验综合了文件读取、构建动态链接库(dll)、反射等知识点,我们逐渐拆开来看:
①构建一个components.txt文件,每行代表一个类的名字;
②程序逐行扫描该文件,构建对应的类的对象;
①要求实现文件逐行读取的功能,通过C#提供的文件流来实现。而②要求我们通过这些名字来构建类的对象。听起来是不是很高大上?我们在学习C++甚至Python的时候都没有遇到过这样的事情(事实上它们都可以实现)。这里,我们将通过C#提供的反射功能来完成这个要求。我们将在下面进行反射的详解。
③把这些对象放到数组中;
④列出每个类的字段、方法;
⑤让用户选择,使得用户可以调用所需要的方法或者操作;
⑥系统随机选择对象,随机的执行其操作,从而看系统演化。可能的话,进行界面展示;
③要求将这些对象存入数组中。④要求列出每个类的字段和方法。⑤要求让用户能够自己选择需要调用的方法或操作。⑥要求随机执行操作。事实上⑤⑥的要求是一致的,只是⑥将用户自己的选择以随机数的方式代替。我们将在接下来的解析中详细将这些要求具体展开来看。

反射·简介
反射(Reflection)是.NET中重要的机制。通过反射可以在运行时获得.NET中每一个类型(包括类、结构、微投、接口、枚举等)的成员,包括方法、属性、事件和构造函数等,还可以获得每个成员的名称、限定符和参数等。 ——参考文献
也就是说,我们通过某些途径拿到了一个类或其他类型的信息或是它们的某个成员,但是我们对这个东西基本上是一无所知,那我们该怎么知道它有什么东西,以及怎么去使用它?这时候就需要通过反射来获取属性、方法列表等信息,我们再进行调用。反射的原理我们不再多讲,笼统地来说就是通过审查元数据(Meta-Data),调用程序运行时加载到内存中的程序集,动态获取对象的信息。
使用反射时,需要包含反射的头文件,即using System.Reflection;。
反射中最常用到的类是Type类。顾名思义,Type类对象即用来获取某个类或对象的类型信息并支持后续调用。例如:
Type t = Type.GetType("Reflection_exp.Square");
这是十分常用,也是不需要编译时信息即可获取类型的方法。例如上面这条语句获得的对象t就含有Reflection_exp.Square这个类的信息了。除此之外还有每个对象都有的GetType()方法,typeof运算符等其他的方法可以获取Type对象。注意,由于Type是抽象类,因而不可直接创建它的对象。
我们获取到Type对象之后就可以对它进行我们之前说的操作了,例如拿到字段列表,方法列表,方法的参数列表等,他们调用的函数分别是GetFields(), GetMethods(), GetParameters()。用法示例如下:
FieldInfo[] fieldInfos = t.GetFields();
这样,fieldInfos数组就有t所指的类型的所有字段了,其他的几个方法也是相同的道理。
另一个在反射中用到的类是Assembly类。它能够加载、了解和操作一个程序集。在这个实验的刚开始,我们会通过它加载动态链接库/dll,具体的方法我们接下来会介绍。
以上就是反射的大概介绍了。接下来我们将会在代码中讲解怎么运用这些东西。

完整代码
类Graph
namespace Reflection_exp
{
public abstract class Graph
{
protected double x=0,y=0;
public double X
{
get { return x; }
set { x = value; }
}
public double Y
{
get { return y; }
set { y = value; }
}
public abstract double GetArea();
public abstract double GetCir();
}
}
类Graphs
using System;
namespace Reflection_exp
{
class Circle : Graph
{
private double r=0;
public double R
{
get { return r; }
set { r = value; }
}
public override double GetArea()
{
return 3.1415926535*r*r;
}
public override double GetCir()
{
return 2*3.1415926535*r;
}
}
class Oval : Graph
{
private double a = 0,b = 0;
public double A
{
get { return a; }
set { a = value; }
}
public double B
{
get { return b; }
set { b = value; }
}
public override double GetArea()
{
return 3.1415926535*a*b;
}
public override double GetCir()
{
return 2*3.1415926535*b+4*(a-b);
}
}
class Triangle : Graph
{
private double l = 0;
public double L
{
get { return l; }
set { l = value; }
}
public override double GetArea()
{
return Math.Sqrt(3)/4.0*l*l;
}
public override double GetCir()
{
return 3*l;
}
}
class Rectangle : Graph
{
private double a = 0, b = 0;
public double A
{
get { return a; }
set { a = value; }
}
public double B
{
get { return b; }
set { b = value; }
}
public override double GetArea()
{
return a*b;
}
public override double GetCir()
{
return a + a + b + b;
}
}
class Square : Graph
{
private double a = 0;
public double A
{
get { return a; }
set { a = value; }
}
public override double GetArea()
{
return a * a;
}
public override double GetCir()
{
return 4*a;
}
}
}
类Program
using System;
using System.Collections;
using System.Reflection;
namespace Reflection_exp
{
class Program
{
static void Main(string[] args)
{
//文件流
System.IO.StreamReader fp = new System.IO.StreamReader(@".\component.txt");
Assembly assembly = Assembly.LoadFrom(@".\Graphs.dll");//找到程序集
//读入类名
ArrayList lst = new ArrayList();
string nameSpace = @"Reflection_exp.";//命名空间的名字,用于构建类名的全称(注意,必须要全称)
string line = fp.ReadLine();//逐行读入
while (line != null)
{
object obj = assembly.CreateInstance(nameSpace + line);
if (obj == null){
Console.WriteLine("错误类名!");
continue;
}
lst.Add(obj);//直接当做父类存入Array中,调用时通过反射进行调用
Type t = obj.GetType();
Console.WriteLine("{0}", t.FullName);
//获取字段
FieldInfo[] fieldInfos = t.GetFields();
Console.WriteLine("{0}有{1}个字段:", t.Name, fieldInfos.Length);
foreach (FieldInfo i in fieldInfos){
Console.WriteLine("{0}含有字段{1} ", t.Name, i.Name);
}
//获取方法
MethodInfo[] methodInfos = t.GetMethods();
Console.WriteLine("{0}有{1}个方法:", t.Name, methodInfos.Length);
foreach (MethodInfo i in methodInfos){
Console.WriteLine("{0}含有方法{1} ", t.Name, i.Name);
}
Console.WriteLine("*********************************");
line = fp.ReadLine();
}
//演化
short mode = 0;//选择模式
Console.WriteLine("请选择用户自行选择(输入1)或者系统演化(输入2):");
mode = Convert.ToInt16(Console.ReadLine());
switch (mode)
{
case 1://用户操作
Console.WriteLine("用户操作模式:");
while (true)
{
string objName = "";
Console.WriteLine("请输入对象名:");
objName = Console.ReadLine().Trim();
//寻找对象
int i = 0;
for (; i < lst.Count; i++)
{
if (nameSpace + objName == lst[i].ToString())
break;
}
if (i == lst.Count)
Console.WriteLine("对象名错误!");
//通过反射调用lst中的对象
Type t = lst[i].GetType();
MethodInfo[] methodInfos = t.GetMethods();
for (int j = 0; j < methodInfos.Length; j++){
Console.WriteLine("{0}:{1}", j + 1, methodInfos[j].Name);
}
//选择方法
Console.WriteLine("请选择方法(输入序号):");
int methodNum = Convert.ToInt32(Console.ReadLine()) - 1;//已经减1
//获取参数列表
ParameterInfo[] parameterInfo = methodInfos[methodNum].GetParameters();
if (parameterInfo.Length != 0)
{
object[] pList = new object[parameterInfo.Length];
Console.WriteLine("请输入{0}个参数:", parameterInfo.Length);
string[] temp = Console.ReadLine().Split();
for (int k = 0; k < parameterInfo.Length; k++){
pList[k] = Convert.ToDouble(temp[k]);
}
Console.WriteLine(methodInfos[methodNum].Invoke(lst[i], pList));
}
else //无参数
Console.WriteLine(methodInfos[methodNum].Invoke(lst[i], null));
//询问是否继续
Console.WriteLine("是否继续?(输入Y/N)");
char con = Convert.ToChar(Console.ReadLine());
while (!(con == 'Y' || con == 'y' || con == 'N' || con == 'n'))
{
Console.WriteLine("输入错误!请尝试再次输入");
con = Convert.ToChar(Console.ReadLine());
}
if (con == 'Y' || con == 'y')
continue;
else
break;
}
break;
case 2://系统演化
Console.WriteLine("系统演化模式:");
Console.WriteLine("请输入演化次数:");
int num = Convert.ToInt32(Console.ReadLine());
while (num-- != 0)
{
int i = RandomIntProduce(0, lst.Count);//随机产生一个数以获得对象
Type t = lst[i].GetType();
Console.Write("{0} - ",t.Name);
MethodInfo[] methodInfos = t.GetMethods();
int methodNum = RandomIntProduce(0, methodInfos.Length);
Console.Write("{0}: ",methodInfos[methodNum].Name);
ParameterInfo[] parameterInfo = methodInfos[methodNum].GetParameters();
if (parameterInfo.Length != 0)
{
object[] pList = new object[parameterInfo.Length];
RandomParaProduce(pList, 0, RandomIntProduce(1,2938));//随机产生
Console.WriteLine(methodInfos[methodNum].Invoke(lst[i], pList));
}
else //无参数
Console.WriteLine(methodInfos[methodNum].Invoke(lst[i], null));
}//while
break;
default:
Console.WriteLine("输入有误!");
break;
}
fp.Close();//结束读入
Console.WriteLine("*********************************");
Console.WriteLine("运行结束,按任意键继续...");
Console.ReadLine();
}//Main()
public static int RandomIntProduce(int a, int b)//返回在[a,b)中的随机数
{
byte[] buffer = Guid.NewGuid().ToByteArray();
int seed = BitConverter.ToInt32(buffer, 0);
Random rand = new Random(seed);
return rand.Next(a, b);
}
public static void RandomParaProduce(object[] objList, double a, double b)//产生数值在[a,b)中的数组,用objList返回
{
for (int i = 0; i < objList.Length; i++)
{
byte[] buffer = Guid.NewGuid().ToByteArray();
int seed = BitConverter.ToInt32(buffer, 0);
Random rand = new Random(seed);
//NextDouble()只产生[0,1)的随机实数,通过公式映射到[a,b)
objList[i] = rand.NextDouble() * (Math.Abs(b - a) + a);
}
return;
}
}//class Program
}
代码片段分析
.bat指令
::设置路径
path C:\Windows\Microsoft.NET\Framework\v4.0.30319
::产生.dll
csc/target:library Graph.cs
csc/target:library /reference:Graph.dll Graphs.cs
以上几条指令需要在DOS环境(cmd)下运行,用于产生.dll文件。所谓.dll文件是指一个包含可由多个程序,同时使用的代码和数据的库,就是将不同的类型等集中在一起变成的一个文件,在程序运行中可以去调用它。.NET平台提供了csc.exe用以产生.dll文件,其路径即是上述代码的第一行(版本号依不同版本而定)。
第二行代码的csc/target:library指令直接产生一个.dll文件。第三行代码的/reference指令即是引用了第二行代码产生的dll文件作为自己的“基类”。参考文献上的定义为:
(/reference)命令导致编译器将指定文件的public类型信息导入到当前项目中,从而可以从指定的程序集文件引用元数据。
Main()方法
获取和展示模块
Assembly assembly = Assembly.LoadFrom(@".\Graphs.dll");
先前提到过Assembly类,它可以加载、了解和操作一个程序集。这里通过调用它的一个静态方法LoadFrom()加载一个程序集到程序中,也就是之前我们制作好的.dll文件,它包含着我们接下来需要用到的类。
我们接着往下看。
ArrayList lst = new ArrayList();
object obj = assembly.CreateInstance(nameSpace + line);//nameSpace = @"Reflection_exp."
CreateInstance()方法用于创造一个相应类的对象。注意,它的参数必须是类的“全名”,即命名空间名.类名。由于不能确定它创造出来的对象到底是什么,因而我们把它当做万物的父对象,即Object对象来进行接收。还记得之前的要求③吗?(③把这些对象放到数组中)。这么做还有一个好处,接下来我们就能把它放进一个Object数组(即lst)中进行存储,从而达到了要求③。
接下来我们就能把它放进一个Object数组(即lst)中进行存储了。
有没有觉得哪里怪怪的?
我们都把它们当做object了,那谁还记得他本来的样子呢?
事实上,这个对象依然是它最初的模样,只是我们用一个类似于“父指针指向子对象”的方法来引用它而已。到时候我们可以再次通过反射来获取它的信息。
我们接着往下看:
//获取字段
FieldInfo[] fieldInfos = t.GetFields();
Console.WriteLine("{0}有{1}个字段:", t.Name, fieldInfos.Length);
foreach (FieldInfo i in fieldInfos){
Console.WriteLine("{0}含有字段{1} ", t.Name, i.Name);
}
//获取方法
MethodInfo[] methodInfos = t.GetMethods();
Console.WriteLine("{0}有{1}个方法:", t.Name, methodInfos.Length);
foreach (MethodInfo i in methodInfos){
Console.WriteLine("{0}含有方法{1} ", t.Name, i.Name);
}
这里就是我们之前提到的获取字段和方法列表的函数,即GetFields()和GetMethods(),它们都返回一个列表。同时,Type对象和FieldInfo、MethodInfo对象等都有Name、FullName之类的属性,它们包含了这些对象的信息。这段代码运行的部分结果如下。

调用模块(用户或系统)
Type t = lst[i].GetType();
MethodInfo[] methodInfos = t.GetMethods();
for (int j = 0; j < methodInfos.Length; j++){
Console.WriteLine("{0}:{1}", j + 1, methodInfos[j].Name);
}
这里我们将刚刚的疑惑消除了。再次反射object对象就能拿到它真正的信息了。
//获取参数列表
ParameterInfo[] parameterInfo = methodInfos[methodNum].GetParameters();
if (parameterInfo.Length != 0)
{
object[] pList = new object[parameterInfo.Length];
Console.WriteLine("请输入{0}个参数:", parameterInfo.Length);
string[] temp = Console.ReadLine().Split();
for (int k = 0; k < parameterInfo.Length; k++){
pList[k] = Convert.ToDouble(temp[k]);
}
Console.WriteLine(methodInfos[methodNum].Invoke(lst[i], pList));
}
else //无参数
Console.WriteLine(methodInfos[methodNum].Invoke(lst[i], null));
MethodInfo类对象下有Invoke()方法。Invoke(n. 援引)用以调用这个对象对应的方法,它拥有两个参数:Object obj和Object[] parameters。第一个参数为方法对应的对象,在这里就是lst[i]。而第二个参数这是该方法的参数列表。若该方法没有参数,则应该填入null。


随机数产生方法
public static int RandomIntProduce(int a, int b)//返回在[a,b)中的随机数
{
byte[] buffer = Guid.NewGuid().ToByteArray();
int seed = BitConverter.ToInt32(buffer, 0);
Random rand = new Random(seed);
return rand.Next(a, b);
}
如果要使用到随机数,最方便的方法就是直接使用Random对象。Random类对象的Next()函数拥有两个参数a, b,以产生属于[a, b)的随机数。而NextDouble()函数只能产生[0, 1)的随机数,因而需要通过数学计算映射到我们需要的[a, b)区间。
但是在像我们的实验这样的环境下,需要在短时间内产生大量随机数(系统演化),这时候Random就会出问题。其原因在于,获取系统时间的间隔太短,播下的种子是相同的(因获取的系统时间相同),从而产生了相同的随机数。解决的问题是通过GUID。
全局唯一标识符(GUID,Globally Unique Identifier)是一种由算法生成的二进制长度为128位的数字标识符。在理想情况下,任何计算机和计算机集群都不会生成两个相同的GUID。GUID 的总数达到了2128(3.4×1038)个,所以随机生成两个相同GUID的可能性非常小。 ——百度百科
通过这个奇妙的东西,我们可以做到基本上每次获取一个不同的数据用于播种,从而达到获取不同的随机数的效果。

总结
在之前的学习中,我们都没有接触到通过未知对象获取类型信息的场景。然而这样的情况在实际开发的环境中是非常普遍的,例如通过网络传输等方式获得。不仅C#,在Java、Python等其他的语言中也存在着一样的功能。通过本次实验,我们能掌握如获取字段、方法、方法参数列表等反射类的基本方法和它们的用法。还了解了.dll文件的原理和构建,随机数获取等知识点。

参考文献
李春葆,曾平,喻丹丹.C#程序设计教程(第3版):清华大学出版社,2015
Copyright @ 2021, Bilibili: ForeverMeteor, all rights reserved.