C#實際案例分析(第三彈)——By 流星
實驗三
題目要求
構(gòu)建一個components.txt文件,每行代表一個類(比如形狀類Shape)的名字(這些類可以來自系統(tǒng)庫,或者自己構(gòu)造);程序逐行掃描該文件,構(gòu)建對應(yīng)的類的對象。要求:
把這些對象放到數(shù)組中;
列出每個類的字段、方法;
讓用戶選擇,使得用戶可以調(diào)用所需要的方法或者操作
系統(tǒng)隨機選擇對象(比如形狀),隨機的執(zhí)行其操作,從而看系統(tǒng)演化??赡艿脑?,進(jìn)行界面展示
環(huán)境設(shè)置
操作系統(tǒng): Windows 10 x64
SDK: .NET Framework 4.7.2
IDE: Visual Studio 2019

題意分析
本次實驗綜合了文件讀取、構(gòu)建動態(tài)鏈接庫(dll)、反射等知識點,我們逐漸拆開來看:
①構(gòu)建一個components.txt文件,每行代表一個類的名字;
②程序逐行掃描該文件,構(gòu)建對應(yīng)的類的對象;
①要求實現(xiàn)文件逐行讀取的功能,通過C#提供的文件流來實現(xiàn)。而②要求我們通過這些名字來構(gòu)建類的對象。聽起來是不是很高大上?我們在學(xué)習(xí)C++甚至Python的時候都沒有遇到過這樣的事情(事實上它們都可以實現(xiàn))。這里,我們將通過C#提供的反射功能來完成這個要求。我們將在下面進(jìn)行反射的詳解。
③把這些對象放到數(shù)組中;
④列出每個類的字段、方法;
⑤讓用戶選擇,使得用戶可以調(diào)用所需要的方法或者操作;
⑥系統(tǒng)隨機選擇對象,隨機的執(zhí)行其操作,從而看系統(tǒng)演化??赡艿脑?,進(jìn)行界面展示;
③要求將這些對象存入數(shù)組中。④要求列出每個類的字段和方法。⑤要求讓用戶能夠自己選擇需要調(diào)用的方法或操作。⑥要求隨機執(zhí)行操作。事實上⑤⑥的要求是一致的,只是⑥將用戶自己的選擇以隨機數(shù)的方式代替。我們將在接下來的解析中詳細(xì)將這些要求具體展開來看。

反射·簡介
反射(Reflection)是.NET中重要的機制。通過反射可以在運行時獲得.NET中每一個類型(包括類、結(jié)構(gòu)、微投、接口、枚舉等)的成員,包括方法、屬性、事件和構(gòu)造函數(shù)等,還可以獲得每個成員的名稱、限定符和參數(shù)等。 ——參考文獻(xiàn)
也就是說,我們通過某些途徑拿到了一個類或其他類型的信息或是它們的某個成員,但是我們對這個東西基本上是一無所知,那我們該怎么知道它有什么東西,以及怎么去使用它?這時候就需要通過反射來獲取屬性、方法列表等信息,我們再進(jìn)行調(diào)用。反射的原理我們不再多講,籠統(tǒng)地來說就是通過審查元數(shù)據(jù)(Meta-Data),調(diào)用程序運行時加載到內(nèi)存中的程序集,動態(tài)獲取對象的信息。
使用反射時,需要包含反射的頭文件,即using System.Reflection;。
反射中最常用到的類是Type類。顧名思義,Type類對象即用來獲取某個類或?qū)ο蟮念愋托畔⒉⒅С趾罄m(xù)調(diào)用。例如:
Type t =?Type.GetType("Reflection_exp.Square");
這是十分常用,也是不需要編譯時信息即可獲取類型的方法。例如上面這條語句獲得的對象t就含有Reflection_exp.Square這個類的信息了。除此之外還有每個對象都有的GetType()方法,typeof運算符等其他的方法可以獲取Type對象。注意,由于Type是抽象類,因而不可直接創(chuàng)建它的對象。
我們獲取到Type對象之后就可以對它進(jìn)行我們之前說的操作了,例如拿到字段列表,方法列表,方法的參數(shù)列表等,他們調(diào)用的函數(shù)分別是GetFields(), GetMethods(), GetParameters()。用法示例如下:
FieldInfo[]?fieldInfos =?t.GetFields();
這樣,fieldInfos數(shù)組就有t所指的類型的所有字段了,其他的幾個方法也是相同的道理。
另一個在反射中用到的類是Assembly類。它能夠加載、了解和操作一個程序集。在這個實驗的剛開始,我們會通過它加載動態(tài)鏈接庫/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.";//命名空間的名字,用于構(gòu)建類名的全稱(注意,必須要全稱)
????????????string?line =?fp.ReadLine();//逐行讀入
????????????while?(line !=?null)
????????????{
????????????????object?obj =?assembly.CreateInstance(nameSpace +?line);
????????????????if?(obj ==?null){
????????????????????Console.WriteLine("錯誤類名!");
????????????????????continue;
????????????????}
????????????????lst.Add(obj);//直接當(dāng)做父類存入Array中,調(diào)用時通過反射進(jìn)行調(diào)用
????????????????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)或者系統(tǒng)演化(輸入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("對象名錯誤!");
????????????????????????//通過反射調(diào)用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;//已經(jīng)減1
????????????????????????//獲取參數(shù)列表
????????????????????????ParameterInfo[]?parameterInfo =?methodInfos[methodNum].GetParameters();
????????????????????????if?(parameterInfo.Length?!=?0)
????????????????????????{
????????????????????????????object[]?pList =?new?object[parameterInfo.Length];
????????????????????????????Console.WriteLine("請輸入{0}個參數(shù):",?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?//無參數(shù)
????????????????????????????Console.WriteLine(methodInfos[methodNum].Invoke(lst[i],?null));
????????????????????????//詢問是否繼續(xù)
????????????????????????Console.WriteLine("是否繼續(xù)?(輸入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://系統(tǒng)演化
????????????????????Console.WriteLine("系統(tǒng)演化模式:");
????????????????????Console.WriteLine("請輸入演化次數(shù):");
????????????????????int?num =?Convert.ToInt32(Console.ReadLine());
????????????????????while?(num--?!=?0)
????????????????????{
????????????????????????int?i =?RandomIntProduce(0,?lst.Count);//隨機產(chǎn)生一個數(shù)以獲得對象
????????????????????????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));//隨機產(chǎn)生
????????????????????????????Console.WriteLine(methodInfos[methodNum].Invoke(lst[i],?pList));
????????????????????????}
????????????????????????else?//無參數(shù)
????????????????????????????Console.WriteLine(methodInfos[methodNum].Invoke(lst[i],?null));
????????????????????}//while
????????????????????break;
????????????????default:
????????????????????Console.WriteLine("輸入有誤!");
????????????????????break;
????????????}
????????????fp.Close();//結(jié)束讀入
????????????Console.WriteLine("*********************************");
????????????Console.WriteLine("運行結(jié)束,按任意鍵繼續(xù)...");
????????????Console.ReadLine();
????????}//Main()
????????public?static?int?RandomIntProduce(int?a,?int?b)//返回在[a,b)中的隨機數(shù)
????????{
????????????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)//產(chǎn)生數(shù)值在[a,b)中的數(shù)組,用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()只產(chǎn)生[0,1)的隨機實數(shù),通過公式映射到[a,b)
????????????????objList[i]?=?rand.NextDouble()?*?(Math.Abs(b -?a)?+?a);
????????????}
????????????return;
????????}
????}//class Program
}
代碼片段分析
.bat指令
::設(shè)置路徑
path C:\Windows\Microsoft.NET\Framework\v4.0.30319
::產(chǎn)生.dll
csc/target:library Graph.cs
csc/target:library /reference:Graph.dll Graphs.cs
以上幾條指令需要在DOS環(huán)境(cmd)下運行,用于產(chǎn)生.dll文件。所謂.dll文件是指一個包含可由多個程序,同時使用的代碼和數(shù)據(jù)的庫,就是將不同的類型等集中在一起變成的一個文件,在程序運行中可以去調(diào)用它。.NET平臺提供了csc.exe用以產(chǎn)生.dll文件,其路徑即是上述代碼的第一行(版本號依不同版本而定)。
第二行代碼的csc/target:library指令直接產(chǎn)生一個.dll文件。第三行代碼的/reference指令即是引用了第二行代碼產(chǎn)生的dll文件作為自己的“基類”。參考文獻(xiàn)上的定義為:
(/reference)命令導(dǎo)致編譯器將指定文件的public類型信息導(dǎo)入到當(dāng)前項目中,從而可以從指定的程序集文件引用元數(shù)據(jù)。
Main()方法
獲取和展示模塊
Assembly assembly =?Assembly.LoadFrom(@".\Graphs.dll");
先前提到過Assembly類,它可以加載、了解和操作一個程序集。這里通過調(diào)用它的一個靜態(tài)方法LoadFrom()加載一個程序集到程序中,也就是之前我們制作好的.dll文件,它包含著我們接下來需要用到的類。
我們接著往下看。
ArrayList lst =?new?ArrayList();
object?obj =?assembly.CreateInstance(nameSpace +?line);//nameSpace = @"Reflection_exp."
CreateInstance()方法用于創(chuàng)造一個相應(yīng)類的對象。注意,它的參數(shù)必須是類的“全名”,即命名空間名.類名。由于不能確定它創(chuàng)造出來的對象到底是什么,因而我們把它當(dāng)做萬物的父對象,即Object對象來進(jìn)行接收。還記得之前的要求③嗎?(③把這些對象放到數(shù)組中)。這么做還有一個好處,接下來我們就能把它放進(jìn)一個Object數(shù)組(即lst)中進(jìn)行存儲,從而達(dá)到了要求③。
接下來我們就能把它放進(jìn)一個Object數(shù)組(即lst)中進(jìn)行存儲了。
有沒有覺得哪里怪怪的?
我們都把它們當(dāng)做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);
}
這里就是我們之前提到的獲取字段和方法列表的函數(shù),即GetFields()和GetMethods(),它們都返回一個列表。同時,Type對象和FieldInfo、MethodInfo對象等都有Name、FullName之類的屬性,它們包含了這些對象的信息。這段代碼運行的部分結(jié)果如下。
?

調(diào)用模塊(用戶或系統(tǒng))
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對象就能拿到它真正的信息了。
//獲取參數(shù)列表
ParameterInfo[]?parameterInfo =?methodInfos[methodNum].GetParameters();
if?(parameterInfo.Length?!=?0)
{
????object[]?pList =?new?object[parameterInfo.Length];
????Console.WriteLine("請輸入{0}個參數(shù):",?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?//無參數(shù)
????Console.WriteLine(methodInfos[methodNum].Invoke(lst[i],?null));
MethodInfo類對象下有Invoke()方法。Invoke(n. 援引)用以調(diào)用這個對象對應(yīng)的方法,它擁有兩個參數(shù):Object obj和Object[] parameters。第一個參數(shù)為方法對應(yīng)的對象,在這里就是lst[i]。而第二個參數(shù)這是該方法的參數(shù)列表。若該方法沒有參數(shù),則應(yīng)該填入null。


隨機數(shù)產(chǎn)生方法
public?static?int?RandomIntProduce(int?a,?int?b)//返回在[a,b)中的隨機數(shù)
{
????byte[]?buffer =?Guid.NewGuid().ToByteArray();
????int?seed =?BitConverter.ToInt32(buffer,?0);
????Random rand =?new?Random(seed);
????return?rand.Next(a,?b);
}
如果要使用到隨機數(shù),最方便的方法就是直接使用Random對象。Random類對象的Next()函數(shù)擁有兩個參數(shù)a, b,以產(chǎn)生屬于[a, b)的隨機數(shù)。而NextDouble()函數(shù)只能產(chǎn)生[0, 1)的隨機數(shù),因而需要通過數(shù)學(xué)計算映射到我們需要的[a, b)區(qū)間。
但是在像我們的實驗這樣的環(huán)境下,需要在短時間內(nèi)產(chǎn)生大量隨機數(shù)(系統(tǒng)演化),這時候Random就會出問題。其原因在于,獲取系統(tǒng)時間的間隔太短,播下的種子是相同的(因獲取的系統(tǒng)時間相同),從而產(chǎn)生了相同的隨機數(shù)。解決的問題是通過GUID。
全局唯一標(biāo)識符(GUID,Globally Unique Identifier)是一種由算法生成的二進(jìn)制長度為128位的數(shù)字標(biāo)識符。在理想情況下,任何計算機和計算機集群都不會生成兩個相同的GUID。GUID 的總數(shù)達(dá)到了2128(3.4×1038)個,所以隨機生成兩個相同GUID的可能性非常小。 ——百度百科
通過這個奇妙的東西,我們可以做到基本上每次獲取一個不同的數(shù)據(jù)用于播種,從而達(dá)到獲取不同的隨機數(shù)的效果。

總結(jié)
在之前的學(xué)習(xí)中,我們都沒有接觸到通過未知對象獲取類型信息的場景。然而這樣的情況在實際開發(fā)的環(huán)境中是非常普遍的,例如通過網(wǎng)絡(luò)傳輸?shù)确绞将@得。不僅C#,在Java、Python等其他的語言中也存在著一樣的功能。通過本次實驗,我們能掌握如獲取字段、方法、方法參數(shù)列表等反射類的基本方法和它們的用法。還了解了.dll文件的原理和構(gòu)建,隨機數(shù)獲取等知識點。

參考文獻(xiàn)
李春葆,曾平,喻丹丹.C#程序設(shè)計教程(第3版):清華大學(xué)出版社,2015
Copyright @ 2021, Bilibili: ForeverMeteor, all rights reserved.??