目錄
一 軟體構建,智者的積木
二 語言沒有“銀彈”正如世間難尋“聖杯”
三 .NET平台結構
一 軟體構建,智者的積木
多年來,眾多的業界巨子給軟體構建做出了不同的比喻,雖然你可能並不認識他們,也不打算瞭解這些巨人的英雄史詩,但是你應該瞭解他們對軟體構建的看法,畢竟他們才是這場遊戲中的進階玩家。我並不想討論這些隱喻是否合理,我只希望以下隱喻能夠協助你重新思考下軟體構建活動。
寫作 這一隱喻暗示著軟體開發過程是一種以編寫代碼為主的代價昂貴的試錯過程,而非仔細的規劃和設計。
耕種 這一隱喻暗示了軟體就想是耕種一樣,你每次只處理它的一小部分,一點一點的加到整個系統,使系統一點一點的“生長”。它也暗示了,工作應該按部就班,正如春種秋收一樣,各個環節有強硬的邏輯存在。
蓋房子 這一隱喻形象的說明了規劃和設計在構建活動中的重要性,同時也暗示了軟體在初期修複缺陷的成本要遠遠低於在後期的修複成本。
焦油坑 經典書籍《人月神話》的第一章提到的:“大型系統的開發就猶如這樣一個焦油坑,很多大型和強壯的動物在其中劇烈的掙紮,他們中大多數開發出了可啟動並執行系統——不過只有極少數的項目滿足了目標、進度和預算的要求。”
不管你是否同意以上觀點,它們確實在一定時期影響了一部分人。而我想從一名程式員的角度來說我對軟體構建的認識。我覺得軟體構建過程對於程式員來說,就像是在玩樂高積木。眾多的語言給我們提供了形形色色的API,就如同不同形狀、顏色的積木塊。業界標準構成了積木之間的接點,使積木塊之間無縫銜接。單個的小積木通過接點組合成你想要的任何形狀和功能的大的模組,模組和模組之間再相互組合,形成更大的系統。軟體構建與玩積木兩者都是考驗腦力的智力活動,你可以按照圖紙玩積木(別人設計的),也可以用積木來拼裝你想要的任何東西(自己做設計)。(ps.我小時候用樂高的“潛水艇”積木拼出了星際I中的“歌利亞”。)也就是說,語言、平台就在那裡,你能做出什麼,就看你能想到什麼,以及你的團隊能實現什麼。
軟體構建是一項十分複雜的工作,絕不是一個人的工作,而是一件團隊活動。下面,給出軟體構建的最普遍活動:
- 定義問題
- 需求分析
- 規劃構建
- 軟體架構
- 詳細設計
- 編碼與調試
- 單元測試
- 整合測試
- 整合
- 系統測試
- 保障維護
以上活動在整個軟體構建過程中會反覆、交替進行,這些活動僅僅是基於技術的產品導向過程,而沒有包括相應的管理活動。對於程式員來說,編碼與調試往往佔據了我們的大部分時間。而其他活動隨項目的類型和規模,往往有所“裁剪”。可見,軟體構建過程十分複雜,正因為其複雜性,人們為了確保軟體項目的成功,試圖尋找一套結構化方法。但是到目前為止,也沒有找到能夠保證軟體項目100%成功的方法。雖然軟體項目中,“聖杯”並不存在,但是我們仍然得到了相當有用的方法或指南,以提高項目成功幾率,並使這種成功得以再現,例如軟體工程理論,PMI制定的專案管理指南(PMBOK)以及敏捷開發的相關思想等等。
二 語言沒有“銀彈”正如世間難尋“聖杯”
如果把開發語言當成一門外語的話,那麼我們程式員無疑掌握了眾多的“外語”,即使外語翻譯也望塵莫及。正如外語一樣,不同語言有不同的特點,要造就了不同的陣營與擁護者。在不同陣營,並相互抨擊的時候,你是否還猶豫不決,是否又有轉移方向的衝動?某位業界大牛說過,好的程式員要掌握3類語言:一、快速開發的語言;二、吃飯的語言;三、最鐘愛的語言。這隻是3類語言,並不是說3種。你完全可以學習一門語言並說它在這3個方面都很出色。(ps.目前我看PHP有這個苗頭。)還有一種說法就是“學得多,學的淺“。不管你是想深入掌握一門語言還是要廣泛瞭解多門語言,你一定要認識到開發語言沒有所謂的“銀彈”,或許某些語言能獨擋一面,當沒有哪種語言是全才,其必有優勢與劣勢,而且隨著時間的推移,今日風光的語言,明日可能就會被淘汰。如果你鐘愛一門語言就不要被路人輕易打動而另投他人門派。學習語言是一件持之以恒的事情,當你想改換門派的時候不妨問問,這門語言真的好嗎,你真的深入理解了你現在的語言了嗎,它真的是你想要的嗎?編程之真諦在于思想,透過語言編程才是正確的道路。
每個人的經曆不同可能會有不同的語言基礎,我在眾多語言之中選擇了C#,我並不認為C#對所有人都適合,但它的諸多優點讓我難以放棄。以下是我選擇C#的理由。
.Net平台非常強大 .NET平台提供了豐富的類庫,涵蓋了幾乎所有領域的應用,不需要藉助其它語言,就能開出幾乎所有的應用。當然,大量的API也需要我們不斷的學習。
強大的開發環境 Visual Studio 可謂是IDE中的王者,用了之後就會被其強大的功能所吸引,在結合微軟的TFS,在軟體專案管理中將發揮出極大的作用。
豐富的資源 微軟的MSDN,微軟社區還有大量的參考書籍,及龐大的使用者群,造就了一個龐大的資源寶庫,你身處於這樣龐大的群體之中,能夠更快更容易的掘取知識。
入門簡單 C#本身就是一門物件導向的進階語言。相比於低級語言,其文法和代碼可讀性更高,更容易讓人接受,你完全可以通過書籍自學成才。
開發週期快 C#或者說.NET的類庫已經為我們做了大量的工作,相比其它語言,你的代碼量更少,你開發系統的周期更短,無論是大型項目還是小型項目你都會得益於這種快速的開發。當然,開發週期短也得益於Visual Studio 的強大功能。
與各種微軟產品的無縫整合 你可以和微軟的其它產品例如SQL Server,Office,Sharepoint,OC等無縫整合。
當然,C#也有一些劣勢:
不跨平台 C#只能用於開發.NET程式,雖然.NET被設計為平台無關,但目前還無法在Linux等平台上穩定運行.NET程式,雖然有個開源的Mono,但其可靠性有待測試。這會讓你失去很大的發揮空間,即使你能構建客戶需要的軟體,如果客戶堅持使用Linux系統你也只能放棄。或許有那麼一天,.NET可以用於其它平台,但現在我們還是在Windows平台玩吧。
開源資源相對較少 相比其他語言,用C#寫的開源項目還是少數,有些時候你只能自己寫架構了。
綜上所述,語言沒有“銀彈”,如果你享受C#給你帶來的便捷,同時接受了它的不足,並決定深入學習的話,我希望下面的文章能對你有所協助。
三 .NET平台結構
從程式員的角度看,.NET可以理解為一個運行庫環境(mscoree.dll)和一個全面的基底類別庫(mscorlib.dll)。如所示:
(一)基底類別庫
.Net Framework 中包含了Framework 類庫(Framework Class Library,FCL),有些書籍中也稱為BCL,微軟正以驚人的速度在完善FCL,為我們構建大型系統提供便利。如果想全面瞭解FCL,我推薦的書籍是《C#進階編程》,目前已經出到第七版了。該書以頁數和全面著稱,但不足之處在於,其內容比較基礎,要想深入學習,需結合其它書籍。FCL十分龐大,而且在不斷完善中,最好有針對性的學習某一分支,避免將精力分散。
(二)通用語言執行平台
運行時(runtime),可以理解為執行給定編譯代碼單元所需的外部服務的集合。如所示,.NET的運行時叫CLR(Common Language Runtime),即通用語言執行平台,是一個可由多種開發語言使用的“運行時”。CLR不關心使用何種開發語言,只要相應的開發語言的編譯器面向CLR即可。微軟已經建立了幾個面向CLR的語言,包括:C++/CLI、C#、VB、F#、Python、Ruby,以及自己的編譯器。其它機構也制定了一些面向“CLR”的開發語言和編譯器。但是,使用最廣泛的還是微軟的C#語言。CLR的核心功能包括:記憶體管理,程式集載入,安全性,異常處理和線程同步等等。通常在CLR的控制下啟動並執行代碼稱為Managed 程式碼。原始碼的編譯和執行過程如下:
1 原始碼編譯
編譯的第一步——把原始碼編譯為託管模組(其中,C++的編譯器比較特殊,它能同時產生託管和Unmanaged 程式碼,並產生到同一模組中。)
託管模組 託管模組是一個標準32(64)位Windows可移植載體PE32(PE32+)檔案,需要CLR才能執行。託管模組的組成如下:
中繼資料 中繼資料是一組資料表。其中一些資料表描述了模組中定義的內容,比如類及其成員。還有一些中繼資料描述了Managed 程式碼模組引用的內容,比如引用的類及成員。 這些中繼資料描述了每一個二進位檔案中定義的類型(類、結構、枚舉等),以及每個類型的成員(屬性、方法、事件等)。Visual Studio 等開發工具通過中繼資料實現智能感知,而中繼資料也成為包括WCF、Web Service、反射、晚期綁定和對象序列化等技術的支柱。
MSIL Microsoft Intermediate Language (MSIL)微軟中繼語言,也被叫做CIL(通用中間語言),或者簡稱IL。其本質上是.NET平台的母語。任何一種支援.NET的語言在邏輯上都需要支援IL的,也都會被編譯為IL。
IL具有以下特徵:
- 物件導向和使用介面
- 實值型別和參考型別之間的巨大差異
- 強資料類型
- 使用異常來處理錯誤
- 使用特性
IL的好處:
- 語言整合性——每種支援.NET的語言產生的是幾乎相同的IL
- 平台無關性
我們不需要直接使用IL來編寫程式,但是在.NET中,使用System.Reflect.Emit這個命名空間提供的類型可以在開發出在運行時能夠在記憶體中產生.NET程式集的程式,即“動態程式集”。由於在構造程式集時需要使用專有的IL指令集,所以如果要使用這部分功能開發軟體,需要掌握IL。
編譯的第二步——將託管模組合并為程式集
CLR不和模組一起工作,它只和程式集一起工作。程式集是一個或多個模組/資源檔的邏輯性分組,是重用、安全性及版本控制的最小單元。通過程式集的概念,我們可以把一組檔案當做一個檔案來對待。將託管模組合并成程式集的過程如下:
程式集包含的足夠的資訊,使其具有自描述性。CLR能判斷出為了執行程式集中的代碼,程式集的直接依賴對象是什麼,不需要註冊表或 Active Directory Domain Services(ADDS)中儲存額外的資訊。所以,相較於非託管組件,程式集更容易部署。
2 一般型別系統與Common Language Specification
公用類型系統與Common Language Specification是,是.NET實現語言互通性的基石。語言互通性的真正含義是用一種語言編寫的類應該能直接與另一種語言編寫的類通訊,特別是:
- 用一種語言編寫的類應該能繼承用另一種語言編寫的類。
- 一個類應該能包含另一個類的執行個體,而不管它們是使用什麼語言編寫的。
- 一個對象應該能直接調用其它語言編寫的另一個對象的方法。
- 對象應該能在方法之間的傳遞。
- 在不同的語言之間調用方法時,應能在調試器中調試這些方法調用,即調試不同語言編寫的原始碼。
2.1 CTS
類型(type)指的是集合{類,介面,結構,枚舉,委託}裡的任意一個成員。CLR完全是圍繞類型展開的,微軟制定了一個正式的規範,即"一般型別系統"(Common Type System,CTS),它描述了類型的定義和行為。CTS規定,一個類型可以包含零個或多個成員,制定了類型的可視性規則和類型成員的訪問類型,為類型的繼承、虛方法、物件存留期定義了相應的規則。我們無論使用哪一種語言,類的行為都是完全一致的,因為最終是由CTS來定義類的行為。
CTS定義了5種類型:
上述5種類型包含眾多的類型成員即集合{構造器,析構器,靜態建構函式,巢狀型別,運算子,方法,屬性,索引器,欄位,唯讀欄位,常量,事件}中的元素之一。
CTS還定義了一個內容豐富的類型階層,其中包含設計合理的位置,在這些位置上,代碼允許定義它自己的類型。CTS的階層如下:
此外,CTS建立了一套定義明確的核心資料類型,以字串類型為例:String(VB.NET)、string(C#)、String^(C++/CLI)最終被解釋成為CTS資料類型System.String。
2.2 CLS
為了能適用多種語言,微軟定義了一個“Common Language Specification”(Common Language Specification,CLS),它詳細描述了一個最小的功能集,任何編譯器產生的類型想要相容由其他符合CLS、面向CLR的語言所產生的組件,就必須支援這個最小功能集。由於IL是一種豐富的語言,大多數編譯器的編寫人員有可能把給定編譯器的功能限制為只支援IL和CLS提供的一部分特性。對於CLS,首先是各個編譯器的功能不必強大到支援.NET的所有功能;其次,CLS提供了如下保證:如果限制類只能使用CLS相容特性,就要保證其它相容語言編寫的代碼可以使用這個類。編寫非CLS相容代碼是完全可以接受的,只是在編寫了這種代碼後,就不能保證編譯好的IL代碼完全支援語言的互通性。也就是說,在開發類型和方法的時候,如果希望它們對外“可見”,能夠從符合CLS的任何一種程式設計語言中訪問,就必須遵守由CLS定義的規則。用一種語言定義一個類型時,如果希望在另一個語言中使用該類型,就不要在該類型的 public 和 protected 成員中使用位於CLS外部的任何功能。否則,其它其它語言可能無法正常訪問這個類型的成員。以下代碼定義了一個不符合CLS的類型:
using System;
//檢查CLS相容性
[assembly:CLSCompliant(true)]
namespace CLRTest
{
//因為是public類,所以驗證相容性
public sealed class Test
{
//傳回型別不符合CLS
public UInt32 get()
{
return 0;
}
//僅大小寫不同標識符,不符合CLS
public string Get()
{
return "0";
}
//私人方法可以不符合CLS,不會顯示警告
private UInt32 GET()
{
return 0;
}
}
}
[assembly:CLSCompliant(true)]特性應用於程式集,這個特性告訴編譯器檢查 public 類型是否符合CLS規範。這種方法的優點是使用CLS相容性的限制只適用於公用和受保護的類的成員和公用類。在類的私人實現方式中,可以編寫非CLS代碼,因為其它程式集中的代碼不能訪問這部分代碼。
CLS的基本規則是“一個類型的每個成員要麼是一個欄位(資料),要麼是一個方法(行為)。”編譯器遇到枚舉、數組、屬性、索引器、委託、事件、構造器、析構器、操作符重載、轉換操作符等任何一種構造,必須將其轉換成欄位和方法,使CLR和其它語言能夠訪問這些構造。(註:CLR的完整規則列表,請參考http://msdn.microsoft.com/zh-cn/library/a2c7tshk.aspx。)
3 載入CLR
產生的程式集既可以是一個可執行應用程式,也可以是一個DLL。理論上,任何基於IL的程式集都可以在任何CPU上運行,但是實際上,程式集有可能是不可移植的(64位程式不能運行在32位平台上)。如果IL中沒有與特定CPU架構或機器語言相關的內容,JIT編譯器就會在運行時為目標CPU產生機器指令。如果開發一個需要特定CPU架構的程式集,我們必須把CPU資訊告訴VS,以便它將資訊合并到二進位檔案中。運行一個可執行檔時,Windows會檢查這個EXE檔案的頭,判斷應用程式是32位的還是64位的(64位Windows提供了WoW64技術,運行運行32位程式,但有效能損耗)。Windows還會檢查標頭檔中嵌入的CPU架構資訊,確保當前電腦符合要求。Windows檢查後完EXE檔案的頭,決定建立32位、64位還是WoW64進程之後,會再進程的地址空間中載入mscoree.dll(CLR中最重要的部分,稱為“公用對象運行庫執行引擎”,它包含大量核心類型,它們封裝了各種常見的編程任務與核心資料類型)的x86、x64或IA64版本。然後進程的主線程調用mscoree.dll中定義的一個方法。這個方法初始化CLR,載入EXE程式集,然後調用其入口方法(Main)。隨即,託管的應用程式將啟動並運行。
4 執行程式集的代碼
4.1 程式集執行過程
如前所述,程式集包含中繼資料與IL。為了執行一個方法,首先必須把IL轉換成本地CPU指令。這是CLR的JIT(just-in-time,即時)編譯器(也叫“JITter”)的職責。展示了,方法在被調用時,發生的事情:
在Main方法執行之前,CLR會檢測出Main的代碼引用的所有類型。這導致CLR分配一個內部資料結構,它用於管理對所有引用的類型的訪問。中,Main方法引用了一個Console類,這導致CLR分配一個內部結構。在這個內部結構中,Console類定義的每個方法都有一個對應的記錄項(entry)。每個記錄項都容納了一個地址,根據此地址即可找到方法是實現。對這個結構初始化時,CLR將每個記錄項都設定成(指向)包含在CLR內部的一個未文檔化的函數,即中的JITCompiler(《CLR Via C#》中使用的名稱)。Main方法首次調用WriteLine時,JITCompiler函數會被調用。JITCompiler函數負責將一個方法的IL代碼編譯成本地CPU命令,根據運行環境不同,JIT編譯器會產生相應的x86、x64或IA64指令。然後,JITCompiler函數會在定義(該類型的)程式集的中繼資料中尋找被調用的方法的IL。接著JITCompiler驗證IL代碼,並將IL代碼編譯為本地CPU命令。本地CPU命令被儲存到一個動態分配的記憶體塊中。然後JITCompiler返回CLR為類型建立的內部資料結構,找到與調用方法對應的那一條記錄,修改最初對JITCompiler的引用,讓他現在指向記憶體塊中的代碼(WriteLine(string)的具體實現)。這些代碼執行完畢並返回時,會返回到Main中的代碼,然後繼續執行。
當執行到Console.WriteLine("World");時,Main要再次調用WriteLine(string)。這時,因為之前已經對WriteLine(string)的代碼進行了驗證和編譯,所以會直接執行記憶體塊中的代碼,WriteLine("World")執行完畢後,再次回到Main。
4.2 JITter
JIT編譯器並不是把整個程式一次編譯完,而是只編譯它調用的那部分代碼。代碼編譯過一次後,得到的內部可執行代碼就儲存起來,直到應用程式終止,編譯好的代碼就才會丟失。所以,如果再次運行程式,或者啟動了程式的兩個執行個體(使用兩個不同的進程),JIT編譯器都要再次編譯IL。一個方法只有在首次調用時才會造成一些效能損耗,之後對該方法的所有調用都以本地代碼的形式全速運行。由於編譯過程的最後一部分是在運行時進行的,JIT編譯器能夠確切地知道程式運行在什麼類型的處理器上,可以利用該處理器提供的任何特性或特定的機器代碼指令來最佳化最後的可執行代碼,以提高效能。