11.1 序列化與反序列化 程序員
11.1.1 爲何須要序列化 編程
在本書第10章中的示例4和示例5中,咱們分別實現了定製頻道信息寫入文本文件和讀取定製頻道信息的功能。試想若是Channel類的屬性發生變化,咱們該如何處理呢?咱們確定要修改示例中的SaveAsTxt()方法和LoadFromTxt()方法。可是若是一些信息須要常常變化,是否每次都要這樣繁瑣地改動呢?答案是否認的,本章咱們要學習一種新技術,只要簡簡單單的幾步就能夠一勞永逸地完成配置信息的讀寫操做。步驟以下。設計模式
(1)在ChannelManager類中引入這樣一個命名空間。數組
using System.Runtime.Serialization.Formatters.Binary; 安全
(2)在SavingInfo、 ChannelBase、 TypeAChannel、 TypeBChannel類的頭部添加一個標記[Serializable],例如,這樣用於標記該類是否可序列化。網絡
[Serializable] 框架
abstract class ChannelBase 編輯器
{ 函數
//… 工具
}
(3)修改SaveAsTxt()和LoadFromTxt()方法,如示例1所示。
示例1
//保存定製頻道信息的文本文件名稱
private string saveFileName = @"files\save";
//將個人電臺信息存儲到文本文件之中
public void SaveAsTxt()
{
FileStream fs = null;
try
{
fs = new FileStream(saveFileName + ".bin", FileMode.Create);
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(fs, this.seria.MyFavor);
//this.seria.MyFavor是"個人電視臺"頻道集合對象
}
catch
{
throw;
}
finally
{
fs.Close();
}
}
// 從文本文件之中讀取"個人電臺"信息
public void LoadFromTxt()
{
FileStream fs = null;
try
{
fs = new FileStream(saveFileName + ".bin", FileMode.Open);
BinaryFormatter bf = new BinaryFormatter();
SavingInfo info=(SavingInfo)bf.Deserialize(fs);
}
catch
{
throw;
}
finally
{
fs.Close();
}
}
通過驗證,程序中對象的值都被正確地保存和讀取了。簡單的幾段代碼,實現了示例1 的一大堆代碼實現的功能,並且不用關心文件的結構。這種實現方式就稱爲序列化。更美妙的是,一旦你的配置發生了變化,直接修改你的SavingInfo類便可,SaveAsTxt()和LoadFromTxt()方法無須改變!
11.1.2 特性
在上面的代碼中。咱們發現了一個特別的地方,就是在咱們的類聲明上面加了以下一行代碼。
[Serializable]
abstract class ChannelBase
{
//…
}
這個[Serializable]主要用來告訴系統,下面的類是可序列化的。而[Serializable]自己,咱們稱之爲可序列化特性。所謂特性,就是爲目標元素(能夠是數據集、模塊、類、屬性、方法、甚至函數參數等)加入附加信息,相似於註釋。特性本質上也是一個類,如[Serializable]對應的類是SerializableAttribute。 (通常來講,特性命名都以Attribute結尾,可是咱們在使用它時,能夠省略這個小尾巴,聰明的.NET會自動找到對應的特性類)。特性能夠直接影響代碼的運行方式,例如示例2中的可序列化特性。在.NET中還有不少特性,能夠標記指定元素的特殊編譯或者運行方式,參考如示例2所示的代碼,ObsoleteAttribute用於標記一個再也不使用的程序元素。
示例2
class Program
{
[Obsolete("不要使用舊的方法, 請使用新的方法", true)]
static void Old() { }
static void New() { }
public static void Main()
{
Old();
}
}
ObsoleteAttribute標記了一個不應再被使用的語言元素Old(),該特性的第一個參數是 string類型,它解釋爲何該元素被荒棄,以及咱們該使用什麼元素來代替它。實際上,咱們能夠書寫任何其餘文原本代替這段文本。第二個參數是告訴編譯器把依然使用這個被標識的元素視爲一種錯誤,這就意味着編譯器會所以而產生一個警告。若是試圖編譯這段代碼就會提示錯誤"MyAttributes.Program.Old()"已過期: "不要使用舊的方法,請使用新的方法"。
定製特性主要應用在序列化、編譯器指令、設計模式等方面。之後咱們會在開發中學習其餘的特性。
11.1.3 序列化
序列化是將對象的狀態存儲到特定存儲介質中的過程,也能夠說是將對象狀態轉換爲可保持或傳輸的格式的過程。在序列化過程當中,會將對象的公有成員、私有成員包括類名,都轉換成數據流的形式,存儲到存儲介質中,這裏說的存儲介質一般指的是文件。例如,示例1中,咱們經過序列化保存了SaveingInfo對象的信息。.NET提供多種形式的序列化,文本或XML流等。目前使用二進制方式對泛型支持得最好。參考示例1中以下代碼。
FileStream fileStream = null;
//定義一個文件流
fileStream = new FileStream("profile.bin", FileMode.Create);
//二進制方式
BinaryFormatter bf = new BinaryFormatter();
//序列化保存配置文件對象Profile
bf.Serialize(fileStream, Profile);
由於序列化須要經過文件流來保存到文件,因此要先定義一個文件流,BinaryFormatter是一個二進制格式化器,這個二進制格式化器具備一個很是重要的Serialize()方法。
語法:
public void Serialize (Stream serializationStream, Object graph)
這個方法的主要功能是將特定對象序列化到特定文件中,具體參數意義以下。
若是咱們要序列化的對象包含子類對象,那麼這個序列化的基本過程大體如圖11-1所示。
圖11-1 序列化的基本過程
若是須要序列化某個特定對象,那麼它的各個成員對象也必須是可序列化的。對咱們的網絡電視精靈而言,若是要將程序中的SavingInfo對象序列化,那麼它包含的對象都須要加上可序列化標記,例如SavingInfo、 ChannelBase、 TypeAChannel等。
11.1.4 反序列化
既然能將對象的狀態保存到特定介質中,那麼咱們又應該怎樣將這些對象狀態讀取回來呢?這就用到了另外一個知識:反序列化。所謂反序列化,顧名思義就是與序列化相反,序列化是將對象的狀態信息保存到存儲介質中,反序列化則是從特定存儲介質中將數據從新構建對象的過程。經過反序列化,能夠將存儲在文件上的對象信息讀取,而後從新構建爲對象。這樣就不須要咱們再將文件上的信息一一讀取、分析再組織爲對象了,仍然以二進制格式化器爲例,它的反序列化方法原型以下。
語法:
public Object Deserialize (Stream serializationStream)
注意,Deserialize()方法將存儲介質的數據文件流轉換爲Object,一般咱們仍然須要進一步將這個Object轉換爲相應的對象類型。參考示例1中的LoadFromTxt()方法。
反序列化將建立出與原對象徹底相同的副本,在序列化時所保存的數據將被無損失地保存下來。
11.1.5 序列化和反序列化的用途
11.2 程序集與反射
11.2.1 什麼是程序集
程序集雖然是一個新概念,可是咱們使用它其實已經好久了。在一個.NET的WinForms應用程序編譯後,在bin\Debug文件夾下會生成一個.exe文件,例如咱們的網絡電視精靈,會生成一個TVXmlRead.exe文件,雙擊這個文件,會打開網絡電視精靈的應用程序,實現整個應用程序的功能,爲何運行這個文件就能實現這個功能,無須打開開發環境呢?其實,這個編譯好的.exe文件,稱爲程序集。程序集是.NET框架應用程序的生成塊,它包含編譯好的代碼邏輯單元。
11.2.2 程序集的結構
程序集由描述它的程序集清單、類型元數據、MSIL代碼和資源組成,這些部分都分佈在一個文件中,或者分佈在幾個文件中,如圖11-2所示。
圖11-2 程序集內容
1.程序集清單
每個程序集都包含描述該程序集中各元素彼此如何關聯的數據集合。程序集清單包含這些程序集的元數據。程序集清單包含指定該程序集的版本要求和安全標識所需的全部元數據。程序集清單的主要內容見下表。
信息 |
說明 |
程序集名稱 |
指定程序集名稱的文本字符串 |
版本號 |
主版本號和次版本號,以及修訂號和內容版本號 |
區域性 |
有關該程序集支持的區域性或語言的信息 |
強名稱信息 |
若是已經爲程序集提供了一個強名稱,則爲來自發行者的公鑰 |
程序集中全部文件的列表 |
構成該程序集的文件 |
類型引用信息 |
控制對該程序集的類型和資源的引用如何映射到包含其聲明和實現的文件中 |
有關被引用程序集的信息 |
該信息用於從程序集導出的類型 |
程序集清單的主要功能以下。
(1)列舉構成該程序集的文件。
(2)控制對該程序集的類型和資源的引用如何映射到包含其聲明和實現的文件中。
(3)列舉該程序集所依賴的其餘程序集。
(4)在程序集的使用者和程序集的實現詳細信息的使用者之間提供必定程度的間接性。
(5)呈現程序集自述。
2.元數據
元數據是一種二進制信息,它以非特定語言的方式描述在代碼中定義的每個類型和成員,程序集清單也是元數據的一部分,上面已經講過它主要存儲如下信息。
(1)程序集的說明。
(2)標識(名稱、版本、區域性、公鑰)。
(3)導出的類型。
(4)該程序集所依賴的其餘程序集。
(5)運行所需的安全權限。
而類型元數據包含如下內容。
(1)類型的說明。
(2)名稱、可見性、基類和實現的接口。
(3)成員(方法、字段、屬性、事件、嵌套的類型)。
(4)屬性。
(5)修飾類型和成員的其餘說明性元素。
3.其餘內容
MSIL是微軟中間代碼,它是實現類型元數據的中間代碼,而資源就是咱們程序中的圖片、音樂文件等。
11.2.3 查看程序集
知道了程序集的結構,如何查看一個程序集的結構呢? .NET中提供了一個反編譯工具ILDasm,使用它能夠查看IL彙編代碼,也能夠看到程序集中的類和方法等。在Visual Studio 2017的命令行窗口,輸入ILDasm.exe,就能夠打開這個反編譯器,打開咱們要查看的TVXmlRead.exe程序集,就可以將程序集中的內容顯示出來,如圖11-3所示。
圖11-3 TVXmlRead的程序集結構
打開該程序集清單,就能夠看到版本號。打開類的方法,就能夠查看MSIL代碼。使用這個工具,你即可以查看一些程序集的清單,瞭解它的結構。在Visual Studio中,全部C#項目類型都會建立一個程序集,不管是類庫仍是可執行的EXE應用程序。在咱們建立一個Visual Studio項目時,會自動生成源文件AssemblyInfo.cs,在這個文件中,可使用通常的源代碼編輯器編輯程序集的特性。下面就是TVXmlRead的AssemblyInfo文件的主要內容。
[assembly: AssemblyTitle("TVXmlRead")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("邯鄲翱翔")]
[assembly: AssemblyProduct("TVXmlRead")]
[assembly: AssemblyCopyright("Copyright © AoXiang")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
這個文件用於配置程序集清單。編譯器讀取程序集的屬性,把特定的信息插入到程序集清單中。用於程序集屬性的參數是命名空間System.Reflection、System.Runtime.CompilerServices等。下表列出了一些程序集屬性
屬性 |
說明 |
AssemblyCompany |
指定公司名 |
AssemblyTitle |
程序集的描述性名稱 |
AssemblyDescription |
描述程序集或產品 |
AssemblyConfifuration |
指定創建信息,例如零售或者調試信息 |
AssemblyProduct |
指定程序集所屬產品的名稱 |
AssemblyCopyright |
包含版權和商標信息 |
AssemblyVersion |
程序集的版本號 |
當右擊查看TVXmlRead.exe文件的屬性時,就能夠看到該程序集中的一些屬性,如圖11-4所示。
圖11-4 TVXmlRead.exe屬性
11.2.4 程序集中的訪問修飾符
在本章以前,咱們學習了3種訪問修飾符private、public、protected。對於它們修飾的成員的做用域,都很熟悉。本章提出另外一個訪問修飾符internal,它修飾的成員在同一個程序集中均可以訪問,可是其餘的程序集就不能訪問,應用程序中的類,若是不指定訪問修飾符,默認就是internal修飾。4種訪問修飾符的做用域見下表。
|
類內部 |
同一程序集的派生類 |
同一程序集的其餘類 |
不一樣程序集的派生類 |
不一樣程序集的其餘類 |
private |
能夠 |
不能夠 |
不能夠 |
不能夠 |
不能夠 |
protected |
能夠 |
能夠 |
不能夠 |
能夠 |
不能夠 |
internal |
能夠 |
能夠 |
能夠 |
不能夠 |
不能夠 |
public |
能夠 |
能夠 |
能夠 |
能夠 |
能夠 |
11.2.5 反射
剛纔介紹了ILDasm工具的使用,能夠用ILDasm反編譯工具瀏覽一個dll和exe的構成,這種機制咱們稱爲反射。它用於在運行時經過編程方式得到類型信息。反射其實在咱們編程中常常可以用到,例如當在Visual Studio中輸入一個類型,而後輸入"."時,就會拉出一個列表,顯示這個類型的屬性、方法、事件等。這都是利用了反射機制。反射能夠獲取已加載的程序集和在其中其中定義的類型(如類、接口和值類型)的信息。也可使用反射在運行時建立類型實例,以及調用和訪問這些實例。反射的一個主要功能就是查找程序集的信息。System.Reflection.Assembly類能夠用於訪問給定程序集的信息,它容許訪問給定程序集的元數據,如示例4所示,咱們利用一個外部應用程序來獲取TVXmlRead的版本號。
示例3
class Program
{
static void Main(string[] args)
{
string version = Assembly.LoadFile(@"D:\TVXmlRead.exe")
.GetName().Version.ToString();
Console.WriteLine(version);
}
}
Assembly.LoadFile(string path)方法用於經過文件路徑加載程序集,其參數path必須爲完整物理路徑。運行結果如圖11-5所示。
圖11-5 反射得到版本號
反射獲得版本號的最大用處就是按期升級軟件,反射獲得當前版本號與升級程序版本號相比較,若是不一致就執行升級程序。反射是一個很是強大的機制,利用反射,咱們能夠了解一些沒有源代碼程序的結構,從而提升程序集的利用效率。
11.2.6 經過反射獲取類型
經過反射除了能夠獲取版本信息以外,咱們還能夠從程序集中獲取類型信息。如實例4所示,咱們能夠從TVXmlRead.exe程序集中獲取其中包含的類型。
實例4
class Program
{
static void Main(string[] args)
{
//加載程序集
Assembly assembly = Assembly.LoadFile(@"D:\TVXmlRead.exe");
//獲取程序集中所有的類型
Type[] types= assembly.GetTypes();
foreach(Type type in types)
{
//輸出類型的全名
Console.WriteLine(type.FullName);
}
Console.ReadLine();
}
}
運行結果如圖11-6所示。
圖11-6 讀取程序集中的所有類型
程序集對象的GetTypes()方法能夠獲得程序集中所有類型信息的數組。另外還有GetType(string name)能夠獲得指定的類型信息。
Type是.Net定義的表示類型的類,位於System命名空間。其經常使用屬性和方法以下表所示。
屬性 |
|
說明 |
Namespace |
|
獲取Type的命名空間。 |
Name |
|
數據類型名。 |
FullName |
|
獲取該類型的徹底限定名稱,包括其命名空間,但不包括程序集 |
BaseType |
|
獲取當前 Type 直接從中繼承的類型。 |
返回值 |
方法 |
說明 |
PropertyInfo |
GetProperty(String name) |
搜索具備指定名稱的公共屬性。 |
PropertyInfo[] |
GetProperties() |
返回爲當前 Type 的全部公共屬性。 |
MethodInfo |
GetMethod(string name) |
搜索具備指定名稱的公共方法。 |
MethodInfo[] |
GetMethods() |
返回爲當前 Type 的全部公共方法。 |
除了經過程序集獲取類型信息外,還能夠經過實例對象和類型獲取類型信息。
(1) 經過實例對象的GetType()方法獲取類型信息
//建立一個對象
Example obj = new Example();
//獲取類型信息
Type type= obj.GetType();
(2) 經過類獲取類型信息
Type type= typeof(Example) ;
11.2.7 動態建立和使用對象
咱們還能夠經過獲取的類型信息建立對象。語法以下:
object obj = Activator.CreateInstance(Type對象);
Activator類的CreateInstance()靜態方法用來建立一個對象,返回類型爲object。
咱們也能夠經過指定的程序集來建立對象。語法以下:
object obj = 程序集對象.CreateInstance("類型全名");
資料
CreateInstance()方法還提供了多個重載版本,例如能夠給類型的有參構造函數傳遞參數。請你們參閱MSDN。
對象建立完成以後,咱們能夠給對象屬性賦值和調用對象的方法,如實例5所示。
實例5
static void Main(string[] args)
{
//加載程序集
Assembly assembly = Assembly.LoadFile(@"D:\TVXmlRead.exe");
//獲取TypeAChannel類型信息
Type channel = assembly.GetType("TVXmlRead.TypeAChannel");
//建立TypeAChannel類型的實例
object obj = assembly.CreateInstance("TVXmlRead.TypeAChannel");
//循環給實例的屬性賦值
foreach(PropertyInfo pi in channel.GetProperties())
{
switch(pi.Name)
{
case "ChannelName":
pi.SetValue(obj, "北京電視臺");
break;
case "Path":
pi.SetValue(obj, @"北京電視臺.xml");
break;
}
}
Console.WriteLine("輸出屬性值:");
Console.WriteLine("ChannelName屬性:"
+ channel.GetProperty("ChannelName").GetValue(obj));
Console.WriteLine("Path屬性:"+channel.GetProperty("Path").GetValue(obj));
//獲取方法信息
MethodInfo Show = channel.GetMethod("Show");
Console.WriteLine("\nShow方法執行結果:");
Show.Invoke(obj,null); //調用方法
Console.ReadLine();
}
程序運行結果如圖11-7所示。
圖11-7 動態建立和使用對象
實例5中,屬性信息的SetValue()方法用來給屬性賦值,GetValue()方法用戶獲取屬性值。方法信息對象的Invoke()方法用來調用方法,第二個參數爲object[]類型,用來給方法傳遞參數。