相關文章連接:
高屋建瓴:梳理編程約定
- 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數據庫訪問工具庫、日志記錄工具庫、字符串處理工具庫、圖片處理工具庫以及加解密工具庫等等。
(本章完)