宋城虎,馬 靜
(中國電子科技集團(tuán)公司第二十七研究所,河南 鄭州450047)
HashTable(也叫哈希表),是根據(jù)關(guān)鍵碼值(key,value)而直接進(jìn)行訪問的數(shù)據(jù)結(jié)構(gòu)。它通過把關(guān)鍵碼值映射到表中一個位置來訪問記錄,以加快查找的速度。在.NET Framework中,System.Collections命名空間提供了HashTable容器的實現(xiàn)。HashTable中key、value鍵值對均為object類型,所以HashTable可以支持任何類型的key、value鍵值對。
在工程應(yīng)用中,上位機(jī)應(yīng)用程序通常需要與多個下位機(jī)或者外圍設(shè)備進(jìn)行通信,從而實現(xiàn)對設(shè)備的控制以及設(shè)備狀態(tài)與數(shù)據(jù)的接收與處理。當(dāng)上位機(jī)通信的對象增多時,上位機(jī)編程也會隨之變得越來越復(fù)雜。
本文以C#WinForm框架下的上位機(jī)編程為例討論一種HashTable的應(yīng)用方法,該方法實現(xiàn)簡單,使用靈活方便,可大幅度簡化上位機(jī)程序開發(fā)中的一些復(fù)雜問題。
HashTable的結(jié)構(gòu)為1個key對應(yīng)1個value,這個結(jié)構(gòu)在實際應(yīng)用中經(jīng)常顯得過于單一,如果兩個key對應(yīng)1個value那么顯然就會靈活許多,我們就可以構(gòu)建類似于“對象‘key1’的‘key2’屬性為‘value’”這樣的邏輯關(guān)系(如圖1、圖2所示)。
由于在.NET Framework中的HashTable中key、value鍵值對均為object類型,那么value本身也可以存儲另一個HashTable,因此我們可以構(gòu)建兩層哈希表來實現(xiàn)雙鍵哈希結(jié)構(gòu),即在第一層HashTable1中通過key1存儲第二層HashTable2,在第二層HashTable2中通過key2存儲value值(如圖3所示)。
于是我們只需要定義哈希表的三個基本操作“存儲、讀取、刪除”即可實現(xiàn)雙鍵哈希表功能。其中刪除需要針對key1和key2定義兩個函數(shù),因此總共需要定義4個函數(shù),以下給出代碼:
void hash_save(Hashtable hash,object key1,object key2,object value)
{
if(hash!=null)
{
Hashtable hash1;
if(hash[key1]==null)
{
hash1=new Hashtable();
hash.Add(key1,hash1);
}
else hash1=(Hashtable)(hash[key1]);
if(hash1[key2]==null)hash1.Add(key2,value);
else{
hash1.Remove(key2);
hash1.Add(key2,value);
}}}
object hash_Load(Hashtable hash,object key1,object key2)
{
i(fhash[key1]!=null)return((Hashtable)(hash[key1]))[key2];
else return null;
}
void hash_remove_key1(Hashtable hash,object key1)
{
((Hashtable)(hash[key1])).Clea(r);
}
void hash_remove_key2(Hashtable hash,object key1,object key2)
{
((Hashtable)(hash[key1])).Remove(key2);
}
上位機(jī)應(yīng)用程序通常需要與多個下位機(jī)或者外圍設(shè)備進(jìn)行通信,這里以udp通信為例(其他通信原理相同)。WinForm框架中有Socket類用來實現(xiàn)網(wǎng)絡(luò)通信,在建立一個udp通信時,我們需要實例化一個Socket對象,定義目標(biāo)IPEndPoint和本地IPEndPoint,建立監(jiān)聽線程,在監(jiān)聽線程的回調(diào)函數(shù)中編寫數(shù)據(jù)處理代碼。
當(dāng)上位機(jī)要與多個對象同時建立通信時,通常將以上過程復(fù)制多次,并用不同的變量名進(jìn)行區(qū)分。當(dāng)通信對象特別多時代碼的編輯和維護(hù)就會變得非常困難,且不方便移植也不利于復(fù)用。
這里借助上文定義的雙鍵哈希表可極大程度地優(yōu)化這一過程。實現(xiàn)思路如下:
(1)給每一條通信鏈路定義一個唯一標(biāo)識Sign;
(2)以標(biāo)識Sign為key1、以參數(shù)標(biāo)識為key2對該鏈路的所有相關(guān)參數(shù)進(jìn)行注冊,存儲在哈希表中,例如給名為“sign1”的連接注冊目標(biāo)IP:
hash_sav(ehash1,"sign1","目標(biāo)IP","192.168.1.10");
(3)定義一個主索引用來記錄所有已經(jīng)注冊的Sign,主索引依然可以使用雙鍵哈希表實現(xiàn),例如索引中增加一個新的名為“Sign1”的連接:
int max=(int)hash_load(hash1,"udp主索引","連接總數(shù)")+1
hash_save(hash1,"udp主索引","連接總數(shù)",max);
hash_save(hash1,"udp主索引",max,"Sign1");
(4)定義udpcreate函數(shù),該函數(shù)針對一個特定Sign,建立一條udp通信鏈路,返回初始化完畢的Socket對象。所有初始化相關(guān)參數(shù)以Sign為key在哈希表中讀取,例如讀取連接“sign1”的目標(biāo)IP:
string ip=hash_load(hash1,"sign1","目標(biāo)IP").ToString();
(5)在程序的udp初始化環(huán)節(jié)遍歷第3步中定義的主索引,調(diào)用udpcreate函數(shù)初始化所有udp連接;
(6)單獨定義每條通信的接收數(shù)據(jù)的回調(diào)函數(shù),回調(diào)函數(shù)可以以委托結(jié)合文本宏的方式注冊在哈希表中。通信鏈路的注冊可以通過文件讀取轉(zhuǎn)移到配置文件中。
主索引操作以及udpcreate函數(shù)等通用型代碼均可封裝到一個模塊中,方便移植和復(fù)用。這樣每次開發(fā)一個新的上位機(jī)程序只需要編寫數(shù)據(jù)處理的回調(diào)函數(shù)以及根據(jù)工程需求編輯配置文件即可。
使用雙鍵哈希表可以不以變量為載體,動態(tài)地存儲和讀取任意數(shù)據(jù),并能將數(shù)據(jù)關(guān)聯(lián)在任意對象上,這意味著它幾乎可以應(yīng)用到程序的任何地方。例如,我們可以在控件刷新時讀取控件上綁定的狀態(tài)數(shù)據(jù)來決定控件的外觀或者顯示文字,這樣我們就可以通過改變哈希值來控制控件的狀態(tài)刷新;我們還可以利用雙鍵哈希表將兩個控件關(guān)聯(lián)起來,實現(xiàn)類似于父節(jié)點和子節(jié)點這樣的結(jié)構(gòu)關(guān)系等。
由于使用雙鍵哈希表會自動創(chuàng)建許多次級HashTable,當(dāng)某個key1不再使用時,應(yīng)當(dāng)注意釋放key1對應(yīng)的資源,也就是調(diào)用上文提到的hash_remove_key1函數(shù),避免出現(xiàn)內(nèi)存泄漏。
在某工程項目的上位機(jī)軟件開發(fā)中,與上位機(jī)通信的分機(jī)有7個,其中包含udp和串口通信。該軟件開發(fā)中大量使用了雙鍵哈希表,與以往的開發(fā)經(jīng)驗做對比極大程度地提高了開發(fā)效率,調(diào)試過程中的bug也有顯著減少,并且能夠簡單快捷地移植到其他項目開發(fā)中(如圖4、圖5所示)。
圖4 某項目上位機(jī)軟件通信調(diào)試界面
圖5 某項目通信配置文件
文章利用.NET Framework中的HashTable的key和value可支持任意類型值的特點,設(shè)計了一種雙鍵哈希表,可實現(xiàn)將任意類型的兩個對象作為索引存儲一個任意類型的value值。文章以WinForm框架下上位機(jī)程序開發(fā)為背景,以udp通信編程為例,詳細(xì)闡述了該技術(shù)的應(yīng)用方法與效果,對該技術(shù)在其他方面的應(yīng)用進(jìn)行了展望。該技術(shù)已在作者參與的多個工程項目中得到應(yīng)用,并得到了非常好的應(yīng)用效果。