相關文章連線:
高屋建瓴:梳理程式設計約定
- 2.1 程式碼中的Client與Server 21
- 2.2 方法與執行緒的關係 22
- 2.3 呼叫執行緒與當前執行緒 24
- 2.4 阻塞方法與非阻塞方法 24
- 2.5 UI執行緒與執行緒 25
- 2.6 原子操作 26
- 2.7 執行緒安全 27
- 2.8 呼叫(Call)與回撥(CallBack) 30
- 2.9 託管資源與非託管資源 31
- 2.10 框架(Frameworks)與庫(Library) 32
- 2.11 面向(或基於)物件與面向(或基於)元件 33
- 2.12 介面 34
- 2.13 協議 35
- 2.14 本章回顧 39
- 2.15 本章思考 39
在實際程式設計中,我們會遇見各種各樣的概念,雖然有的並沒有官方定義,但是我們可以自己給它取一個形象的名稱。本章總結了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,前者為後者提供服務。
注:Client和Server不一定指的是物件,A程式集呼叫B程式集中的型別,我們可以把A當作Client,把B當作Server。Client與Server也不是絕對的,在一定場合,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.Invoke、Control.BeginInvoke(Control.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等等。
怎麼完美地處理一個物件使用的非託管資源,是一門相當重要而且必學的技術,後面第四章有詳細提到。
注:現在普遍有一種錯誤的觀點就是,將FileStream、Socket這樣的型別物件稱為非託管資源,這個是錯誤的,只能說這些物件使用到了非託管資源。
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¶meter2=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資料庫訪問工具庫、日誌記錄工具庫、字串處理工具庫、圖片處理工具庫以及加解密工具庫等等。
(本章完)