高屋建瓴:梳理程式設計約定

周見智發表於2015-02-03

相關文章連線:

程式設計之基礎:資料型別(一)

程式設計之基礎:資料型別(二)

動力之源:程式碼中的“泵”

完整目錄與前言

高屋建瓴:梳理程式設計約定

在實際程式設計中,我們會遇見各種各樣的概念,雖然有的並沒有官方定義,但是我們可以自己給它取一個形象的名稱。本章總結了13條會在本書中出現的概念。

2.1 程式碼中的Client與Server

我們一般說到Client和Server,就會聯想到網路通訊,TCP、UDP或者Socket這些概念馬上就浮現在頭腦中。其實Client和Server不僅僅可以用來形容網路通訊雙方,它還可以用來形容程式碼中兩個有互動的程式碼塊。

通訊結構中的Client與Server有資訊互動,一般Server為Client提供服務。程式碼中的一個方法呼叫另外一個物件的方法,同樣涉及到資訊互動,也同樣可以看作這個物件為其提供服務。見下圖2-1:

圖2-1 Client與Server關係圖

圖2-1中"物件"稱作Server,Client與Server可以不在一個程式集中,也可以不在同一個AppDomain裡,更可以不在一個程式中,甚至都不在一臺主機上。下面程式碼演示了Client與Server的關係:

 1 //Code 2-1
 2 
 3 class A
 4 {
 5     //
 6     public int DoSomething(int a,int b)
 7     {
 8         //do something here
 9         return a+b;
10     }
11 }
12 class Program
13 {
14     static void Main()
15     {
16         A a = new A();
17         int result = a.DoSomething(3,4); //invoke public method a.DoSomething
18     }
19 }

程式碼Code 2-1中a物件是Server,Program是Client,前者為後者提供服務。

注:ClientServer不一定指的是物件,A程式集呼叫B程式集中的型別,我們可以把A當作Client,把B當作ServerClientServer也不是絕對的,在一定場合,Client也可以看作是Server

2.2 方法與執行緒的關係

執行緒和方法沒有一對一的關係,也就是說,一個執行緒可以呼叫多個方法,而一個方法又可以被多個執行緒呼叫。由於在程式碼中,我們看得見的只有方法,因此有時候我們很難分清楚某個方法到底會執行在哪個執行緒之中。

 1 //Code 2-2
 2 
 3 class Program
 4 {
 5     static void DoSomething()
 6     {
 7         //do something here
 8     }
 9     static void Main()
10     {
11         //
12         Thread th1 = new Thread(new ThreadStart(DoSomething));
13         Thread th2 = new Thread(new ThreadStart(DoSomething));
14         th1.Start();
15         th2.Start();
16     }
17 }

程式碼Code 2-2中的DoSomething方法可以同時執行在兩個執行緒當中。以上程式碼還是比較直觀的情況,有時候,一點執行緒的影子都看不見,

 1 //Code 2-3
 2 
 3 class Form1:Form
 4 {
 5     //
 6     private void DoSomething()
 7     {
 8         //do something here
 9         //maybe invoke UI controls
10     }
11     private btn1_Click(object sender,EventArgs e)
12     {
13         BackgroundWorker back = new BackgroundWorker();
14         back.DoWork += back_DoWork;
15         back.Start();
16         DoSomething(); //NO.1
17     }
18     private void back_DoWork(object sender,DoWorkEventArgs e)
19     {
20         DoSomething(); //NO.2
21     }
22 }

上面程式碼Code 2-3中有兩處呼叫了DoSomething方法,一個在btn1.Click的事件處理程式中,一個在back.DoWork的事件處理程式中,前者在UI執行緒中執行,而後者在非UI執行緒中執行,兩者可以同時進行。

當我們不確定我們編寫的方法到底會在哪些執行緒中執行時,我們最好需要特別注意一下,如果方法訪問了公共資源,多個執行緒同時執行這個方法時可能會引起資源異常。另外,只要我們確定了兩個方法只會執行在同一個執行緒中,那麼這兩個方法不可能同時執行,跟方法處在的位置無關,

 1 //Code 2-4
 2 
 3 class Form1:Form
 4 {
 5     //
 6     private void DoSomething()
 7     {
 8         //do something here
 9         //maybe invoke UI controls
10     }
11     private void btn1_Click(object sender,EventArgs e)
12     {
13         DoSomething(); //NO.1
14     }
15     private void btn2_Click(object sender,EventArgs e)
16     {
17         DoSomething(); //NO.2
18     }
19 }

上面程式碼Code 2-4中btn1.Click和btn2.Click的事件處理程式中都呼叫了DoSomething方法,但是由於btn1.Click和btn2.Click的事件處理程式都在UI執行緒中執行,所以這兩處的DoSomething方法不可能同時執行,只可能一前一後,此時我們不需要考慮方法中訪問的公共資源是否執行緒安全。

注:正常情況下,上面的結論成立,但是如果你非要在DoSomething中寫了一些特殊程式碼,比如Application.DoEvents(),那麼情況就不一定了,很有可能在btn1_Click中的DoSomething方法中呼叫btn2_Click方法,從而造成DoSomething方法還未結束,另一個DoSomething方法又開始執行,這個涉及到Windows訊息迴圈的知識,本書第八章有講到。

2.3 呼叫執行緒與當前執行緒

前一節中說明了執行緒與方法的關係,一個執行緒很少只呼叫一個啟動方法,多數情況下,啟動方法中會呼叫其它方法,一個方法在哪個執行緒中執行,那麼這個執行緒就是它的當前執行緒,

 1 //Code 2-5
 2 
 3 class A
 4 {
 5     public void DoSomething()
 6     {
 7         //do something here
 8         Console.WriteLine("currentthread is " + Thread.CurrentThread.Name);
 9     }
10 }
11 class Program
12 {
13     //the start method of main thread
14     static void Main()
15     {
16         A a = new A();
17         a.DoSomething(); //NO.1
18         Thread th1 = new Thread(new ThreadStart(th1_proc));
19         th1.Start();
20     }
21     static void th1_proc()
22     {
23         A a = new A();
24         a.DoSomething(); //NO.2
25     }
26 }

上面程式碼Code 2-5中,在NO.1處,主執行緒就是呼叫執行緒,它呼叫了a.DoSomething方法,這時候a.DoSomething中會輸出主執行緒的Name屬性值。在NO.2處,th1才是呼叫執行緒,它呼叫了a.DoSomething方法,這時候a.DoSomething中會輸出th1執行緒的Name屬性值。

也就是說,哪個執行緒呼叫了方法,哪個執行緒就叫做這個方法的呼叫執行緒,方法在哪個執行緒中執行,哪個執行緒就是該方法的當前執行緒。

2.4 阻塞方法與非阻塞方法

首先,阻塞和非阻塞的概念是相對的,一個方法耗時很長才能返回,返回之前會一直阻塞呼叫執行緒,我們叫它阻塞方法;相反,一個方法耗時短,一呼叫馬上就返回,我們叫它非阻塞方法。但是這個"很長"與"很短"根本就沒有標準,

 1 //Code 2-6
 2 
 3 class Program
 4 {
 5     static void Func1()
 6     {
 7         for(int i=0;i<100;++i)
 8         {
 9             Thread.Sleep(10);
10         }
11     }
12 
13     static void Func2()
14     {
15         for(int i=0;i<100;++i)
16             for(int j=0;j<100;++j)
17             {
18                 Thread.Sleep(10);
19             }
20     }
21     static void Main()
22     {
23         Func1(); //NO.1
24         Console.WriteLine("Func1 over");
25         Func2(); //NO.2
26         Console.WriteLine("Func2 over");
27     }
28 }

上面程式碼Code 2-6中,Func1相對於Func2來講,耗時短,我們把Func1叫做非阻塞方法,Func1不會阻塞它的呼叫執行緒,下面的Console.WriteLine很快就會執行;而相反,Func2耗時長,我們把Func2叫做阻塞方法,Func2會阻塞它的呼叫執行緒,下面的Console.WriteLine不能馬上執行。

現實程式設計中,也沒有嚴格標準,如果一個方法有可能耗時長,那麼就把它當作阻塞方法。在程式設計中,需要注意阻塞方法和非阻塞方法的使用場合,有的執行緒中不應該呼叫阻塞方法,比如Winform中的UI執行緒。

有時候一個類會提供兩個功能相同的方法,一種是阻塞方法,它會阻塞呼叫執行緒,一直等到任務執行完畢才返回,另一種是非阻塞方法,不管任務有沒有執行完畢,馬上就會返回,不會阻塞呼叫執行緒,至於任務何時執行完畢,它會以另一種方式通知呼叫執行緒。這兩種呼叫方式也稱為"同步呼叫"和"非同步呼叫",FileStream.Read和FileStream.BeginRead就屬於這一類。

注:同步呼叫和非同步呼叫在後面的章節會講到,非同步程式設計模型(Asynchronous Programming Model)是.NET中一項重要技術。我們既可以把耗時10S的方法稱為阻塞方法,也可以把耗時100MS的方法稱為阻塞方法,理論上沒有標準。

2.5 UI執行緒與執行緒

UI執行緒一般出現在Winform程式設計中,主要負責使用者介面(User Interface)的訊息處理。本質上,UI執行緒跟普通執行緒沒有什麼區別。

一個執行緒只有不停地迴圈去處理任務才不會馬上終止,也就是說,執行緒必須想辦法去維持它的執行,不然很快就會執行結束。UI執行緒中包含一個Windows訊息迴圈,使用常見的While結構實現,該迴圈不停地獲取使用者輸入,包括滑鼠、鍵盤等輸入資訊,然後不停地處理這些資訊,正因為有這樣一個While迴圈存在,UI執行緒才不會一開始就馬上結束。

圖2-2 一個維持執行緒執行的迴圈結構

Winform中的UI執行緒預設由Program.Main方法中的Application.Run()進入,While迴圈結構存在於Application.Run()內部(或者由其呼叫)。

注:詳細的Windows訊息迴圈,請參見第八章。使用任何語言開發的Windows桌面應用程式都至少包含一個UI執行緒。

2.6 原子操作

所謂"原子",即不可再分的意思。程式碼中的原子操作指程式碼執行的最小單元,也就是說,原子操作不可以被中斷,它只有三個狀態:未執行、正在執行和執行完畢,絕對沒有執行到一半暫停下來,等待一會,又繼續執行的情況。原子操作又稱為程式中不可以被執行緒排程打斷的操作。

比如給一個整型變數賦值"int a = 1;",這個操作就是原子操作,不可能有給a賦值到一半,操作暫停的情況。相反有很多操作,屬於非原子操作,比如對一些集合容器的操作,向某些集合容器中增加刪除元素等操作都是非原子操作,這些操作可能被打斷,出現操作一半暫停的情況。非原子操作由許許多多的原子操作組成。

圖2-3 原子操作與非原子操作

圖2-3中虛線框表示一個非原子操作,一個非原子操作由多個原子操作組成,虛線框中的操作可能在NO.1、NO.2、 NO.3任何一個地方暫停。

注:原子操作與非原子操作不是以程式碼行數來區分的,是以這個操作在底層怎麼實施去區分的,比如"a++;"只有一行程式碼,但是它不是原子操作,它底層實現是由許多原子操作組合而成。

2.7 執行緒安全

我們操作一個物件(比如呼叫它的方法或者給屬性賦值),如果該操作為非原子操作,也就是說,可能操作還沒完成就暫停了,這個時候如果有另外一個執行緒開始執行同時也操作這個物件,訪問了同樣的方法(或屬性),這個時候可能會出現一種問題:前一個操作還未結束,後一個操作就開始了,前後兩個操作一起就會出現混亂。

當多個執行緒同時訪問一個物件(資源)時,如果每次執行可能得到不一樣的結果,甚至出現異常,我們認為這個物件(資源)是"非執行緒安全"的。造成一個物件非執行緒安全的因素有很多,上面提到的由於非原子操作執行到一半就中斷是一種,還有一種情況是多CPU情況中,就算操作沒有中斷,由於多個CPU可以真正實現多執行緒同時執行,所以還是有可能出現"對同一物件同時操作出現混亂"的情況。

圖2-4 兩種可能引起非執行緒安全的情況

圖2-4中左邊兩個執行緒執行在單CPU系統中,A執行緒中的非原子操作中斷,對R的操作暫停,B執行緒開始操作R,前後兩次操作相互干擾,可能出現異常。圖中右邊兩個執行緒執行在雙CPU中,無論操作是否中斷,都可能出現兩個操作相互干擾的情況。

為了解決多執行緒訪問同一資源有可能引起的不穩定性,我們需要在操作方法中做一些改進,最常見的是:對可能引起不穩定的操作加鎖。在程式碼中使用lock程式碼塊、互斥物件等來實現。如果一個物件,在多個執行緒訪問它時,不會出現結果不穩定或異常情況,我們稱該物件為"執行緒安全"的,也稱訪問它的方法是"執行緒安全"的。

 1 //Code 2-7
 2 
 3 class A
 4 {
 5     //
 6     int _a1 = 0;
 7     int _a2 = 0;
 8     object _syncObj = new object();
 9     public Int Result
10     {
11         get
12         {
13             lock(_syncObj)
14             {
15                 if(_a2!=0) //NO.1
16                 {
17                     return _a1/_a2; //NO.2
18                 }
19                 else
20                 {
21                     return 0;
22                 }
23             }
24         }
25     }
26     public void DoSomething(int a1, int a2)
27     {
28         lock(_syncObj)
29         {
30             _a1 = a1;
31             _a2 = a2;
32         }
33     }
34     //other public methods
35 }

上面程式碼Code 2-7中,單CPU時,如果沒有lock塊,多執行緒訪問A類物件,一個執行緒在訪問A.Result屬性時,在判斷if(_a2!=0)為true後,可能在NO.1之後和NO.2之前處出現中斷(執行緒掛起),此時另一執行緒透過DoSomething方法修改_a2的值為0,中斷恢復後,程式報錯。雙CPU中,如果沒有lock塊,多執行緒訪問A類物件,情況更糟,一個執行緒訪問A.Result屬性時,不管在NO.1之後和NO.2之前會不會中斷,另一個執行緒都有可能透過DoSomething方法修改_a2的值為0,程式報錯。

另外,在Winform程式設計中,我們常遇見的"不在建立控制元件的執行緒中訪問該控制元件"的異常,原因就是對UI控制元件的操作幾乎都不是執行緒安全的(部分是),一般UI控制元件只能由UI執行緒操作,其餘的所有操作均需要投遞到UI執行緒之中執行,否則就像前面講的,程式出現異常或不穩定。

 1 //Code 2-8
 2 
 3 class Form1:Form
 4 {
 5     //
 6     private btn1_Click(object sender,EventArgs e)
 7     {
 8         DealControl(null); //NO.1
 9         Thread th1 = new Thread(new ThreadStart(th1_proc));
10         th1.Start();
11     }
12     private void th1_proc()
13     {
14         DealControl(null); //NO.2
15     }
16     private void DealControl(object[] args)
17     {
18         //
19         if(this.InvokeRequired) //NO.3
20         {
21             this.Invoke((Action)delegate() //NO.4
22             {
23                 DealControl(args);
24             });
25         }
26         else
27         {
28             //access ui controls directly
29             //
30         }
31     }
32 }

上面程式碼Code 2-8中,DealControl方法中需要操作UI控制元件,如果我們不知道DealControl到底會在哪個執行緒中執行,有可能在UI執行緒也有可能在非UI執行緒,那麼我們可以使用Control.InvokeRequired屬性去判斷當前執行緒是否是建立控制元件的執行緒(UI執行緒),如果是,則該屬性返回false,可以直接操作UI控制元件,否則,返回true,不能直接操作UI控制元件。程式碼中NO.1處直接在UI執行緒中呼叫DealControl,DealControl中可以直接操作UI控制元件,NO.2處在非UI執行緒中呼叫DealControl,那麼此時,就需要將所有的操作透過Control.Invoke投遞封送到UI執行緒之中執行。

注:Control包含有若干個執行緒安全的方法和屬性,我們可以在非UI執行緒中使用它們。有Control.InvokeRequired屬性、Control.InvokeControl.BeginInvokeControl.Invoke的非同步版本,後續章節有講到)、Control.EndInvoke以及Control.CreateGraphics方法。跨執行緒訪問這些方法和屬性不會引起異常。

2.8 呼叫(Call)與回撥(CallBack)

呼叫(Call)和回撥(CallBack)是程式設計中最常遇見的概念之一,幾乎出現在程式碼中的每一處,只是許多人並沒有在意。現在最流行的解釋是:呼叫指我們呼叫系統的方法,回撥指系統呼叫我們寫的方法。類似下面圖2-5描述的:

圖2-5 呼叫與回撥的區別

上圖2-5是目前對"呼叫"和"回撥"的解釋。但是需要清楚一點,本章第一節中已經講到過,客戶端(圖中Client)並不是絕對的,也就是說,Client也有可能成為圖中的"系統"部分,別人再呼叫它,它再回撥另一個client。

圖2-6 程式中呼叫與回撥的關係

圖2-6中描述一個程式中呼叫與回撥的關係,我們平常對"呼叫"和"回撥"的定義只侷限在圖中虛線框中,它只是一個小範圍的規定。嚴格意義上講,不應該有呼叫和回撥之分,因為所有程式碼最終均由作業系統呼叫(甚至更底層)。

.NET中的回撥主要是透過委託(Delegate)來實現的,委託是一種代理,專門負責呼叫方法(委託的詳細資訊在本書第五章有講到)。

2.9 託管資源與非託管資源

其實這裡的"託管"跟第一章中講到的託管環境、託管程式碼或者託管時代中的"託管"意思一樣。在.NET中,物件使用的資源分兩種:一種是託管資源,一種是非託管資源。託管資源由CLR管理,也就是說不需要開發人員去人工控制,相對開發人員來講,託管資源的管理幾乎可以忽略,.NET中託管資源主要指"物件在堆中的記憶體"等;非託管資源指物件使用到的一些託管環境以外(比如作業系統)的資源,CLR不會管理這些資源,需要開發人員人工去控制。.NET中物件使用到的非託管資源主要有I/O流、資料庫連線、Socket連線、視窗控制程式碼等各種直接與作業系統相關的資源。

圖2-7 一個堆中物件使用的資源

圖2-7中虛線框表示"可能有",即一個堆中物件可能使用到了非託管資源,但是它一定使用了託管資源。一個物件在使用完畢後(進入不可達狀態,並不是死亡,第四章會講到區別),我們應該確保它使用的(如果使用了)非託管資源能夠及時釋放,歸還給作業系統,至於託管資源,我們大部分時間不需要去關心,因為CLR(具體應該是Garbage Collector)會幫我們處理。.NET中使用了非託管資源的型別有很多,比如FileStream、Socket、Font、Control(及其派生類)、SqlDataConnection等等,它們內部封裝了非託管資源,沒有使用非託管資源的型別也有很多,比如Console、EventArgs、ArrayList等等。

怎麼完美地處理一個物件使用的非託管資源,是一門相當重要而且必學的技術,後面第四章有詳細提到。

注:現在普遍有一種錯誤的觀點就是,將FileStreamSocket這樣的型別物件稱為非託管資源,這個是錯誤的,只能說這些物件使用到了非託管資源。

2.10 框架(Frameworks)與庫(Library)

框架和類庫都是一系列可以被重用的程式碼集合。不同的是,框架算是不完整的應用程式,理論上,我們不用寫任何程式碼,框架本身可以執行起來;而類庫多半指能夠提供一些具體功能的類集合,它包含的內容和功能一般比框架更簡單。我們使用框架去開發一個應用程式,其實就是在框架的基礎上寫一些擴充套件程式碼,框架就像一個沒有裝修的毛坯房屋,我們需要給它各種裝飾,在這個過程中,我們可以使用類庫,因為類庫可以為我們提供一些封裝好了的功能。下圖2-8為框架、程式(開發人員編寫)以及類庫三者之間的關係:

圖2-8 框架程式類庫之間的關係

圖2-8中的呼叫關係其實是雙向的,畫出的箭頭只顯示了主要呼叫關係,即框架呼叫開發人員程式碼,後者再選擇性呼叫一些類庫。

從上圖2-8中我們可以看出,整個應用程式的最終控制權並不在開發人員手中,而是在框架方,這種現象稱為"控制轉換"(Inversion Of Control,IOC),即程式的執行流程由框架控制,幾乎所有框架都遵循這個規則。

 1 //Code 2-9
 2 
 3 class Program
 4 {
 5     //
 6     static int GetTotal(int first,int second)
 7     {
 8         return first + second;
 9     }
10     static void Main()
11     {
12         int first,second;
13         Console.WriteLine("Input first:");
14         first = int.Parse(Console.ReadLine()); //NO.1
15         Console.WriteLine("Input second:");
16         second = int.Parse(Console.ReadLine()); //NO.2
17         int total = GetTotal(first,second); //NO.3
18         Console.WriteLine("the total is:" + total);
19         Console.Read();
20     }
21 }

上面程式碼Code 2-9演示了從控制檯程式(不使用框架開發)中獲取使用者輸入的兩個資料,然後輸出兩個資料之和,每個步驟的方法均由我們自己呼叫(NO.1. NO.2以及NO.3)。如果我們採用Winform程式(使用框架開發)實現,程式碼如下:

 1 //Code 2-10
 2 
 3 class Form1:Form
 4 {
 5     public Form1()
 6     {
 7         //
 8         this.btn1.Click+=(EventHandler)(delegate(object sender,EventArgs e)
 9         {
10             int first = int.Parse(txtFirst.Text); //NO.1
11             int second = int.Parse(txtSecond.Text); //NO.2
12             int total = GetTotal(first,second); //NO.3
13             MessageBox.Show("the total is:" + total);
14         });
15     }
16     private int GetTotal(int first,int second)
17     {
18         return first + second;
19     }
20 
21 }

上面程式碼Code 2-10演示了從窗體介面中的txtFirst和txtSecond兩個文字框中獲取資料,然後計算出兩個資料之和,每個步驟的方法都是由系統(框架)呼叫(在btn1.Click事件處理程式中)。使用框架開發的程式,程式碼中大部分方法都屬於"回撥方法"。

注:"控制轉換原則"又稱為"Hollywood Principle",即Don't call us, we will call you.意思是指好萊塢製片公司會主動聯絡演員,而不需要演員自己去找電影製片公司。

2.11 面向(或基於)物件與面向(或基於)元件

這四個概念中最為熟悉的當然是"物件導向",其它三個離我們有點遙遠,平時接觸不多。

基於物件:如果一種程式語言有封裝的概念,能夠將資料和操作封裝在一起,形成一個整體,同時它又不具備像繼承、多型這些OO特性,那麼就說這種語言是基於物件的,比如JavaScript。

物件導向:在基於物件的基礎之上,還具備繼承、多型特性的程式語言,我們稱該程式語言是物件導向的,比如C#,Java。

基於元件:元件是共享二進位制程式碼的基本單元,它是一個已經編譯完成的模組,可以在多個系統中重用。在軟體開發中,我們事先定義好固定介面,然後將各個功能分開獨立開發,最後生成各自獨立的模組。程式執行之後,分別載入這些獨立的模組,各個模組負責完成自己的功能,我們稱這種開發模式是基於元件的。基於元件開發模式中,除了二進位制程式碼可以重用外,還有另外一個優點,如果我們需要更新某一功能,或修復某一功能中的bug,在不改變原有介面前提下,我們不用重新編譯整個程式的原始碼,而只需要重新編譯某個元件原始碼即可。元件應該是語言獨立的,一種語言開發出來的元件,理論上任何一種語言都可以使用它。

面向元件:基於元件開發中,我們只能重用已經編譯完成的二進位制程式碼,並不能從這個已經編譯好的元件中讀取其它資訊,比如識別元件中的型別資訊,派生出新的型別。面向元件指,在開發過程中,我們不僅能夠重用元件中的程式碼,還能以該元件為基礎,擴充套件出新的元件,比如我們可以識別.NET程式集中的型別資訊,以此派生出新的型別。.NET開發便是一種面向元件的開發模式。

注:如果說物件導向是強調型別與型別之間的關係,那麼面向元件就是強調元件與元件之間的關係。另外,我們需要知道,.NET中的元件(程式集)並不包含傳統意義的二進位制程式碼。

2.12 介面

我們在閱讀一些書籍或者網上瀏覽一些文章時,經常會碰到"介面"的概念,比如"一個類應該儘可能少的對外提供公共介面"、"我們應該先取得淘寶的支付介面許可權"、"繪製圖形時,我們需要呼叫系統的DrawImage介面"等等。那麼,介面到底是什麼?

其實我們碰到的這些"介面"概念跟它字面意思一樣:對外提供的、可以完成某項具體功能的通道。比如我們電腦上的USB口,透過它,我們能夠與電腦傳輸資料,還比如電視機的音量按鈕,透過它,我們可以調節電視機喇叭發出聲音的大小。介面是外界與系統(或模組)內部通訊的通道。

    注:"介面"的概念基於"封裝"前提之上,如果沒有"封裝",那麼就沒有"外界"與"內部"之說。

在軟體一般架構設計圖中,介面用以下表示:

圖2-9 介面示意圖

如上圖2-9所示,圓圈代表對外公開的通道,S的內部細節對外界C是不可見的。注意圖中的S不一定代表一個類,它可以是一個系統(跟C所屬不同的系統)、一個模組或者其它具有"封裝"效果的單元個體。下圖2-10顯示某些場合存在的介面:

圖2-10 各種場合下的介面

如上圖2-10顯示了各種場合中的介面,可以看到,介面的概念不僅侷限在程式碼層面。下表2-1顯示了各種介面的表現形式:

表2-1各種場合中介面的具體表現形式

序號

場合

介面的表現形式

誰是外界

說明

1

類的公開方法,如

People p = new People();

p.Walk();

類的使用者

類的使用者不知道People類內部具體實現,但是可以與之通訊

2

作業系統

Win32 API,如

SetWindowText(hWnd,”text”); //設定某視窗標題

GUI開發者

GUI開發者不知道作業系統內部實現,但是可以與之通訊

3

微博開放平臺

https協議url,如載入最新微博

https://api.weibo.com/2/statuses/public_timeliti.json?parameter1=12&parameter2=22

微博第三方應用開發者

微博第三方應用開發者不知道微博伺服器內部實現,但是可以與之通訊

4

Google地圖服務

http協議url,如查詢指定城市

地理座標資訊

http://maps.googleapis.com/maps/api/geocode/xml?address=london&sensor=false

地圖第三方應用開發者

地圖第三方應用開發者不知道地圖伺服器內部實現,但是可以與之通訊

在.NET程式設計中,還存在另外一種意義的"介面",即我們使用interface關鍵字定義的介面型別,這種"介面"嚴格意義上講跟我們剛才討論的"介面"不能做相等比較。更準確來說,它代表程式設計過程中的一種"協議",是程式碼中呼叫方和被呼叫方必須遵守的契約,如果某一方不遵守,那麼呼叫就不會成功。

    注:有關"協議",請參見下一節。

2.13 協議

協議,即約定、契約。兩個(或兩個以上)個體合作時需要共同遵守的準則,哪一方不遵守該準則,大部分時候將會導致合作失敗,這個是現實生活中我們理解的"協議"。在計算機(程式設計)世界中,"協議"帶來的效果同樣如此。

計算機網路通訊中,OSI(Open System Interconnection,開放系統互聯模型)將網路分為7層,每層均有多種協議,通訊雙方必須分別遵守各層中對應的協議,如下圖2-11:

圖2-11 網路七層協議

如上圖2-11所示,資料傳送方必須按照規定協議封裝資料,然後才能傳送給另一方;同理,資料接收方必須按照對應協議解析接收到的資料包,然後才能獲得傳送方傳送的原始資料。在實際通訊程式設計中,這些"封裝/解析"的步驟均已被計算機底層模組完成,因此對使用者來講,這些過程都是透明的,它們一直都在,並且是雙方通訊的關鍵。

網路通訊協議是一種資料結構,很多書籍中講到了TCP/UDP協議結構,介紹了協議結構中每(幾)個位元組分別代表什麼內容,資料傳送方按照規定的格式填充該資料結構,資料接收方按照規定的格式去解析該資料結構,從而得到原始資料。不管TCP協議還是UDP協議,均屬於傳輸層協議。對於某些高階語言(如C#、Java)開發者而言,接觸這些協議的機會很少,更多時候,我們接觸的是應用層協議,如HTTP協議、FTP協議等,除了這些主流、廣為人知的協議外,我們自己在開發網路程式時,也可以自己定義自己的應用層協議,如在編寫雷達航跡顯示系統時,我們可以將接收到的原始雷達資料進行預處理,以某一種預先定義的資料結構(也就是協議)轉發給其他人,其他人按照預先定義好的資料結構(協議)去解析接收到的資料包;還比如在一些即時通訊程式中,可能存在"文字訊息"、"圖片"、"表情"或者"檔案"等一些資料型別,那麼我們完全可以定義一個自己的應用層協議,見下圖2-12:

圖2-12 自定義應用層協議

如上圖2-12所示,第一個位元組表示訊息型別,是文字訊息還是表情,可以透過該位元組區分,第2~5個位元組表示雙方通訊次數,第6~9個位元組表示"資料區"長度,之後的N個位元組表示傳送的"原始資料",倒數兩個位元組為一些附加資料,最後一個位元組為校驗碼,整個資料結構的長度為:(1+4+4+資料區長度+2+1)個位元組。傳送方填充完整個資料結構,然後傳送給接收方,接收方接收到資料後,按照已知的資料結構格式去解析獲得其中的原始資料。傳送"文字訊息"的示例程式碼如下:

 1 //Code 2-11
 2 
 3 public static void SendStringMsg(int sequence, string msg)
 4 {
 5     byte[] msg_buffer = Encoding.Unicode.GetBytes(msg);
 6     byte[] send_buffer = new byte[12 + msg_buffer.Length]; //     NO.1 1 + 4 + 4 + N + 2 + 1
 7     using (MemoryStream ms = new MemoryStream(send_buffer))
 8 {
 9         using (BinaryWriter bw = new BinaryWriter(ms))
10         {
11             bw.Write((byte)1); //NO.2
12             bw.Write(sequence); //NO.3
13             bw.Write(msg_buffer.Length); //NO.4
14             bw.Write(msg_buffer); //NO.5
15             bw.Write((short)0); //NO.6
16             bw.Write((byte)0); //NO.7
17         }
18     }
19     //send 'send_buffer' to receiver with socket... NO.8
20 }        

如上程式碼Code 2-11所示,首先定義一個傳送緩衝區(NO.1處),因為12個位元組已固定,所以緩衝區的長度應該是:12+文字訊息長度,然後依次將訊息型別(NO.2處)、順序號(NO.3處)、資料長度(NO.4處)、文字訊息內容(NO.5處)、附加字(NO.6處)和校驗碼(NO.7處)寫入緩衝區,傳送方按照預定義格式填充位元組流緩衝區,再將其傳送給對方(NO.8處);對應的,接收方接收到資料後,按照預定義格式解析位元組流。下圖2-13顯示順序號為10,文字訊息為"ABC"時傳送緩衝區send_buffer中的內容:

圖2-13 傳送緩衝區中的內容

    注:在TCP通訊中,由於資料是以"流"的形式傳遞的,前後傳送的資料連線在一起,接收方無法區分單個的訊息(找不到訊息邊界),若按照上面提到的預先定義一個傳輸協議,接收方可以按照該協議解析出一條完整的訊息。詳細參見本書中後續有關"網路程式設計"的第九章。

不僅網路通訊需要"協議"的輔助,計算機世界中還有很多場合需要"協議"的輔助,如加密和解密、編碼和解碼以及CPU執行機器指令、計算機透過USB口與外設交換資料等,下面表2-2顯示了各種場合中的"協議":

表2-2 各種場合中的協議

序號

場合

協議

說明

1

加密/解密

使用的同一套演算法

加密和解密的演算法必須配套,否則會解密失敗

2

編碼/解碼

使用的同一種編碼規範

如各種編碼規範:Unicode、UTF-8、Ascll,編碼和解碼必須使用同一套規範,否則會出現亂碼

3

CPU執行機器指令

CPU和編譯器使用的同一套CPU指令集

CPU和編譯器必須使用同一套指令集,傳統編譯器將高階語言直接編譯成與平臺相關的機器碼,機器碼只能在指定平臺上執行,CPU和編譯器須遵守同一個規範

4

USB介面

計算機和外設使用的同一種USB規範

計算機與外設必須使用同一種USB規範,如USB1.0、USB1.1或USB2.0,否則兩者之間不能正常互動(不考慮相容情況)

到目前為止,我們講到的"協議"都能很好地跟現實關聯起來,或者說,它們都跟協議字面意思接近。其實在.NET程式開發過程中,也有一種"協議",它便是使用關鍵字interface宣告的介面。使用interface宣告的介面也是一種"協議",它規定了程式碼呼叫方與程式碼被呼叫方共同遵守的一種規範,前面說過,程式碼中Client端與Server端需要互動,那麼只有雙方共同遵守某一約定,工作才能正常進行。這種協議在程式碼中具體體現在:

    1)呼叫方必須存在一個介面引用;

    2)被呼叫方必須實現該介面。

具體示例程式碼見Code 2-12:

 1 //Code 2-12
 2 
 3 interface IWalkable //NO.1
 4 {
 5     void Walk();
 6 }
 7 class People:IWalkable //NO.2
 8 {
 9     public void Walk()
10     {
11         //
12     }
13 }
14 class Program
15 {
16     static void Main()
17     {
18         IWalkable w = new People();
19         Func(w);
20     }
21     static void Func(IWalkable w) //NO.3
22     {
23         w.Walk();
24     }
25 }

如上程式碼Code 2-12中,NO.1處定義了一個協議(介面),被呼叫方(NO.2處)遵守了該協議(實現介面),呼叫方也遵守了該協議(NO.3處,包含一個介面型別引數)。雙方都遵守了同一個協議,才能協調好工作。下圖2-14顯示了"協議"在程式碼呼叫中起到的作用:

圖2-14 程式碼呼叫中的協議

    注:程式碼中使用interface宣告的"介面"在面向抽象程式設計中起到了非常重要的作用,詳細參見本書第十二章。

2.14 本章回顧

本章共介紹了13個將在本書中遇到的概念(術語),或許我們曾經瞭解過某些概念的含義,但一直處於似懂非懂的狀態,那麼閱讀完本章,你肯定會拍下腦袋,高呼:原來是這樣!有些概念在其它地方几乎找不到準確的解釋,比如"執行緒和方法的關係"、"庫與框架區別"以及"程式碼中的協議"等等;另外一些概念雖然能找到一些解釋說明,但並沒有像本章講得這麼詳細。總之,本章定會掃清我們在程式設計道路上遇見的虐心絆腳石。

2.15 本章思考

1.下面程式碼Code 2-13中MyContainer類中的_int_list成員是否是執行緒安全的,為什麼?

 1 //Code 2-13
 2 
 3 class MyContainer
 4 {
 5     List<int> _int_list = new List<int>();
 6     public void Add(int item)
 7     {
 8         _int_list.Add(item);
 9     }
10     public int GetAt(int index)
11     {
12         return _int_list[index];
13     }
14 }

A:不是執行緒安全的,因為無論是MyContainer.Add()方法還是MyContainer.GetAt()方法,均可以同時在多個執行緒中執行,這就意味著可能存在多個執行緒同時訪問集合容器_int_list,可以在MyContainer.Add()以及MyContainer.GetAt()方法中加上鎖(lock(object))來解決該問題。

2.舉例說明實際開發過程中遇見的框架和庫有哪些。

A:框架有:Asp.NET MVC、Asp.NET Webforms、Windows Forms、WCF、WPF以及SilverLight等;庫包括公司內部一些通用庫,如MySQL資料庫訪問工具庫、日誌記錄工具庫、字串處理工具庫、圖片處理工具庫以及加解密工具庫等等。

(本章完)

 

相關文章