維's profileIT : 是工作還是嗜好?PhotosBlogListsMore ![]() | Help |
|
June 29 Borland最後花落誰家似乎仍有變數!今年5月6日根據外電報導英國軟體廠商Micro Focus將收購Borland,雖然我早已離開Borland數年之久,也對後期的Borland實無好感,但從學生時期便建立的感情仍然讓我無法忽略Borland在業界的消息,在5月7日我得知Borland即將被收購的消息時,心中並無太大的心情波動,因為在離開Borland時我早已預測Borland將會瓦解,因為那時Borland的CEO每天只想著股票的價格,把產品搞得亂七八糟,又要求每個人都做銷售,連技術人員都被送去做銷售的培訓,取消了Borland優良傳統的深度產品和技術培訓,那時我被強迫送去參加了數次的銷售培訓,最後我心中只想著Borland還要技術人員做什麼,這樣下去乾脆大家都轉做銷售人員不就好了,我只覺得那時的Borland實在是@!$#@%#$&^$%&。 沒想到最近又有消息指出在Micro Focus出了收購價之後,似乎又有不具名的對象也出價想要收購Borland,看來在Borland的股東沒有正式投票之前,Borland花落誰家似乎仍有變數。 May 19 CodeGear大幅揭露Delphi和C++Builder未來的發展方向!WoW,在這次的Delphi Live 2009的大會中CodeGear可以說是精銳盡出,透露了許多Delphi和C++Builder開發人員關心的產品發展方向,從這個次的大會中也終於看出Embarcadero在併購了CodeGear之後的確是真的投入研發資源讓Delphi和C++Builder放手發展,這對比於Borland在日前被Micro Focus併購,讓Borland這個昔日的金字招牌也蒙塵於競爭激烈的IT界,想必當初的Borland董事會在夜深人靜的時候也會深切懊惱不該斷送當初大好的開發工具江山吧。 在這次的Delphi Live 2009中CodeGear揭露了目前正在開發的專案,下面是這些專案的列表: Project Weaver User Experience Documentation IDE usability Team Productivity Touch IDE – Insight (easy Keyboard access to almost everything) Improvements to DataSnap Firebird Support .NET AOP SCM Support Enhanced RTTI Support Attribute Support Seamless .NET <> Native communication Windows 7 APIs and Direct 2D Full Support of SOAP 1.2 Clients Project X Cross-platform Windows, Mac OS, and Linux Cross-platform component library DataSnap on all platforms Project Chromium, Quality, Quality Quality, Quality, Quality Pascal Code Formatter Documentation of the OTA New Data binding model allowing binding to almost any property on a control More integration with the database tools. Project Commodore 64 Bit native Full compiler, RTL and VCL support for 64 native Multi-Core. Multi-threaded applications. 光從這些專案的列表就非常令人興奮了,因為很快的我們就可以在Delphi和C++Builder中看到這些技術實作出來,如果您不太瞭解其中的一些技術,沒有關係,在下面我將簡單的說明一下,因為既然Delphi Live 2009已經公開揭露了大部份的資訊,我也就可以說明一下今年年初我參加Embarcadero大會聽到的一些東西了。 Project Weaver就是即將推出的Delphi 2010和C++Builder 2010,雖然上面的列表主要是和Delphi相關的,但C++Builder 2010也將有許多新的功能。Project Weaver令人期待的就是支援Windows 7和Touch這個所謂的觸碰技術了,由於Windows 7已經獲得了業界好評因此Weaver將是第一個支援Windows 7開發的原生工具,此外Weaver也將觸碰技術實作在VCL框架之中,這代表Delphi和C++Builder的開發人員將可以非常簡單就能夠開發觸碰式的應用程式了,這實在太cool,更棒的是我聽說Weaver的觸碰技術不局限於Windows 7,似乎XP等不支援觸碰技術的OS也將由VCL框架來支援,只要您擁有觸碰設備即可,WoW,我等不及來玩玩這個功能了。 另外Weaver在編譯器方面將有大幅的強化,CG準備為Delphi和C++Builder提供類似Java和.NET的Reflection技術,這也就是Enhanced RTTI Support,因為Enhanced RTTI Support技術將做為稍後我即將說明的Delphi和C++Builder下一世代的資料存取技術New Data binding model的基石。 另外Weaver的DataSnap技術將有重大的進步,因為CG也即將把DataSnap帶入到Linux和Mac環境,DataSnap也終於開始實現它在跨平台方面的優勢。Weaver的DataSnap將支援更強大的JSON分散式架構,除了提供tcp/ip之外,也支援http/https,REST,資料加密,資料壓縮,非同步呼叫和伺服器回叫用戶端等強大的功能,我對DataSnap特別感到興趣,特別是想到DataSnap能夠提供跨平台的分散式開發時就令人高興。 Project X是跨平台Delphi的專案,我們也可以稱它為DelphiX,它將讓開發人員可以在Windows,Linux和Mac環境下使用Delphi,CG也在考慮開發一個跨平台的元件框架(VCLX? CLX?, 這個目前我也不清楚),而且會把DataSnap移植到Linux和Mac中,仔細看看下面這張由Robert Love提供的照片,CG已經快完成了Delphi在Mac下的編譯器,這張照片顯示CG的人使用Delphi For Mac的編譯器編譯一個簡單的文字程式並且實際執行在Mac OS中。 今年年初時CG便透露Delphi For Mac的編譯器可能在2009年的夏天可以完成,呵呵,想想當初Delphi 1.0即是用Delphi編譯器開發的模型,那麼Delphi For Mac 1.0似乎也即將…. Project Commodore是Delphi 64的專案,Commodore應該會在2011年出現,它即將把Delphi全面帶入64位元的世界,類似當年Delphi 2.0把Delphi從16位元帶入32位元世界的里程碑一樣,Commodore將在Delphi的發展史中再次設定另外一個里程碑,而且Commodore也將提供多核心,多執行緒開發的新技術/新函式庫。 Project Chromium應該是一個Delphi/C++Builder的再進化專案,其中最關鍵的技術是New Data binding model,這個技術是我早已等待多年的技術,因為它將提供類似MS Entity Framework,RoR的ActiveRecord,或是Hibernate等的功能,其實我應該說它是Chuck在離開Borland之前於Borland內部進行的秘密計劃Apollo的實現,早在數年前Chuck便想為Delphi加入這些先進的資料存取技術,看看Borland浪費多少年,又浪費了多少當年這些技術精英的眼光和技術(其實還有許多非常棒的秘密計劃到現在還未見曙光,包括了Chuck的Z語言,Danny的編譯器和除錯器先進技術等以及Anders早在Delphi 2就想做的Delphi Garbage Collector)。 由於我的時間不多,不能再把更多比較詳細的資訊和大家分享,很抱歉,不過各位可以到下面的URL中仔細看看Robert Love提供的照片,您會看到許多的秘密,有機會的話我再解釋它們好了,Have Fun! http://picasaweb.google.com/LovePhotoStore/DelphiLive2009?feat=directlink#5336279721373168578 April 29 對今年的NBA季後賽沒有多大的興趣了!Kevin Garnett是我個人自Michael Jordan之後最喜歡的球員,看Garnett打球也是我最喜歡的休閒活動之一,但今年Garnett深受受傷之苦,到現在季後賽開打也一直沒有回來,因此今年我也沒什麼興致看NBA季後賽了,希望Garnett能夠儘快康復,再打幾年的好球吧。 不過心底卻有一種說不出來的淡淡憂愁 : 『狼王似乎漸老矣』。 April 28 也許我不應該說的!自從不再是CG的正式員工之後,我也必須申請參加CG產品的Beta,獲得批准之後我才能拿到Beta的產品來測試。因此前一陣子我也和一些我的朋友一樣申請參加下一版C++Builder/Delphi的Beta,很幸運的我獲得了許可,也終於在最近拿到了Beta版來玩一玩,以便填補一下Diablo 3出來之前苦苦等待的真空時期。 下一版的C++Builder/Delphi在進行Beta測試早已不是新聞了,因為Delphi的產品經理Nick已經公開的討論了許多次,CG現在也在她的網站上公開歡迎有興趣的人申請參加,申請的URL如下: https://beta.embarcadero.com/callout/default.html?callid={56DB5BD1-6DD9-40EF-8743-7C3E293085E9} 事實上我也是在上述的URL中申請參加的。 那麼這篇文章到底是討論呢? OK,除了我希望把申請參加Beta的資訊和尚不知道的朋友分享之外,主要的原因就是我有許多已經參加Beta的朋友在測試新版Delphi時發現他們無法在XP下連結到MS SQL Server 2005或是2008,一旦新版Delphi在XP下試著連結2005或是2008時就會出現代號為340的錯誤: 我也在大陸的一些網站上看到許多人就開始批評下一版Delphi的DBX驅動程式有大臭蟲,又不穩定,連MS SQL Server都連不上,但我覺得很奇怪的是既然現在是Beta版那當然有臭蟲,否則進行Beta測試是做什麼的呢?不過我想說的是這個340錯誤並不是DBX的臭蟲,而是我的朋友們並不瞭解新的DBX對於MS SQL Server的驅動程式進行了大幅的改變,因此在使用下一版Delphi連結MS SQL Server 2005/2008之前,開發人員需要安裝一個軟體。那麼是什麼軟體呢? 在回答之前,我想順便談談如何進行有效的Beta測試並且以這個例子做為說明,一旦讀者掌握了之後就做個更好的Beta測試人員了。 但是在下一版Delphi中卻改變了使用的用戶端函式庫,DBX4改用了sqlncli10.dll,如下所示: 而sqlncli10.dll是MS SQL Server 2008的原生用戶端程式(Native Client),而這也就是為什麼會出現340錯誤的原因,因為在下一版Delphi的DBX是改用了Native Client來連結MS SQL Server而不再使用oledb,出現340錯誤是因為我們的機器中沒有安裝MS SQL Server 2008的原生用戶端程式的原因。 因此,我到下列的URL中下載MS SQL Server 2008的原生用戶端程式: http://www.microsoft.com/downloads/details.aspx?FamilyId=C6C3E9EF-BA29-4A43-8D69-A2BED18FE73C&displaylang=en 下載Microsoft SQL Server 2008原生用戶端安裝程式 : sqlncli.msi並且安裝它之後,就可以使用下一版Delphi來連結MS SQL Server 2005/2008了,也不會再出現340的錯誤了。 從上面的討論我們可以推論出下一版Delphi的DBX對於MS SQL Serevr有如下的特性:
嗯, 推論了上述3點之後我更喜歡新的DBX了。 April 21 善用C++Builder 2009的Pre-Compiled header精靈也許是我的記憶已經很模糊了,我記得在C++Builder 5時能夠提供了背景編譯的能力,允許BCB的開發人員在IDE中編譯專案時能夠啟動在背景編譯,如此一來BCB的開發人員就可以在等待BCB編譯專案的同時在IDE中執行一些其他的工作,BCB之所以提供這個功能實則是因為C++是一個3-pass的編譯器,因此需要遠多於One-Pass的Delphi編譯器更多的編譯時間。但當時BCB 5的背景編譯有許多的限制,例如開發人員無法異動正在編譯中的專案,也無法異動編譯專案的圖形使用者介面設計等,因此大部份BCB開發人員使用背景編譯編譯BCB專案時,大都是在BCB的IDE中對其他的專案進行同時開發的工作。 另外一個BCB非常重要的功能就是Code Insight,這個功能能夠幫助開發人員大幅減少需要撰寫打字的程式碼,進而增加開發的生產力。但是我知道很多BCB的開發人員關閉了這項功能,因為在早期的BCB版本中這個功能實在太慢了,導致許多BCB的開發人員抱怨為什麼BCB無法像Delphi的Code Insight一樣那麼的快速。其實BCB的Code Insight太過緩慢的問題我個人也是感同身受,因為我記得每次在做BCB的活動時,為了避免在BCB編輯器中撰寫程式碼反應太過緩慢的問題,我也都是關閉了BCB的Code Insight功能。 ![]() 最後一個我要討論的問題就是BCB的Pre-Compiler Header了,雖然BCB很早就提供了Pre-Compiler Header功能以加快編譯速度,但老實說早期BCB提供的Pre-Compiler Header雖然的確能夠幫助開發人員加快編譯速度,但這個Pre-Compiler Header在C++Builder 2009之前已經有數年沒有改善了,因此仍然有很大的進步空間。 OK,看到這裡您可能會想,為什麼同時敘述上述的三個問題呢?是它們有什麼共點嗎?不然這篇文章到目前看起來是令人摸不著頭緒的。OK,現在就讓我們回到本篇文章的正題。 下圖是我在RAD Studio 2009中的一個範例BCB專案,在沒有使用新的Pre-Compiled header精靈之前,在第一次編譯這個C++Builder專案時RAD Studio仍然會為這個專案建立Pre-Compiled header,如此一來當開發人員稍後再次編譯這個專案時,編譯速度就會加快。例如如果我修改下圖中的 this->Button1->Caption = "test"; 為 this->Button1->Caption = "測試"; 接著再次編譯,那麼此時C++Builder 2009需要再次編譯21281行的程式碼,雖然整個編譯速度算是相當快速,但仍然令人奇怪,為什麼只是修改一行的程式碼卻需要編譯21281行。 ![]() 其實仔細觀察原始程式就可以發現問題所在,因為在原始程式中存在如下的程式碼: #include <vcl.h> #pragma hdrstop #include "SecondForm.h" #include "MainForm.h" 舊的BCB Pre-Compiled header精靈只把VCL.H相關的程式碼預先編譯,但再看看原始程式的表頭檔,我們卻發現了下面的程式碼: #include <Classes.hpp> #include <Controls.hpp> #include <StdCtrls.hpp> #include <Forms.hpp> #include <DB.hpp> #include <DBCtrls.hpp> #include <DBGrids.hpp> #include <ExtCtrls.hpp> #include <Grids.hpp> 問題似乎就出現在這裡,因此我們需要一個能夠分析整個專案的Pre-Compiled header精靈幫助我們產生最佳的預先編譯表頭資訊,並且能夠讓開發人員進行客製化的調整。 C++Builder 2009新的Pre-Compiled header精靈就可以幫助我們完成這些工作,讓我們現在就來看看它的功效如何。 首先點選Tools|Pre-Compiled header Wizard…功能表啟動精靈,如下圖所示,如果你是第一次使用這個精靈,那麼請選擇第一個選項為專案進行一次完整的分析: ![]() 在精靈成功分析之後,它會顯示如下的對話盒,詢問你客製化的設定,例如你可以設定要包含那些表頭檔或是排除特定的表頭檔。 ![]() 點選『Next>>』按鈕之後會顯示最後精靈進行的設定,它決定了預先編譯表頭檔中包含的資訊: ![]() 繼續點選『Next>>』按鈕,最後BCB會產生一個新的表頭檔pch1.h,例如在我的範例中最後的pch1.h包含如下的資訊: /* This precompiled header include file was generated on 2009/4/21 下午 03:00:09 by the RAD Studio Precompiled Header Wizard with the following settings: Project: G:\MyBogs\20090421\Demos\pBCBDemo.cbproj AllowUnguarded = 0 ExcludeProjectFiles = -1 IncludePathsOn = -1 IncludePaths = ExcludePaths = IncludeCount = 1 ManageHeader = -1 */ #ifndef pch1_H #define pch1_H #include <vcl.h> #include <tchar.h> #include <DB.hpp> #include <DBClient.hpp> #include <DBGrids.hpp> #include <DBXMsSQL.hpp> #include <Provider.hpp> #include <SqlExpr.hpp> #include <DBXInterbase.hpp> #endif 在精靈產生了pch1.h並且加入到專案之中後,請先進行一次完整的專案Build讓BCB編譯器建立新的預先編譯表頭檔。有了新的預先編譯表頭檔之後如果我再回到前面修改: this->Button1->Caption = "測試"; 為 this->Button1->Caption = "第2次測試"; 接著選擇make專案,那麼現在BCB只會編譯86行的程式碼,比使用舊的預先編譯方式快了好幾倍的速度。 更棒的是,使用新的Pre-Compiled header精靈之後由於它會產生最佳化的表頭資訊,因此此時如果你再使用BCB的Code Insight,你會發現Code Insight快速的不得了,例如在我的機器中此時再使用BCB的Code Insight功能,它的反應速度幾乎和Delphi的Code Insight一樣快了,現在我再也不需要關閉BCB的Code Insight功能了。 BTW,RAD Studio 2009的Update 3不但修改了許多的臭蟲,IDE的速度又加快了不少,例如對於相同的BCB專案,使用Update 3的編譯速度硬是比Update 1和Update 2又快了許多,絕對建議您儘快昇級到Update 3。 OK,解決了預先編譯和Code Insight的問題之後,最後就是BCB的背景編譯了,在C++Builder 2006/2007/2009中背景編譯都被移除了,因為舊的背景編譯限制太多,在BCB 5那時沒有多核CPU的時代,舊的背景編譯架構也無法利用現在最新的多核CPU,不過我知道下一版的BCB也許將提供全新的背景編譯技術,不但速度快,限制又少,甚至可以讓開發人員在背景編譯專案的同時又進行相同專案的持續開發,這對於大型的BCB專案來說實在太棒了。 試著想想,未來在BCB中我們將可結合預先編譯表頭,背景編譯和快速的Code Insight,那麼使用BCB來進行C++專案的開發將會非常的愉快,因為如此一來BCB即可讓C++的開發人員享受類似Delphi,C#和Java等快速開發的環境了。 February 06 2009開年動動腦!在2009的大年初一終於去了花蓮一遊,應該有2, 3年沒有到花蓮了吧,當我走在海岸路之際乾涸許久的心靈仿佛立刻久旱逢甘霖,滋潤了起來,連腦筋似乎都活絡了起來。隔天騎著自行車沿著海岸路一路南行的路程中,封塵已久的思緒也逐漸的脫繭而出,回想這1,2年的變化可真大啊,不但在我個人的小世界中工作劇烈的改變中,世界的經濟狀況的變化更是令人瞠目結舌,對於5年級世代的人來說,在40多歲遇上這些巨大的衝擊應該是相當大的打擊吧。 回想這幾年來,從Borland,到DevCo,CodeGear最後到Embarcadero,我個人認為C++Builder,Delphi和JBuilder的未來似乎已經漸入佳境,為什麼? 因為:
http://dn.codegear.com/article/39174 在這篇文章中Nick說明了Delphi編譯器的歷史以及Delphi編譯器即將發展的方向,在其中Nick說Delphi64位元的編譯器將在2010年中才大概會準備好,因此合理的判斷Delphi 64最早應該會在2010底或是2011年初才可能出現,那麼在2009年C++Builder和Delphi會做什麼呢? 這也得從為什麼原生Delphi 64會從2009年底延遲到2010或2011年分析起。 在Nick的文章中說明了,CodeGear因為決定為C++Builder和Delphi開發一個共同的後端編譯器(Compiler Back End),因此延遲了原生BCB 64和Delphi 64,所謂共同的後端編譯器是指BCB 64和Delphi 64將使用一個共用的最佳化和機械程式碼產生器,如此一來不但日後在維護和發展上比較方便,由於BCB是使用由Object Pascal撰寫的VCL框架,因此當這兩個產品使用相同的後端編譯器之後,可讓BCB和Delphi的相容性更高。而且現在BCB在C/C++程式語言方面開始率先支援CPP0X標準,而Delphi又開始進入增加新的程式語言功能的階段,這個新的後端編譯器可以讓BCB和Delphi同時在程式語言方面大幅增加新的功能,因此非常讓人期待,更重要的是CodeGear可以在這個新的後端編譯器中加入更多最佳化的功能,在編譯器最佳化方面從Borland後期開始就很久沒有著墨了,因此我個人非常期待這個機會。 更有趣的是,在CodeGear中已經有許多人提倡混合編譯和跨平台編譯許多年了,但Borland/DevCo/CodeGear一直都沒有朝混合編譯和跨平台編譯發展,直到現在這個機會。在說明什麼是混合編譯和跨平台編譯之前,讓我們看看現在的BCB和Delphi這兩個產品如何編譯Delphi和C/C++原始程式。 在BCB和Delphi的BIN目錄下,BCC32和DCC32分別是BCB和Delphi的前端編譯器(Front End Compiler),目前BCB和Delphi分別是使用各自的後端編譯器,它們是comp32x.dll和dcc120.dll(我是使用RAD Studio 2009),如果我使用TDump來檢查這兩個dll輸出的函式,我們可以得到如下的結果: 2009開年動動腦! 在2009的大年初一終於去了花蓮一遊,應該有2, 3年沒有到花蓮了吧,當我走在海岸路之際乾涸許久的心靈仿佛立刻久旱逢甘霖,滋潤了起來,連腦筋似乎都活絡了起來。隔天騎著自行車沿著海岸路一路南行的路程中,封塵已久的思緒也逐漸的脫繭而出,回想這1,2年的變化可真大啊,不但在我個人的小世界中工作劇烈的改變中,世界的經濟狀況的變化更是令人瞠目結舌,對於5年級世代的人來說,在40多歲遇上這些巨大的衝擊應該是相當大的打擊吧。 回想這幾年來,從Borland,到DevCo,CodeGear最後到Embarcadero,我個人認為C++Builder,Delphi和JBuilder的未來似乎已經漸入佳境,為什麼? 因為: 第1, 2008年底Borland傳來非常不好的訊息,整個公司巨幅虧損不說,CEO等高階主管相繼離職,看來Borland出售開發工具之後不但沒有轉運,反而愈來愈糟糕,更證實了數年前 Borland決定放棄開發工具走向ALM是嚴重的錯誤,C++Builder,Delphi和JBuilder能夠離開Borland是最好的結果。 第2, 在2009年年初Embarcadero邀請我去舊金山參加全球大會,在Embarcadero的全球大會上我終於聽到了類似早年Borland全球大會的良好慣例,Embarcadero的CEO除了在銷售方面的強調之外,更強調了產品和品質的重要性,整個Embarcadero的全球大會也提供了許多產品和技術方面的訓練,而不像後期的Borland全球大會,Borland CEO只有滿嘴的空談和股票經,根本沒有公司和產品的發展計劃,整個Borland全球大會只有銷售訓練,產品和技術的內容一點都沒有。因此我在Embarcadero的全球大會上看到了C++Builder,Delphi和JBuilder在Embarcadero將有完全不一樣的未來。 第3, 全世界經濟狀況的改變讓許多人瞭解到簡單,好用的東西是最好的,因此許多人從昂貴的Java/.NET世界回到了原生Win32的開發世界中,準備迎接足夠未來數年使用的原生Win64開發平台的到來,這給了C++Builder,Delphi一個絕好的機會,因為原生Win64開發正是C++Builder,Delphi未來的發展道路。 第4, 這正是我想深入一點討論的內容,從許多跡象分析中可以發覺C++Builder,Delphi和JBuilder正在進行巨大的發展中,這些變化將帶來重大的改變,也將會激勵Win32和Win64開發工具的市場,對於C++Builder,Delphi和JBuilder的開發人員來說也將會覺得振奮不已。那麼C++Builder,Delphi和JBuilder會什麼樣的巨大改變呢? 讓我們一一的從一些蛛絲馬跡中分析一下,順便做為2009開年的腦筋體操,其實這些分析正是當我在花蓮海岸路一邊騎自行車一邊思考的結果。 首先我們知道從2年前開始DevCo和CodeGear便一直有公佈C++Builder,Delphi和JBuilder的發展路線圖,雖然後來DevCo和CodeGear的命運坎坷,以致C++Builder,Delphi和JBuilder的發展路線圖不斷的延後和改變,但在C++Builder,Delphi和JBuilder最終進入Embarcadero之後C++Builder,Delphi和JBuilder的發展路線也終於開始穩定下來。在前一陣子Delphi的產品經理Nick Hodges寫了一篇有關未來Delphi編譯器的文章: http://dn.codegear.com/article/39174 在這篇文章中Nick說明了Delphi編譯器的歷史以及Delphi編譯器即將發展的方向,在其中Nick說Delphi64位元的編譯器將在2010年中才大概會準備好,因此合理的判斷Delphi 64最早應該會在2010底或是2011年初才可能出現,那麼在2009年C++Builder和Delphi會做什麼呢? 這也得從為什麼原生Delphi 64會從2009年底延遲到2010或2011年分析起。 在Nick的文章中說明了,CodeGear因為決定為C++Builder和Delphi開發一個共同的後端編譯器(Compiler Back End),因此延遲了原生BCB 64和Delphi 64,所謂共同的後端編譯器是指BCB 64和Delphi 64將使用一個共用的最佳化和機械程式碼產生器,如此一來不但日後在維護和發展上比較方便,由於BCB是使用由Object Pascal撰寫的VCL框架,因此當這兩個產品使用相同的後端編譯器之後,可讓BCB和Delphi的相容性更高。而且現在BCB在C/C++程式語言方面開始率先支援CPP0X標準,而Delphi又開始進入增加新的程式語言功能的階段,這個新的後端編譯器可以讓BCB和Delphi同時在程式語言方面大幅增加新的功能,因此非常讓人期待,更重要的是CodeGear可以在這個新的後端編譯器中加入更多最佳化的功能,在編譯器最佳化方面從Borland後期開始就很久沒有著墨了,因此我個人非常期待這個機會。 更有趣的是,在CodeGear中已經有許多人提倡混合編譯和跨平台編譯許多年了,但Borland/DevCo/CodeGear一直都沒有朝混合編譯和跨平台編譯發展,直到現在這個機會。在說明什麼是混合編譯和跨平台編譯之前,讓我們看看現在的BCB和Delphi這兩個產品如何編譯Delphi和C/C++原始程式。 在BCB和Delphi的BIN目錄下,BCC32和DCC32分別是BCB和Delphi的前端編譯器(Front End Compiler),目前BCB和Delphi分別是使用各自的後端編譯器,它們是comp32x.dll和dcc120.dll(我是使用RAD Studio 2009),如果我使用TDump來檢查這兩個dll輸出的函式,我們可以得到如下的結果: Exports from comp32x.dll 44 exported name(s), 44 export addresse(s). Ordinal base is 1. Sorted by Name: RVA Ord. Hint Name -------- ---- ---- ---- 0016E65C 32 0000 ASMFILENAME 0016E664 34 0001 ASMOPTIONS 0010C008 44 0002 BCCGETINTERFACE 00005A44 25 0003 BREAKCOMPILE 000FFAB8 14 0004 BROWSERFILEINDEXTONAME 000FCE90 1 0005 BROWSERFINDDECLARATION 000FE48C 6 0006 BROWSERFINDSYMBOL 000FD1D0 4 0007 BROWSERGETAUTORESULTTYPE 000FD1C4 3 0008 BROWSERGETAUTOTYPE 000FF9DC 13 0009 BROWSERGETBASETYPE 000FE4B8 7 000A BROWSERGETOBJTYPE 000FD0D0 2 000B BROWSERGETREFERENCES 000FF82C 11 000C BROWSERGETRESULTTYPE 000FE5F8 8 000D BROWSERGETSYMBOLFLAGS 000FDCE0 5 000E BROWSERGETSYMBOLS 000FF070 9 000F BROWSERGETSYMBOLTEXT 000FF0EC 10 0010 BROWSERGETTYPECODE 000FF940 12 0011 BROWSERGETTYPESYMBOL 000054F4 28 0012 COMPILE 00005528 29 0013 COMPILEANDBROWSE 00005568 30 0014 COMPILEANDKIBITZ 000055E0 31 0015 COMPILEWITHOPTIONS 0014110C 36 0016 COMPMESSAGES 0014A0F3 24 0017 CONFIG 0016E7C0 27 0018 CUMLINENO 0010CCEC 22 0019 DbEval32InitProc 00141BC4 40 001A ERRBREAK 001CAD28 26 001B FILENAME 00141BC0 37 001C FIRSTERROR 00141BB8 38 001D FIRSTWARNING 00004B2C 42 001E FLUSHPCH 0011C0AC 43 001F GetCompilerListeners 00100888 16 0020 KIBITZGETARGINDEX 0010087C 15 0021 KIBITZGETKIND 00100D20 21 0022 KIBITZGETOVERLOADS 00100CEC 20 0023 KIBITZGETVALIDSYMBOLS 00100898 17 0024 KIBITZRESULTGETFLAGS 001008D0 19 0025 KIBITZRESULTGETNAME 001008B0 18 0026 KIBITZRESULTSETFLAGS 0016E660 33 0027 OBJFILENAME 00005614 41 0028 TOTALERRORCOUNT 00141BBC 39 0029 WARNINGCOUNT 00149C70 35 002A WARNINGS 0013E0F8 23 002B ___CPPdebugHook Exports from dcc120.dll 105 exported name(s), 112 export addresse(s). Ordinal base is 8. Sorted by Name: RVA Ord. Hint Name -------- ---- ---- ---- 0000ECA0 73 0000 BrowserFindDeclaration 0000F54C 84 0001 BrowserFindSymNamespace 0000CC24 58 0002 BrowserFindSymSource 0000F4FC 83 0003 BrowserFindSymUnit 0000CBA0 57 0004 BrowserFindSymbol 0000F4C8 82 0005 BrowserFindUnit 0000F660 89 0006 BrowserGetAncestor 0000F738 93 0007 BrowserGetArrayIndex 0000F6FC 92 0008 BrowserGetArrayIndexSymFromType 0000F7CC 96 0009 BrowserGetArrayOfSym 0000F7A8 95 000A BrowserGetArrayOfSymFromType 0000F7E8 97 000B BrowserGetArrayOfType 0000F754 94 000C BrowserGetArrayOfTypeFromType 0000F974 102 000D BrowserGetAssemblyLocation 0000F27C 78 000E BrowserGetAutoResultType 0000F21C 77 000F BrowserGetAutoType 0000EC64 72 0010 BrowserGetBaseType 0000E468 64 0011 BrowserGetCallingConvention 0000F620 88 0012 BrowserGetClassHelpers 0000F5A4 85 0013 BrowserGetClassRef 0000FAB0 104 0014 BrowserGetContainsList 0000F804 98 0015 BrowserGetDefaultProperty 0000E3C8 63 0016 BrowserGetDefaultValue 0000F2E0 79 0017 BrowserGetDirectlyUsedUnits 0000F6D8 91 0018 BrowserGetHelpedType 0000F6A0 90 0019 BrowserGetImplementedInterfaces 0000F5F0 87 001A BrowserGetInheritedScope 0000F0D0 75 001B BrowserGetObjType 0000D900 61 001C BrowserGetOverloads 0000F948 101 001D BrowserGetPointedType 0000F010 74 001E BrowserGetReferences 0000EB84 69 001F BrowserGetResultType 0000EBCC 70 0020 BrowserGetResultTypeType 0000F4B0 81 0021 BrowserGetSymbolDocumentation 0000DB5C 62 0022 BrowserGetSymbolFlags 0000FA88 103 0023 BrowserGetSymbolNameOffset 0000F3F4 80 0024 BrowserGetSymbolPath 0000E480 65 0025 BrowserGetSymbolText 0000E4B0 66 0026 BrowserGetSymbolTextBuff 0000F5CC 86 0027 BrowserGetSymbolValue 0000D7D0 60 0028 BrowserGetSymbols 0000F888 100 0029 BrowserGetSymbolsFromUnit 0000EB58 68 002A BrowserGetTypeCode 0000E9C8 67 002B BrowserGetTypeCodeType 0000F200 76 002C BrowserGetTypeFromSymbol 0000EC10 71 002D BrowserGetTypeSymbol 0000F874 99 002E BrowserGetUnitSymbol 0008852C 108 002F BuildPackages 0007A994 107 0030 CallNextUnitFreeHook 0000B4BC 52 0031 ClearAllStates 0000B4CC 53 0032 ClearCompState 00002868 20 0033 ClearPackageCache 00002854 19 0034 ClearUnitCache 000021BC 13 0035 CompilerCompile 000021E0 14 0036 CompilerCompileCmdLine 0000219C 12 0037 CompilerCompileUnit 00001FA8 11 0038 CompilerDone 00002738 18 0039 CompilerFindError 0000237C 15 003A CompilerGetUnit 000024B0 16 003B CompilerGetUnitSymbol 00001F2C 10 003C CompilerInit 00002B2C 26 003D CompilerShouldCompile 00002B70 27 003E CompilerVerifyCodePageOption 0001673C 8 003F DbEval32InitProc 00001754 9 0040 DccInitialize 000084F8 43 0041 DoneEvaluate 000084C0 41 0042 DoneProcess 00006D2C 30 0043 EvalCondition 000068A8 28 0044 Evaluate 00007C64 34 0045 FindLineCode 00007D1C 35 0046 FindLineCodes 00007DF0 36 0047 FindSourceLine 000088A8 44 0048 FreeCompile 00007428 31 0049 GetCallCount 000079CC 33 004A GetCallHeader 00007438 32 004B GetCallPos 000083C4 39 004C GetCodePosRanges 00008154 38 004D GetCodeRanges 0000A60C 48 004E GetFileCount 0000A8AC 51 004F GetFileIndex 0000A61C 49 0050 GetFileName 0000A27C 47 0051 GetNearestSymName 00008968 46 0052 GetPropertyInfo 000080AC 37 0053 GetSrcLines 000084E4 42 0054 InitEvaluate 00008474 40 0055 InitProcess 0000250C 17 0056 KibitzCompiler 000A19B0 112 0057 KibitzGetOverloads 0008C068 109 0058 KibitzGetValidSymbols 00090F5C 111 0059 LoadCompState 000088BC 45 005A LookupProc 0000CDF8 59 005B MakeSureUnitIsLinked 00006CFC 29 005C Modify 0000B588 54 005D NewCompState 000908F0 110 005E SaveCompState 0000B604 56 005F SetCompState 0000B5E4 55 0060 SetCompStateFast 0000A64C 50 0061 SetFileName 0007A974 106 0062 SetUnitFreeHook 00002974 23 0063 UIDiscardUnit 00002924 22 0064 UIGetExplicitUnitImports 000029A0 24 0065 UIGetPackagesUsed 00002878 21 0066 UILoadUnit 00002A08 25 0067 UISetDebugDir 001150FC 105 0068 ___CPPdebugHook 各位看到上面許多的輸出函式都和編譯工作有關的嗎? 從上面的結果我們可以使用下列簡化的圖形來呈現: ![]() 等到新的後端編譯器出來之後,BCB和Delphi的編譯架構會形成如下的狀態: ![]() OK,那麼混合編譯和跨平台編譯是什麼? 動腦想想如果我們再把機械碼產生器抽離出來形成如下的架構: ![]() 你看到了什麼? 現在回到BCB和Delphi本身,現在BCB/Delphi中的dbExpress是跨平台的架構,RTL在擁有了跨平台編譯之後也就可以跨平台了, If begin Delphi程式語言 + VCL + RTL + dbExpress ~= Delphi 而且 C/C++程式語言 + VCL + RTL + dbExpress ~= BCB 的話, end then begin Delphi程式語言 + RTL + dbExpress + 跨平台編譯 = ? C/C++程式語言 + RTL + dbExpress +跨平台編譯 = ? end 剩下的問題就是VCL了,VCL在原生Win32/Win64不是問題,但在Linux或是其他平台怎麼辦? 還記得CLX嗎? 本來Borland說要開放CLX,但現在CodeGear又似乎踩剎車了,為什麼? 我也不知道, 但我想如果我們把CLX帶入上面的公式: Delphi程式語言 + 新的CLX + RTL + dbExpress + 跨平台編譯 = ? C/C++程式語言 + 新的CLX + RTL + dbExpress +跨平台編譯 = ? 真是令人好奇啊,希望我猜的都能成真,那麼C++Builder和Delphi將進化成最佳的”跨平台原生開發工具”, 也別忘記現在由Delphi Prism建立的應用程式已經可以執行在Window, Linux和Mac等 3個平台之上了。 我知道現在BCB 2009/Delphi 2009的Patch 3和Patch 4都在開發之中,從目前的測試版來看CodeGear修正了許多多年的臭蟲,整個速度和穩定性更勝RAD Studio 2009,所以我猜想2009年CodeGear會再推出一個BCB/Delphi 2010的32位元版本,這個版本將會是最穩定,最快速的32位元版本,而且應該會支援Vista和Window 7等和其他許多最新的PC技術,CodeGear在離開Borland之後決心再把C++Builder/Delphi打造成最好的Win32/Win64開發工具看來是非常認真的。 好了,大過年的寫到這裡文章也夠長的了,我也該去打打Wii和Wii Fit了,下次再讓我們繼續的動動腦吧(後註, 這篇文章我在大年初四就寫完了, 只是忘了Post出來, 看來記性愈來愈差了)。 最後祝各位 新年快樂,2009年心想事成。 February 02 興德開始提供CodeGear技術文章如果您還不知道的話,興德資訊開始在興德的網站上提供我寫的一些技術文章的和書提供有需要的朋友下載使用,這個小小的園地希望能夠提供一些中文的CodeGear技術資料給有需要的朋友。
我們希望一開始至少在這個小小的技術網提供技術文章,我以前舊的書籍和技術研討會相關的slides和範例程式,我們更我們希望日後擁有更多的資源可以提供更多的東西,而且能夠繼續的維護下去,當然我們更需要您的支持,謝謝! 這個技術網目前的URL是 :
December 15 『使用Delphi開發分散式JSON應用系統』的範例和PowerPoint檔案開始提供下載忙碌完了上星期四場的『使用Delphi開發分散式JSON應用系統』技術研討會之後,我要再次謝謝所有花時間參與的朋友們,希望您們都有所收穫。現在您可在下面的URL下載研討會的範例和PowerPoint檔案,謝謝。 November 28 平行處理機制,平行Linq和Delphi Prism 在上篇我從一些蛛絲馬跡來猜測Delphi除了朝原生64位元發展之外,也可能會在非同步,平行執行方向進行突破的發展。當這篇文章貼出之後,很快的我的朋友就問我『這樣的』Delphi會在什麼時候出現,說實話我不知道,因為這也是我從一些蛛絲馬跡『猜測』的,所以我當然不知道,只能等時間來告訴我們了,呵呵。 Delphi Prism程式語言中提供了許多非同步,平行執行的機制,除了上次我們看到的async和parallel之外,當我們使用這些非同步,平行執行的機制結合PFX 框架時,也能夠很方便的在使用Linq時也享受平行執行的好處,例如下面是傳統使用Linq的一個典型,簡單的範例: class method ConsoleApp.Linq; var words : sequence of string; begin words := ['嗨', '世界' , 'Linq', 'Parallel Linq' , 'Delphi Prism', 'Delphi 2009', 'BCB 2009', '3rdRail']; var rQuery := from word in words select word; for each s in rQuery do Console.WriteLine(s); Console.ReadLine; end; 當我們使用Delphi Prism編譯並且執行之後,會看到如下的結果: 我們從上圖的執行結果可以看到,它輸出的結果次序和我們在程式碼中定義sequence的次序是一樣。 但是如果我們稍微修改一點程式碼,在words之後呼叫擴展方法AsParallel如下所示: class method ConsoleApp.PLinq; var words : sequence of string; begin words := ['嗨', '世界' , 'Linq', 'Parallel Linq' , 'Delphi Prism', 'Delphi 2009', 'BCB 2009', '3rdRail']; var rQuery := from word in words.AsParallel select word; rQuery.ForAll<string>(word -> Console.WriteLine(word)); Console.ReadLine; end; 那麼我們會看到如下的執行結果: 從上圖中可以發現每次執行平行查詢時執行的結果次序都不同,而且輸出的次序也不是程式碼中定義的次序,這個證明了當使用Delphi Prism的平行機制結合PFX框架時的確是以多個平行執行的核心來處理的。 使用.NET Reflector反組編PLinq方法也可以看到它的確是呼叫PFX框架中提供的平行處理機制: class method ConsoleApp.PLinq; begin ParallelEnumerable.ForAll<String>(ParallelQuery.AsParallel<String>(array of(['嗨', '世界', 'Linq', 'Parallel Linq', 'Delphi Prism', 'Delphi 2009', 'BCB 2009', '3rdRail'])), delegate (word: String) begin Console.WriteLine(word) end; Console.ReadLine end; 和呼叫Linq方法是截然不同的。 class method ConsoleApp.Linq; begin var rQuery: IEnumerable<String> := array of(['嗨', '世界', 'Linq', 'Parallel Linq', 'Delphi Prism', 'Delphi 2009', 'BCB 2009', '3rdRail']); if (rQuery <> nil) then {pseudo} goto Label_0058; rQuery.GetEnumerator; var enumerator: IEnumerator<String> := nil; if (enumerator <> nil) then while enumerator.MoveNext do begin var s: String := enumerator.Current; Console.WriteLine(s) end; Console.ReadLine end; Delphi Prism提供的先進非同步,平行執行機制突然讓原生Delphi 32/64有了新的發展方向。 November 24 使用Delphi開發分散式JSON應用系統 在12月我又受委託將舉行4場有關DataSnap和分散式應用系統開發的技術研討會, 在這次的研討會中我將討論下面相關的內容:
同樣的, 這是免費的技術研討會, 日期是在12/09~12/12, 如果您有興趣的話, 您可以在下面的URL報名參加: http://www.sinter.com.tw/codegear/seminars/delohi200920081120.html November 21 非同步,平行執行技術,從Delphi到Delphi Prism,再到未來的Delphi? 前一陣子Delphi的現任總架構師Allen Bauer討論了許多有關非同步和平行處理的文章,而更早MS也在.NET平台上推出Microsoft Parallel Extensions to Framework 3.5套件,開始進行對於平行開發技術的演進,這個趨勢是想見而知的,因為現今的CPU都是多核心,每一個核心又都支援多執行緒執行的能力,因此軟體技術如何搭配硬體的進步,進而發揮硬體的計算能力是軟體突破的重要方向之一。如果各位嗅覺靈敏的話,從Allen的一系列文章以及他的研究方向來看,各位應該可以開始猜測未來Delphi的發展方向了。 除了這些蛛絲馬跡之外,CodeGear最近又宣佈了Delphi Prism,Delphi Prism除了是一個獨立的產品之外,也將和Delphi/C++Builder 2009整合為RAD Studio 2009,提供在.NET方面的解決方案。不過Delphi Prism並不是Delphi.NET,Delphi Prism是.NET平台上使用類似Delphi語法的程式語言,並且包含了許多先進的程式語言功能。有趣的是Delphi Prism已經在程式語言本身加入了許多非同步,平行執行功能,當結合Microsoft PFX framework時,能夠自動使用PFX中的平行執行技術。 今天我在CodeGear的部落格上看到了Andreano討論有關Delphi Prism的文章,其中就有討論到Delphi Prism程式語言中使用關鍵字async來建立非同步執行的範例,我覺得蠻有意思的,因為這代表現在可以開始討論Delphi Prism了。在Andreano的範例中,他使用了async來宣告一個方法,在Delphi Prism中使用async關鍵字宣告的方法就合格成為一個非同步執行的方法,讓我使用一個範例來說明一下。 在下面的程式碼中宣告了5個類別方法,以及兩個使用async宣告的非同步方法CallLoops和CallParallelLoops: type ConsoleApp = class public class method Main; class method CallAsyncMethod; class method CallAsyncWithParallel; class method Linq; class method PLinq; method CallLoops(iID : Integer) ; async; method CallParallelLoops(iID : Integer); async; end; 接著我在類別方法Main中呼叫其他四個類別方法: class method ConsoleApp.Main; begin ConsoleApp.CallAsyncMethod; ConsoleApp.CallAsyncWithParallel; ConsoleApp.Linq; ConsoleApp.PLinq; end; 在CallAsyncMethod中,它在一個迴圈中呼叫了使用async宣告的CallLoops方法,因此.NET會產生四個執行緒來執行CallLoops: class method ConsoleApp.CallAsyncMethod; begin Console.WriteLine('開始執行非同步方法'); with lConsoleApp := new ConsoleApp() do begin for i : Integer := 0 to 4 do lConsoleApp.CallLoops(i); end; Console.WriteLine('結束執行非同步方法'); Console.ReadLine; end; 而CallLoops方法只是很簡單的執行一個迴圈,列印訊息並且使用Thread在每一次迴圈暫時暫停0.1秒。 method ConsoleApp.CallLoops(iID : Integer) ; begin for i : Integer := 0 to 4 do begin Console.WriteLine('執行緒' + iID.ToString + ' : ' + i.ToString); System.Threading.Thread.Sleep(100); end; Console.WriteLine('執行緒' + iID.ToString + '執行完成'); end; 讓我們看看這個執行的結果,從下圖中讀者可以看到.NET果然以四個執行緒,以不定的次序執行每一個執行緒,所以在Delphi Prism中要開發非同步執行的方法非常的簡單的,只要使用關鍵字async來宣告方法即可。 然而讀者如果仔細觀察上圖的執行狀態,會發現雖然四個執行緒以不定的次序執行,但是每一個執行緒在執行CallLoops方法的迴圈時仍然是以順序的方式在執行迴圈,這代表CallLoops雖然是在獨立的執行緒中執行,但仍然沒有使用平行執行技術。但在Delphi Prism中我們只需要使用一個parallel關鍵字就可以立刻使用Microsoft PFX framework提供的平行技術。 例如下面的CallAsyncWithParallel使用了幾乎和CallAsyncMethod一樣的程式碼,只是CallAsyncWithParallel呼叫了非同步方法CallParallelLoops: class method ConsoleApp.CallAsyncWithParallel; begin Console.WriteLine('開始執行非同步, 並行方法'); with lConsoleApp := new ConsoleApp() do begin for i : Integer := 0 to 4 do lConsoleApp.CallParallelLoops(i); end; Console.WriteLine('結束執行非同步, 並行方法'); Console.ReadLine; end; 而CallParallelLoops也幾乎和CallLoops一樣,只是注意CallParallelLoops在呼叫for迴圈時使用了另外一個關鍵字parallel。 method ConsoleApp.CallParallelLoops(iID : Integer) ; begin for parallel i : Integer := 0 to 4 do begin Console.WriteLine('執行緒' + iID.ToString + ' : ' + i.ToString); System.Threading.Thread.Sleep(100); end; Console.WriteLine('執行緒' + iID.ToString + '執行完成'); end; 在Delphi Prism中如果使用關鍵字parallel而且開發人員參考了Microsoft PFX framework,那麼Delphi Prism便會編譯出如下的程式碼:
下圖是CallAsyncWithParallel執行的結果,讀者可以仔細觀察,CallAsyncWithParallel也產生了四個執行緒來執行,但是每一個執行緒在執行CallParallelLoops迴圈時卻不是以順序的方式來執行,而是使用平行的方式執行for迴圈。 從上面的討論中我們可以瞭解Delphi Prsim提供了非常方便的非同步和平行處理的機制,讓開發人員能夠在結合PFX時開發出平行執行架構。 在下次的文章中讓我們再看看Delphi Prism在支援Linq和平行Linq時是多麼的方便。 November 20 DBX框架篇 : 第1章 初探DBX4框架 - 21-2 執行SQL命令 : TDBXCommand類別 在DBX4框架中TDBXCommand類別是使用來執行SQL命令的,在上一小節中我們已經在開發人員必須呼叫TDBXConnection的CreateCommand方法來建立TDBXCommand物件,再藉由TDBXCommand物件來執行SQL命令。 下面的表格列出了TDBXCommand類別中最常使用的方法和特性: 要使用TDBXCommand物件來執行SQL命令,開發人員必須把 SQL命令指定給Text特性,例如 aCommand.Text := ‘Select count(*) from DBXTESTTABLE’; 如果要執行擁有動態參數的SQL命令,那麼動態參數必須使用’?’字元來代表,例如下面的SQL命令藉由在執行時指定“研討會名稱”欄位的實際數值從DBXTESTTABLE資料表中選擇符合條件的結果資料集: aCommand.Text := ‘Select * from DBXTESTTABLE where “研討會名稱” = ? ’; 那麼如何在應用程式執行時實際設定SQL命令中的動態參數值呢? 這就必須使用TDBXParameter類別。 1-2-1 代表SQL敘述中動態參數的類別 : TDBXParameter TDBXParameter類別物件可代表SQL命令中的每一個以’?’字元代表的動態參數,因此SQL命令中擁有多少個以’?’字元代表的動態參數,那麼開發人員就必須為每一個以’?’字元代表的動態參數建立一個相對應的TDBXParameter物件,然後設定TDBXParameter物件中正確的參數特性值,最後再呼叫TDBXCommand物件的Parameters特性的AddParameter方法把每一個TDBXParameter物件加入到TDBXCommand物件,最後才能呼叫TDBXCommand的ExecuteQuery或是ExecuteQuery真正的執行SQL命令。 TDBXParameter類別也擁有許多的方法和特性,下面的表格列出了比較重要的方法和特性: 現在讓我們看看如何使用TDBXParameter來執行動態SQL,假設我們需要執行下面的SQL敘述: select * from SEMINARS where “主講人” = ? 由於在上面的SQL敘述中包含一個以’?’字元代表的動態參數,因此我們需要建立一個TDBXParameter物件,設定它的資料型態和數值再加入到TDBXCommand物件中才能呼叫ExecuteQuery來執行此SQL敘述。下面的程式碼展示了如何執行上面的動態SQL敘述: 001 procedure TForm5.執行SQL命令; 002 var 003 aCommand : TDBXCommand; 004 aParameter : TDBXParameter; 005 aReader: TDBXReader; 006 begin 007 if (Assigned(dbx4Conn)) then 008 begin 009 aCommand := dbx4Conn.CreateCommand; 010 try 011 aCommand.CommandType := TDBXCommandTypes.DbxSQL; 012 aCommand.Text := Self.edtSQL.Text; 013 aParameter := aCommand.CreateParameter; 014 aParameter.DataType := TDBXDataTypes.WideStringType; 015 aParameter.Value.AsString := Self.edt主講人.Text; 016 aCommand.Parameters.AddParameter(aParameter); 017 aReader := aCommand.ExecuteQuery; 018 while (aReader.Next) do 019 begin 020 Self.ListBox1.Items.Add(aReader.Value['名稱'].GetWideString + ' : ' + aReader.Value['主講人'].GetWideString); 021 end; 022 finally 023 FreeAndNil(aReader); 024 FreeAndNil(aCommand); 025 end; 026 end; 012行設定了TDBXCommand要執行的SQL敘述之後,013行建立TDBXParameter物件,014行設定TDBXParameter物件的資料型態,015行設定實際動態參數的數值,016行把TDBXParameter物件加入到TDBXCommand物件,017行執行動態SQL敘述,018行之後藉由TDBXReader物件來處理結果資料集,在下一小節中將說明TDBXReader類別。 下圖是上面程式碼執行動態SQL敘述的結果: 圖1-4使用TDBXCommand和TDBXParameter執行動態SQL敘述 在使用TDBXCommand物件執行命令時,開發人員必須指定TDBXCommand物件執行的命令種類,而這些TDBXCommand物件能夠執行的命令種類是由TDBXCommandTypes類別定義。TDBXCommandTypes類別定義了數個常數值(const)來定義不同的命令種類,開發人員需要使用它來設定TDBXCommand物件的CommandType特性,類似上面的011行程式碼。 下面的表格整理了TDBXCommandTypes定義的常數值以及這些常數值使用的意義: 從上表可知,TDBXCommand不但可以執行SQL命令,執行後端預儲程序,存取資料庫元資料,更可以執行DataSnap 2009中加入的DataSnap伺服器輸出的方法。例如,假設我們有一個DataSnap 2009開發的JSON伺服器,它輸出了一個GetSpeakerCount方法能夠回傳後端Speaker資料表中所有記錄的總數值,那麼我們就可以使用下面的程式碼在用戶端使用TDBXCommand物件來執行遠端JSON伺服器中的GetSpeakerCount方法並且取得執行結果: function TDSServerModule2Client.GetSpeakerCount: Integer; begin if FGetSpeakerCountCommand = nil then begin FGetSpeakerCountCommand := FDBXConnection.CreateCommand; FGetSpeakerCountCommand.CommandType := TDBXCommandTypes.DSServerMethod; FGetSpeakerCountCommand.Text := 'TDSServerModule2.GetSpeakerCount'; FGetSpeakerCountCommand.Prepare; end; FGetSpeakerCountCommand.ExecuteUpdate; Result := FGetSpeakerCountCommand.Parameters[0].Value.GetInt32; end; 我們只需要如上設定TDBXCommand物件的CommandType特性值為TDBXCommandTypes.DSServerMethod命令種類之後就可以執行遠端服務方法了。 在稍後討論如何開發DataSnap 2009分散式應用程式時會再詳細的說明如何在用戶端執行遠端JSON伺服器輸出的方法。 1-2-2 存取結果資料集 : TDBXReader類別 當開發人員使用TDBXCommand執行命令時,TDBXCommand的ExecuteQuery方法會回傳TDBXReader物件,在DBX4框架中TDBXReader通常使用來代表執行命令之後回傳到用戶端的結果物件,如果開發人員執行的結果是資料集,那麼就可以使用TDBXReader來取得結果資料集,如果執行命令的結果是單一數值,那麼也會以結果資料集的形式儲存在TDBXReader物件中,開發人員仍然可以使用TDBXReader物件來存取結果數值。 TDBXReader是一個單向,只能往前存取的資料結構型態,類似TDataSet。當開發人員取得TDBXReader物件並且欲存取其中的執行結果時,一開始先要呼叫它的Next方法以便把cursor指到TDBXReader物件中第一筆的資料位置,那麼在處理完每一筆資料之後再呼叫Next指到下一筆資料,而在每一筆資料位置上,開發人員是藉由存取TDBXReader的Value特性來取得每一個欄位的執行結果,下面的表格整理了TDBXReader類別中最重要的方法和特性的說明: TDBXReader的Value特性是一個複載的特性,開發人員可以使用名稱或是索引值來存取TDBXReader物件中每一筆資料中每一個欄位其中的數值,在TDBXReader中Value有如下的複載的宣告: property Value[const Ordinal: TInt32]: TDBXValue read GetValue; default; property Value[const Name: UnicodeString]: TDBXValue read GetValueByName; default; 例如在前面執行SQL敘述的範例中,我們藉由TDBXReader複載的Value來存取結果資料集中'名稱'欄位的數值如下: aReader.Value['名稱'].GetWideString; 如果'名稱'欄位是SEMINARS資料表中的第二個欄位,那麼我們也可以使用索引值的方式來如下它的數值如下: aReader.Value[1].GetWideString; 因此使用TDBXReader物件通常擁有下面的兩個型式:
aDBXReader. Value[0].Get資料型態
begin aDBXReader. Value[0].Get資料型態; aDBXReader. Value[1].Get資料型態; … end; 或是: while (aDBXReader.Next) begin for iCount := 0 to aDBXReader. ColumnCount – 1 do begin aDBXReader. Value[iCount].Get資料型態; aDBXReader. Value[iCount].Get資料型態; end; … end; 此外,當開發人員實際在使用DBX4開發資料庫或是分散式應用程式時,應該大都是使用DBX4的元件,只有在很少的情形下才會直接使用TDBXReader,不過如果實的遇上必須使用TDBXReader時,那麼TDBXReader可以轉換為TDataSet嗎? 答案是可以的,在DXB4框架中TCustomSQLDataSet類別提供了一個重載建構函式,這個建構函式可以接受TDBXReader參數並且根據它建立TDataSet物件,如此一來就可以完成轉換TDBXReader物件為TDataSet物件的須要。下面即即是此TCustomSQLDataSet重載的建構函式宣告: TCustomSQLDataSet = class(TWideDataSet) … constructor Create(AOwner: TComponent; DBXReader: TDBXReader; AOwnsInstance: Boolean); reintroduce; overload; 因此我們可以使用如下簡單的程式碼就可以轉換TDBXReader物件為TDataSet物件: aDataSet := TCustomSQLDataSet.Create(nil, aCommand.ExecuteQuery, True); 在取得了TDataSet物件之後,它就更容易使用而且能夠和DBX4的元件一起工作了。 1-2-3 TDBXValue類別 在上面的討論中,當我們使用TDBXReader的Value特性存取執行結果數值時,實際上是取得TDBXValue類別物件,每一個TDBXValue物件中包含一個執行結果數值,或是包含一個執行結果資料集欄位的數值。 TDBXValue類別定義了許多GetXXXX方法讓開發人員取得執行結果數值,而XXXX就是不同的執行結果數值的資料型態。例如,如果執行結果是整數值,那麼就呼叫GetInt32方法來取得結果,如果是字串數值,就呼叫GetWideString,開發人員可以藉由判斷TDBXValue的ValueType特性來判斷其中數值的資料型態。 下面的表格整理了TDBXValue類別重要的方法和特性: 因此在一般的應用中,我們經常會使用如下的程式碼樣例來使用TDBXValue: case aDBXValue.ValueType.DataType of TDBXDataTypes.DateType: Result := IntToStr(aDBXValue.GetDate); TDBXDataTypes.WideStringType: Result := aDBXValue.GetWideString; TDBXDataTypes.Int32Type: Result := IntToStr(aDBXValue.GetInt32); TDBXDataTypes.DoubleType: Result := FloatToStr(aDBXValue.GetDouble); else ... end; November 14 DBX框架篇 : 第1章 初探DBX4框架 DBX技術從Delphi 6開始歷經了2次重大的改變,從Delphi 6一直到Delphi 2006是DBX技術的第1階段,在這段DBX的目的是在發展出一個輕量級,可跨平台的通用資料存取框架,以取代BDE/IDAPI成為Delphi/C++Builder的標準資料存取技術。在這個階段中,DBX技術是由VCL框架中的DBX相關類別/介面和DBX驅動程式組成,由於當時的DBX驅動程式是使用C/C++撰寫的,因此VCL中相關的DBX類別/介面也大量的使用了指標和靜態連結的方式來實作,這樣的架構在日後的擴展和維護上比較麻煩。 從Delphi 2006之後,DBX進入第二個階段,這是因為DBX除了提供Win32的存取能力之外,也需要在.NET框架 2.0之後也改變架構的ADO.NET之上加入DBX的技術,以取代BDP.NET技術,同時DBX在此時也決定採取了另外一個重大的變革,那就是將重新使用Object Pascal程式語言來實作所有的DBX驅動程式。由於DBX驅動程式改用了Object Pascal實作,因此在VCL框架中和DBX驅動程式相關的程式碼再也不需要大量使用指標來呼叫其中的函式,這個優勢讓VCL中的DBX框架有了重新使用更好的方式來實作的機會,再加上Object Pascal也能夠執行於.NET平台之後(CodeGear在.NET平台上的Object Pascal稱為Delphi程式語言),DBX框架也終於可以使用相同的設計架構執行於Win32和.NET平台,因此第二階段的DBX改以現代化的類別和介面的架構為設計,實作基礎,讓DBX真正成為完全以物件導向思維設計出來的框架,在讀者隨著本書的章節閱讀之後也將對DBX框架有更完整的瞭解。 現在讓我們先開始討論DBX4框架中的骨架類別,並且從這些最基礎而重要的類別中逐漸展開我們對於DBX4框架的掌握。 任何資料存取技術都由一些固定的樣版類別或是物件來完成,例如首先需要連結到資料來源,需要執行SQL命令,需要存取資料庫中的物件資訊等,所有的資料存取技術都是由這些基本的類別/物件提供的服務再逐漸的提供更多豐富的功能。DBX也不例外,在DBX框架中也是以這些基本而重要的類別為核心架構而成,例如:
1-1 使用DBX4技術連結資料庫 – TDBXConnectionFactory類別 DBX框架中的TDBXConnectionFactory類別嚴格的說是一個抽象父類別,它的類別靜態方法GetConnectionFactory可以回傳一個從它繼承下來的實體類別,這實體類別將實作如何載入DBX驅動程式和資料來源連結設定等重要的資訊。在目前的DBX4框架實作中,這個實體類別是TDBXIniFileConnectionFactory。 TDBXIniFileConnectionFactory會根據dbxdrivers.ini檔案中的驅動程式設定以載入驅動程式,並且根據dbxconnections.ini檔案中設定的資訊來設定連結資訊。 因此在DBX4要和資料來源連結,首先我們可以使用下面的程式碼來取得TDBXIniFileConnectionFactory物件: TDBXConnectionFactory.GetConnectionFactory. 接著在TDBXConnectionFactory類別中的GetConnection方法即可讓我們取得可連結資料來源的TDBXConnection物件。GetConnection有兩個原型宣告,它是複載(override)方法: function GetConnection( const ConnectionName : UnicodeString; const UserName: UnicodeString; const Password: UnicodeString) : TDBXConnection; overload; function GetConnection(ConnectionProperties : TDBXProperties) : TDBXConnection; overload; 第一個GetConnection重載方法接受一個連結名稱,資料來源的登錄使用者名稱和密碼來回傳TDBXConnection物件,第二個GetConnection重載方法則接受一個TDBXProperties參數而回傳TDBXConnection物件。 讓我們先解釋如何使用第一個GetConnection方法,它的第一個參數ConnectionName其實就是讀者在Delphi中使用DataExplorer建立的連結別名,例如下圖所示,假設我們有一個InterBase的連結別名IB_UCONNECTION,那麼我們就可以使用下面的程式碼來呼叫GetConnection以取得連結到InterBase的TDBXConnection物件: 圖1-1 DataExplorer顯示存在dbxconnections.ini中的連結資訊 var dbx4Conn : TDBXConnection; begin … dbx4Conn := TDBXConnectionFactory.GetConnectionFactory.GetConnection( ‘IB_UCONNECTION’, 'sysdba', 'masterkey'); … 或是建立下面的TDBXProperties物件,設定必要的特性值之後再傳遞TDBXProperties物件給GetConnection來取得和資料來源的連結: var dbxProp : TDBXProperties; dbxConn : TDBXConnection; begin dbxProp := TDBXProperties.Create; try dbxProp[TDBXPropertyNames.ConnectionName] := 'IB_UCONNECTION'; dbxProp[TDBXPropertyNames.UserName] := 'sysdba'; dbxProp[TDBXPropertyNames.Password] := 'masterkey'; dbxConn := TDBXConnectionFactory.GetConnectionFactory.GetConnection(dbxProp); Assert(Assigned(dbxConn), TDBXPropertyNames.ConnectionName +'連結失敗'); finally dbxProp.Free; dbxConn.Close; dbxConn.Free; end; 上面程式碼中的TDBXPropertyNames類別中定義了所有資料來源可能需要設定的特性值名稱,例如下面是TDBXPropertyNames中的一些定義: TDBXPropertyNames = class Const ConnectionName = 'ConnectionName'; UserName = 'User_Name'; Password = 'Password'; … end; 上面的程式碼就設定了TDBXProperties物件要連結的資料來源名稱,登錄的使用者名稱和密碼等資訊,GetConnection方法就根據這些資訊取得資料來源的連結並且回傳代表此連結的TDBXConnection物件。 TDBXConnectionFactory類別最重要的功能當然就是回傳TDBXConnection物件,因為有了TDBXConnection物件之後開發人員就可以使用它來執行各種工作,例如執行SQL命令,存取資料來源物件等,稍後本書都會說明。除此之外,TDBXConnectionFactory類別也可以讓開發人員取得所有資料來源的連結資訊,也就是圖1-1中DataExplorer顯示的資訊,以及所有dbExpress驅動程式資訊以及目前執行中的應用程式中註冊的dbExpress驅動程式資訊。 例如TDBXConnectionFactory的GetConnectionItems可以取得圖1-1中的所有資料來源連結: aSL := TStringList.Create; TDBXConnectionFactory.GetConnectionFactory.GetConnectionItems(aSL); 而GetDriverNames可以取得所有在dbxdrivers.ini檔案中的驅動程式名稱: aSL := TStringList.Create; TDBXConnectionFactory.GetConnectionFactory.GetDriverNames(aSL); GetRegisteredDriverNames則可以取得在目前的應用程式中註冊的dbExpress驅動程式,只有在應用程式中真正註冊的驅動程式才能使用來連結相對的資料來源。 aSL := TStringList.Create; TDBXConnectionFactory.GetConnectionFactory.GetRegisteredDriverNames(aSL); 例如下圖是使用上面的三個方法取得資料來源連結資訊,驅動程式以及這個範例應用程式中註冊的驅動程式,從圖中讀者就可以猜到這個範例是使用InterBase做為範例資料庫,因為目前註冊的驅動程式是InterBase,稍後我們會說明什麼是在應用程式中註冊的驅動程式。 圖1-2 TDBXConnectionFactory可以存取資料來源和驅動程式資訊 在離開TDBXConnectionFactory之前,我們必須再討論一下前面我們看到的GetConnectionFactory方法。 GetConnectionFactory的功能是回傳TDBXConnectionFactory物件,但它實際回傳的是使用Singleton設計樣例的TDBXIniFileConnectionFactory物件。 GetConnectionFactory方法首先會試著載入dbxdrivers.ini和dbxconnections.ini這兩個重要的檔案內容,載入的次序是 1. 先從應用程式目前的目錄中搜尋,如果沒有, 2. 再從系統註冊器中的HKEY_CURRENT_USER\Software\CodeGear\BDS\6.0\dbExpress中找尋, 3. 最後再後從系統註冊器中的HKEY_LOCAL_MACHINE\Software\CodeGear\BDS\6.0\dbExpress中找尋, 因此在您的系統註冊器中應該可以看到如下的資訊,記載了dbxdrivers.ini和dbxconnections.ini檔案的所在地: 圖1-3 系統註冊器中記錄了dbxdrivers.ini和dbxconnections.ini所在地 之後,GetConnectionFactory會建立唯一的一個TDBXIniFileConnectionFactory物件,再呼叫由TDBXIniFileConnectionFactory物件複載TDBXConnectionFactory的兩個抽象方法LoadDrivers和LoadConnections來分析dbxdrivers.ini和dbxconnections.ini中的資料來源資訊和驅動程式資訊: procedure LoadDrivers; virtual; procedure LoadConnections; virtual; 並且為每一個資料來源建立一個相對的TDBXProperties物件,而TDBXProperties物件中記錄的資訊就是LoadDrivers和LoadConnections方法從dbxdrivers.ini和dbxconnections.ini中分析出來的資訊。 最後當開發人員呼叫GetConnection要取得TDBXConnection物件時,TDBXConnectionFactory便會根據開發人員欲開啟的資料來源相關的TDBXProperties物件中記錄的資訊來真正的載入dbExpress驅動程式。 1-1-1 TDBXIniFileConnectionFactory類別 TDBXIniFileConnectionFactory是從TDBXConnectionFactory繼承下來的實體類別,當開發人員呼叫TDBXConnectionFactory. GetConnectionFactory方法後實際取得的物件也就是TDBXIniFileConnectionFactory物件,在目前的DBX框架實作中使用了Singleton設計樣例,也就是說整個DBX應用程式中只會建立一個TDBXIniFileConnectionFactory物件。 TDBXIniFileConnectionFactory類別最重要的就是複載TDBXConnectionFactory宣告的兩個抽象LoadDrivers和LoadConnections,這在上節中已經說明過了: TDBXIniFileConnectionFactory = class(TDBXConnectionFactory) … protected procedure LoadDrivers; override; procedure LoadConnections; override; … end; TDBXIniFileConnectionFactory提供了ConnectionsFile特性可以存取資料來源設定檔,也就是dbxconnections.ini的完整檔名,DriversFile特性則可以存取dbxdrivers.ini的完整檔名。例如下面的程式碼可以分別取得目前的DBX應用程式載入和使用的dbxconnections.ini和dbxdrivers.ini檔案。 (TDBXConnectionFactory.GetConnectionFactory as TDBXIniFileConnectionFactory).ConnectionsFile (TDBXConnectionFactory.GetConnectionFactory as TDBXIniFileConnectionFactory).DriversFile 1-1-2 TDBXConnection類別 TDBXConnection類別物件是真正代表連結到資料來源的物件,TDBXConnection類別主要的功能是讓開發人員可以建立執行SQL命令的TDBXCommand物件,啟動資料庫交易,確定資料庫交易或是取消交易,存取MetaData物件以處理資料來源物件以及存取資料來源中特定資訊等功能,可以說TDBXConnection是開發人員實際執行有關資料庫處理工作中最基礎而重要的類別。 TDBXConnection提供了許多的方法和特性讓開發人員能夠進行重要的工作,下面的表格列出了其中最重要的方法/特性以及簡單的說明:
上述的一些方法都擁有複載(override)的原型,例如BeginTransaction方法就提供了下面的兩個原型宣告: function BeginTransaction(Isolation: TDBXIsolation): TDBXTransaction; overload; virtual; function BeginTransaction: TDBXTransaction; overload; virtual; 讓我們看看如何使用TDBXConnection物件讀者就可以瞭解如何使用上面的方法了。假設現在我們希望藉由DBX4執行下面的SQL命令: select count(*) from DBXTESTTABLE 我們需要使用下面的步驟來執行這個SQL命令: 1. 取得TDBXConnection物件 2. 使用TDBXConnection物件建立TDBXCommand物件,稍後的小節會討論TDBXCommand類別 3. 使用TDBXCommand物件執行SQL命令 4. 使用TDBXReader物件擷取執行結果,稍後的小節會討論TDBXReader類別 前面我們已經討論如何解決步驟1的方法,步驟2的TDBXCommand物件可以藉由呼叫TDBXConnection物件的CreateCommand方法來建立,例如下面的程式碼即是如何使用DBX4執行SQL命令的範例,在008行呼叫CreateCommand方法建立TDBXCommand物件。 接著步驟3的工作在010到012行完成,010行先設定TDBXCommand物件的CommandType特性值為執行SQL命令,011行設定TDBXCommand物件要執行的SQL命令,最後012行呼叫TDBXCommand的ExecuteQuery方法執行SQL命令。 001 procedure TForm1.執行SQL命令; 002 var 003 aCommand : TDBXCommand; 004 aReader: TDBXReader; 005 begin 006 if (Assigned(dbx4Conn)) then 007 begin 008 aCommand := dbx4Conn.CreateCommand; 009 try 010 aCommand.CommandType := TDBXCommandTypes.DbxSQL; 011 aCommand.Text := Self.edtSQL.Text; 012 aReader := aCommand.ExecuteQuery; 013 if aReader.Next then 014 begin 015 if aReader.ValueType[0].DataType = TDBXDataTypes.Int64Type then 016 Self.Caption := IntToStr(aReader.Value[0].GetInt64) 017 else 018 Self.Caption := IntToStr(aReader.Value[0].GetInt32); 019 end; 020 finally 021 FreeAndNil(aReader); 022 FreeAndNil(aCommand); 023 end; 024 end; 025 end; TDBXCommand的ExecuteQuery方法在執行完SQL命令之後會回傳TDBXReader物件,TDBXReader物件包含了執行SQL命令的結果資料集,藉由TDBXReader的方法開發人員就可以取得執行結果。由於在這個範例中執行的結果只是一個數值,因此013行先呼叫TDBXReader的Next方法把指標指到第一筆的執行結果,再藉由存取它的Value特性值來取得最後的結果。 讀者需要注意的是,在使用完TDBXCommand和TDBXReader物件之後都需要釋放它們,這在021和022行可以看到。 再讓我們使用另外一個範例來看看如何使用TDBXConnection物件控制資料庫交易。下面的程式碼展示了如何使用DBX4在資料庫中建立資料表,這是由DBX4框架中的MetaData等相關類別提供的服務,在稍後的章節中我們會詳細的說明這些MetaData類別,在這裡讓我們先集中焦點在TDBXConnection類別。 在下面的013行中呼叫了TDBXConnection的BeginTransaction啟動資料庫交易,之後的程式碼就執行在這個資料庫交易環境,當一切正確的執行後033行呼叫TDBXConnection的CommitFreeAndNil確定資料庫交易完成,如果在015~033行中發生的錯誤就會產生例外狀況,此時035行呼叫了TDBXConnection的RollbackFreeAndNil來取消資料庫交易。 001 procedure TForm1.建立測試資料表; 002 var 003 aProvider: TDBXMetaDataProvider; 004 Transaction: TDBXTransaction; 005 MetaDataTable: TDBXMetaDataTable; 006 anIndex : TDBXMetaDataIndex; 007 aColumn : TDBXInt32Column; 008 begin 009 if (Assigned(dbx4Conn)) then 010 begin 011 aProvider := DoGetDBXMetaProvider(dbx4Conn); 012 try 013 Transaction := dbx4Conn.BeginTransaction; 014 try 015 aProvider.DropTable(TESTTABLENAME); 016 MetaDataTable := TDBXMetaDataTable.Create; 017 MetaDataTable.TableName := TESTTABLENAME; 018 aColumn := TDBXInt32Column.Create('序號'); 019 aColumn.Nullable := False; 020 MetaDataTable.AddColumn(aColumn); 021 MetaDataTable.AddColumn(TDBXUnicodeVarCharColumn.Create('研討會名稱', 32)); 022 MetaDataTable.AddColumn(TDBXUnicodeVarCharColumn.Create('主講人', 10)); 023 MetaDataTable.AddColumn(TDBXDateColumn.Create('日期')); 024 aProvider.CreateTable(MetaDataTable); 025 026 anIndex := TDBXMetaDataIndex.Create; 027 anIndex.TableName := MetaDataTable.TableName; 028 anIndex.IndexName := 'pidx_序號'; 029 anIndex.Unique := True; 030 anIndex.AddColumn('序號'); 031 aProvider.CreatePrimaryKey(anIndex); 032 033 dbx4Conn.CommitFreeAndNil(Transaction); 034 except 035 dbx4Conn.RollbackFreeAndNil(Transaction); 036 end; 037 finally 038 anIndex.Free; 039 MetaDataTable.Free; 040 aProvider.Free; 041 dbx4Conn.RollbackIncompleteFreeAndNil(Transaction); 042 end; 043 end; 044 end; 請注意的是在041行又呼叫了TDBXConnection的RollbackIncompleteFreeAndNil方法,為什麼呢?這是因為在上面的except敘述中呼叫了TDBXConnection的RollbackFreeAndNil來取消資料庫交易,但是RollbackFreeAndNil本身也可能發生而又產生例外狀況,這有可能造成一些資源未被釋放,因此才在037行開始的finally程式區塊中呼叫RollbackIncompleteFreeAndNil來釋放任何可能未被釋放的資源,而RollbackIncompleteFreeAndNil本身不會產生例外狀況,它只是試著釋放應該被釋放的資源而已。 因此在使用TDBXConnection物件來啟動資料庫交易時,可以使用如下的設計樣例來撰寫程式碼: aTransaction := TDBXConnection物件.BeginTransaction; try //實際工作程式碼 TDBXConnection物件.CommitFreeAndNil(aTransaction); Except TDBXConnection物件. RollbackFreeAndNil(Transaction); Finally TDBXConnection物件. RollbackIncompleteFreeAndNil(Transaction) End; November 06 第3章 使用DBX4的測試框架類別(2) : 如何客製化自動產生測試資料的流程 面展示了如何使用TDbxDataGenerator自動產生測試資料,雖然DBX4框架提供了簡單的自動產生資料的能力,但在實際的開發中開發人員一定會需要產生有意義的測試資料,那麼開發人員如何能夠讓TDbxDataGenerator使用開發人員的方式來產生測試資料呢? 答案就在TDBXDataGeneratorColumn類別。 當TDbxDataGenerator要產生測試資料時,它會使用DBX4的元資料相關類別來判斷欲產生測試資料的資料表的結構,然後根據資料表的欄位型態來建立TDBXDataGeneratorColumn衍生類別的物件來產生測試資料。也許讓我們來看看TDBXDataGeneratorColumn類別的定義,再以解釋讀者就更容易瞭解這整個流程。 下面是TDBXDataGeneratorColumn的定義,它是一個抽象類別,其中定義了許多的虛擬方法以便讓衍生類別繼承並且加以實作。 TDBXDataGeneratorColumn = class abstract public constructor Create(const InMetaDataColumn: TDBXMetaDataColumn); procedure Open; virtual; destructor Destroy; override; function GetString(const Row: Int64): UnicodeString; virtual; abstract; function GetBoolean(const Row: Int64): Boolean; virtual; function GetInt8(const Row: Int64): Byte; virtual; function GetInt16(const Row: Int64): SmallInt; virtual; function GetInt32(const Row: Int64): Integer; virtual; function GetInt64(const Row: Int64): Int64; virtual; function GetDouble(const Row: Int64): Double; virtual; function GetSingle(const Row: Int64): Single; virtual; function GetBytes(const Row: Int64): TBytes; virtual; function GetDecimal(const Row: Int64): UnicodeString; virtual; function GetYear(const Row: Int64): Integer; virtual; function GetMonth(const Row: Int64): Integer; virtual; function GetDay(const Row: Int64): Integer; virtual; function GetHour(const Row: Int64): Integer; virtual; function GetMinute(const Row: Int64): Integer; virtual; function GetSeconds(const Row: Int64): Integer; virtual; function GetMilliseconds(const Row: Int64): Integer; virtual; procedure SetValue(const Row: Int64; const Value: TDBXWritableValue); virtual; abstract; protected procedure SetDataGenerator(const DataGenerator: TDBXCustomDataGenerator); virtual; function GetColumnName: UnicodeString; virtual; function GetDataType: Integer; virtual; function GetMetaDataColumn: TDBXMetaDataColumn; virtual; private function TypeNotSupported: TDBXDataGeneratorException; protected FMetaDataColumn: TDBXMetaDataColumn; FDataGenerator: TDBXCustomDataGenerator; public property DataGenerator: TDBXCustomDataGenerator write SetDataGenerator; property ColumnName: UnicodeString read GetColumnName; property DataType: Integer read GetDataType; property MetaDataColumn: TDBXMetaDataColumn read GetMetaDataColumn; end; 例如假設欲產生測試資料的資料表的第一個欄位型態是整數序列號,那麼TDbxDataGenerator便會建立TDBXInt32SequenceGenerator的物件來產生測試資料,由於TDBXInt32SequenceGenerator是提供產生整數序列號的測試資料,因此它只複載了TDBXDataGeneratorColumn的GetInt32虛擬方法以提供整數序列號數值,複載GetString虛擬方法以字串的型態提供整數序列號數值, TDBXInt32SequenceGenerator並不需要複載所有TDBXDataGeneratorColumn的虛擬方法。 TDBXInt32SequenceGenerator = class(TDBXDataGeneratorColumn) public constructor Create(const MetaData: TDBXMetaDataColumn); procedure Open; override; function GetInt32(const Row: Int64): Integer; override; function GetString(const Row: Int64): UnicodeString; override; procedure SetValue(const Row: Int64; const Value: TDBXWritableValue); override; end; 而例如假設欲產生測試資料的資料表的第一個欄位型態是字串型態,那麼TDbxDataGenerator便會建立TDBXWideStringSequenceGenerator的物件來產生測試資料,TDBXWideStringSequenceGenerator的定義如下,它的使用方式和前面介紹的TDBXInt32SequenceGenerator是一樣的,只是它是負責提供產生字串型態的測試資料: TDBXWideStringSequenceGenerator = class(TDBXDataGeneratorColumn) public constructor Create(const MetaData: TDBXMetaDataColumn); procedure Open; override; function GetString(const Row: Int64): UnicodeString; override; procedure SetValue(const Row: Int64; const Value: TDBXWritableValue); override; end; 在DBX4框架中類似TDBXInt32SequenceGenerator類別以提供各種不同型態的測試資料類別超過10個以上,而且開發人員還可以自己定義/實作客製化類別,稍後我們就會討論。下面的表格說明了這些類別:
讓我們看看TDBXWideStringSequenceGenerator如何產生測試資料,下面是TDBXWideStringSequenceGenerator的GetString方法,它會在TDbxDataGenerator要為字串型態的欄位產生測試資料時呼叫: function TDBXWideStringSequenceGenerator.GetString(const Row: Int64): UnicodeString; var Value: UnicodeString; begin Value := 'W' + IntToStr(Row); if FMetaDataColumn.FixedLength then while Length(Value) < FMetaDataColumn.Precision do Value := Value + ' '; Result := Value; end; 從上面的實作程式碼可以知道,TDBXWideStringSequenceGenerator.GetString在產生測試資料時是以’W’字元為起頭並且加入正在產生測試資料的列行數的數值,例如為第一筆資料產生的是W1,第2筆就是W2等以此類推,這也解釋了為什麼圖3-2中『研討會名稱』和『主講人』欄位的測試資料就是這樣的內容。 瞭解了上面討論的原理之後,那麼我們如何讓TDbxDataGenerator產生我們需要的測試資料而不是內定的簡單而無意義的測試資料呢? 答案很簡單,我們只需要進行下面的兩項工作:
首先讓我們看看如何完成上面的第二個步驟,這個工作很簡單,我們只需要建立客製化TDBXDataGeneratorColumn衍生類別物件,並且加入到TDbxDataGenerator物件之中即可。 例如在下面的程式碼中,我們首先在047行中藉由元資料類別取得測試資料表的每一個欄位物件,然後在049到067行中一一的判斷每一個欄位物件的資料型態,接著就建立相對應的TDBXDataGeneratorColumn客製化衍生類別物件,再呼叫TDbxDataGenerator的AddColumn方法把它加入到TDbxDataGenerator物件之中。 001 procedure TForm5.加入欄位物件(aProvider : TDBXMetaDataProvider; DataGenerator: TDbxDataGenerator); 002 003 function 建立整數欄位(cName : string) : TDBXInt32SequenceGenerator; 004 var 005 col : TDBXInt32Column; 006 begin 007 col := TDBXInt32Column.Create(cName); 008 try 009 Result := TDBXInt32SequenceGenerator.Create(col); 010 finally 011 FreeAndNil(col); 012 end; 013 end; 014 015 function 建立字串欄位(cName : string; iLength : Integer) : TDBXWideStringSequenceGenerator; 016 var 017 col : TDBXUnicodeVarCharColumn; 018 begin 019 col := TDBXUnicodeVarCharColumn.Create(cName, iLength); 020 try 021 if (col.ColumnName = '主講人') then 022 Result := T主講人產生器.Create(col) 023 else 024 if (col.ColumnName = '研討會名稱') then 025 Result := T研討會產生器.Create(col) 026 else 027 Result := TDBXWideStringSequenceGenerator.Create(col); 028 finally 029 FreeAndNil(col); 030 end; 031 end; 032 033 function 建立日期欄位(cName : string) : TDBXDateSequenceGenerator; 034 var 035 col : TDBXDateColumn; 036 begin 037 col := TDBXDateColumn.Create(cName); 038 try 039 Result :=TDBXDateSequenceGenerator.Create(col); 040 finally 041 FreeAndNil(col); 042 end; 043 end; 044 var 045 cols : TDBXColumnsTableStorage; 046 begin 047 cols := 取得欄位物件(aProvider, TESTTABLENAME); 048 049 while (cols.InBounds) do 050 begin 051 case cols.DbxDataType of 052 TDBXDataTypes.UnknownType, TDBXDataTypes.Int32Type : 053 begin 054 DataGenerator.AddColumn(建立整數欄位(cols.ColumnName)); 055 end; 056 TDBXDataTypes.WideStringType : 057 begin 058 DataGenerator.AddColumn(建立字串欄位(cols.ColumnName, cols.Precision)); 059 end; 060 TDBXDataTypes.DateType : 061 begin 062 DataGenerator.AddColumn(建立日期欄位(cols.ColumnName)); 063 end; 064 end; 065 cols.Next; 066 end; 067 end; 那麼步驟1如何完成? 也很簡單,我們只需要定義從TDBXDataGeneratorColumn或是它的衍生類別繼承下來的客製化類別,再於其中撰寫如何產生測試資料的程式碼即可。 例如下面就是上面程式碼使用的『T研討會產生器』類別,這個類別負責在測試資料表的『研討會名稱』欄位中產生測試資料: 001 unit u研討會產生器; 002 003 interface 004 uses 005 DBXCommon, 006 DBXCommonTable, 007 DBXMetaDataProvider, 008 SysUtils, 009 DBXCustomDataGenerator; 010 011 type 012 T研討會產生器 = class(TDBXWideStringSequenceGenerator) 013 public 014 constructor Create(const MetaData: TDBXMetaDataColumn); 015 procedure Open; override; 016 function GetString(const Row: Int64): UnicodeString; override; 017 procedure SetValue(const Row: Int64; const Value: TDBXWritableValue); override; 018 private 019 dataArray : array[0..5] of UnicodeString; 020 iPos : Integer; 021 procedure InitailizeData; 022 end; 023 024 implementation 025 026 { TDBXMyWideStringSequenceGenerator } 027 028 constructor T研討會產生器.Create( 029 const MetaData: TDBXMetaDataColumn); 030 begin 031 inherited Create(MetaData); 032 InitailizeData; 033 end; 034 035 function T研討會產生器.GetString( 036 const Row: Int64): UnicodeString; 037 begin 038 Result := dataArray[iPos mod 6]; 039 Inc(iPos); 040 end; 041 042 procedure T研討會產生器.InitailizeData; 043 begin 044 dataArray[0] := 'Delphi 2009產品技術發表會'; 045 dataArray[1] := 'C++Builder 2009產品技術發表會'; 046 dataArray[2] := 'Delphi 2009 Unicode程式設計'; 047 dataArray[3] := 'Delphi 2009 DataSnap程式設計'; 048 dataArray[4] := 'C++Builder 2009 Unicode程式設計'; 049 dataArray[5] := 'C++Builder 2009 DataSnap程式設計'; 050 iPos := 0; 051 end; 052 053 procedure T研討會產生器.Open; 054 begin 055 inherited open; 056 end; 057 058 procedure T研討會產生器.SetValue(const Row: Int64; 059 const Value: TDBXWritableValue); 060 begin 061 Value.SetWideString(GetString(Row)); 062 end; 063 end. 正是由於我們提供了客製化的『T研討會產生器』類別,因此TDbxDataGenerator才在圖3-3中產生了客製化的測試資料,如何?整個客製化的流程很簡單吧。 Ok,希望現在各位就瞭解如何使用和整合DBX4框架來自動測試任何的測試資料了。 October 26 第3章 使用DBX4的測試框架類別(1)DBX框架中除了處理資料庫的類別之外,其實還包含了許多有用的輔助類別,這些輔助類別除了可以幫助開發人員撰寫資料處理功能之外,也可以幫助開發人員建立測試資料庫,測試資料表以及自動建立測試資料等一般在開發資料庫應用程式經常需要進行的工作。DBX框架甚至擴展了DUnit的測試驅動類別讓開發人員可以建立資料庫測試案例以測試應用程式以及資料是否正確,如此一來可以讓開發人員結合測試驅動開發(TDD)以及資料庫開發的工作。 本章的討論重點就是為各位介紹這些非常有用的相關類別,在解釋了這些類別之後本章將使用一些範例來展示如何使用這些有用的類別,現在讓我們從最基礎的TDBXCustomDataGenerator類別開始說起。 3-1 可客製化的自動建立測試資料類別 - TDBXCustomDataGenerator TDBXCustomDataGenerator類別從它的名稱上我們就可以猜到這個類別提供的功能是能夠進行客製化工作的資料產生類別,沒錯,TDBXCustomDataGenerator提供的服務就是和上一章討論的TDBXMetaDataProvider類別合作,一起在開發人員指定的資料表中自動建立測試資料並且進行各種測試的工作。 下面的表格整理了TDBXCustomDataGenerator提供的公用(public)方法和特性的說明,開發人員可以藉由呼叫這些方法或是存取這些特性來自動建立測試資料: 在上一章中我們詳細的說明了TDBXMetaDataProvider類別的功能,由於TDBXMetaDataProvider類別可提供處理資料庫物件的功能,因此TDBXCustomDataGenerator類別可以藉由TDBXMetaDataProvider提供的服務來自動建立測試資料庫,測試資料表和測試資料表中的欄位,之後再根據這些資訊來建立正確的Insert SQL敘述在測試資料表中自動建立測試資料。 此外,當TDBXCustomDataGenerator自動產生測試資料時,是使用TDBXDataGeneratorColumn類別來產生測試資料表欄位中的數值,而TDBXDataGeneratorColumn是一個抽象類別,它的實體類別由其他10多個繼承類別來實作以提供特定的資料型態欄位的數值,開發人員可以直接使用這些實體類別來自動產生欄位資料,也可以再從這些實體類別繼承下來實作客製化的TDBXDataGeneratorColumn類別來產生符合應用程式需要的測試資料,在稍後的小節中我們將會說明TDBXDataGeneratorColumn和它相關的實體類別。 雖然開發人員可以直接使用TDBXCustomDataGenerator類別,但是DBX框架提供了一個TDBXCustomDataGenerator的繼承類別: TDBXDataGenerator,開發人員應該盡量使用TDBXDataGenerator而不是直接使用TDBXCustomDataGenerator類別,因為TDbxDataGenerator又額外提供許多驗證資料的服務,因此接下來讓我們說明TDbxDataGenerator類別的功能。 3-2 DBX4自動建立測試資料類別 –TDbxDataGenerator TDBXDataGenerator是TDBXCustomDataGenerator的子類別,它在TDBXCustomDataGenerator提供的服務之外主要是加上驗證資料的服務。TDBXDataGenerator藉由提供許多靜態類別的方法來提供驗證資料的服務,這些方法都是以Validate為開頭的方法,例如ValidateAnsiString,ValidateWideString等,下面即是TDBXDataGenerator的類別宣告: TDbxDataGenerator = class(TDBXCustomDataGenerator) private FParam: TParam; //Remove once all drivers support Fractions (milliseconds) for timestamp (255605, 255767, 255768, 255769) function GetIsFractionsSupported: Boolean; procedure SetParameter(Ordinal: Integer; Row: Integer; Param: TParam); procedure ValidateParam(Ordinal: Integer; Row: Integer; Param: TParam); public constructor Create; destructor Destroy; override; class procedure ValidateAnsiString(Value1, Value2: String); static; class procedure ValidateWideString(Value1, Value2: String); static; class procedure ValidateBoolean(Value1, Value2: Boolean); static; class procedure ValidateInt64(Value1, Value2: Int64); static; class procedure ValidateDouble(Value1, Value2: Double); static; class procedure ValidateBcd(Value1, Value2: TBcd); static; class procedure ValidateDate(Value1, Value2: TDBXDate); static; class procedure ValidateTime(Value1, Value2: TDBXTime); static; class procedure ValidateTimestamp(Value1, Value2: TSQLTimeStamp); static; class procedure ValidateBytes(Value1, Value2: TBytes); static; class function BytesEquals(const Value1: TBytes; const Value2: TBytes; Count: Integer): Boolean; static; class procedure ReadBytesFromStream(Value: TDBXValue; var Buffer: TBytes); function ValidateRow(const Reader: TDBXReader; Row: Integer): Boolean; overload; function ValidateRow(const DataSet: TDataSet; Row: Integer): Boolean; overload; procedure ValidateParameters(const Command: TDBXCommand; Row: Integer); procedure ValidateParams(const Params: TParams); procedure AddParameters(const Command: TDBXCommand); overload; procedure AddParameters(const SQLQuery: TSQLQuery); overload; {$IF DEFINED(CLR)} procedure AddParameters(const Command: DbCommand); overload; {$IFEND} procedure SetInsertParameters(const Command: TDBXCommand; Row: Integer); overload; procedure SetInsertParameters(const SQLQuery: TSQLQuery; Row: Integer); overload; {$IF DEFINED(CLR)} procedure SetInsertParameters(const Command: DbCommand; Row: Integer); overload; {$IFEND} procedure PopulateParams(Params: TParams); procedure PopulateDataset(DataSet: TDataSet; RowCount: Integer); //Remove once all drivers support Fractions (milliseconds) for timestamp (255605, 255767, 255768, 255769) property IsFractionsSupported: Boolean read GetIsFractionsSupported; end; 當開發人員使用TDBXCustomDataGenerator建立了測試資料之後,就可以再藉由TDbxDataGenerator的服務來測試資料,因此開發人員直接標準專家TDbxDataGenerator類別就可以同時進行這兩個工作。 從上面的各個ValidateXXXX方法我們可以猜到,開發人員能夠藉由呼叫這些不同的ValidateXXXX方法來測試各種不同的資料型態的欄位數值,至於TDbxDataGenerator類別其他的一些方法則是使用來在其他物件中根據TDbxDataGenerator代表的資料表的欄位在其他物件中建立相對應的物件,例如下面的AddParameters方法: procedure AddParameters(const Command: TDBXCommand); overload; 它的功能就根據TDbxDataGenerator代表的資料表欄位在它的參數TDBXCommand物件中建立TDBXParameter物件。 至於SetInsertParameters方法: procedure SetInsertParameters(const Command: TDBXCommand; Row: Integer); overload; 則是根據TDbxDataGenerator代表的資料表欄位數值設定它的參數TDBXCommand物件中TDBXParameter物件的數值。 說明了TDBXCustomDataGenerator和TDbxDataGenerator這兩個類別之後,讓我們來看看如何使用它們。 3-3 使用TDbxDataGenerator類別 現在讓我們在一個資料表中來自動產生測試資料並且使用TDbxDataGenerator提供的驗證方法來驗證資料,從這個範例中我們將可以瞭解如何使用TDBXCustomDataGenerator和TDbxDataGenerator類別。 3-3-1 使用TDBXCustomDataGenerator自動產生測試資料 在第2章中我們使用了DBX的Meta類別展示了如何建立資料表DBXTESTTABLE,因此讓我們繼續使用這個範例資料表來展示如何在其中動建立測試資料。DBXTESTTABLE是一個擁有如下結構的資料表: 圖3-1 測試資料表DBXTESTTABLE的結構 要在其中產生測試資料,我們需要建立TDbxDataGenerator物件,但首先我們需要先取得DBX連結物件,再取得DBX元物件之後才能使用TDbxDataGenerator物件。因此首先呼叫『取得資料來源』連結使用第一章介紹的TDBXConnectionFactory類別來連結DBX資料來源: procedure TForm5.btn建立測試資料重構的版本Click(Sender: TObject); begin 取得資料來源連結; end; 取得了連結資料來源的TDBXConnection物件之後,就可以呼叫『建立測試資料』方法使用TDbxDataGenerator來自動建立測試資料了: procedure TForm5.btn建立測試資料重構的版本Click(Sender: TObject); begin 取得資料來源連結; 建立測試資料; end; 『建立測試資料』方法的工作就是使用TDbxDataGenerator自動建立測試資料,因此它需要進行的工作如下: 1. 藉由TDBXMetaDataProvider取得要建立測試資料的資料表的結構 2. 根據步驟1的結果建立正確的TDbxDataGenerator物件 3. 呼叫TDbxDataGenerator物件的方法建立資料 下面的程式碼中顯示了如何完成上述的兩個步驟,005行的dbx4Conn即是前面呼叫『取得資料來源』方法而取得的TDBXConnection物件,007行的『取得DBXMetaProvider』即可取得TDbxDataGenerator物件,接著009行呼叫『資料表存在嗎』方法來判斷資料庫中是否存在要建立測試資料的資料表TESTTABLENAME。 001 procedure TForm5.建立測試資料; 002 var 003 aProvider: TDBXMetaDataProvider; 004 begin 005 if (Assigned(dbx4Conn)) then 006 begin 007 aProvider := 取得DBXMetaProvider(dbx4Conn); 008 try 009 if (資料表存在嗎(aProvider, TESTTABLENAME)) then 010 begin 011 end; 012 finally 013 aProvider.Free; 014 end; 015 end; 016 end; 現在有了TDBXConnection和TDBXMetaDataProvider這兩個必要的物件之後下面的013行建立TDbxDataGenerator物件,014行設定它的目標資料表名稱,015行設定它連結的TDBXMetaDataProvider物件,接著016行呼叫加入『欄位物件』方法在TDbxDataGenerator物件中建立測試資料相對應的欄位物件: 001 procedure TForm5.建立測試資料; 002 var 003 aProvider: TDBXMetaDataProvider; 004 DataGenerator: TDbxDataGenerator; 005 begin 006 if (Assigned(dbx4Conn)) then 007 begin 008 aProvider := 取得DBXMetaProvider(dbx4Conn); 009 try 010 if (資料表存在嗎(aProvider, TESTTABLENAME)) then 011 begin 012 try 013 DataGenerator := TDBXDataGenerator.Create; 014 DataGenerator.TableName := TESTTABLENAME; 015 DataGenerator.MetaDataProvider := aProvider; 016 加入欄位物件(aProvider, DataGenerator); 017 finally 018 Reader.Free; 019 DataGenerator.Free; 020 end; 021 end; 022 finally 023 aProvider.Free; 024 end; 025 end; end; 為什麼需要執行上面『欄位物件』方法呢? 這是因為TDbxDataGenerator在資料表中自動建立測試資料時,它需要知道測試資料表每一個欄位的型態以便呼叫稍後會介紹的TDBXDataGeneratorColumn的衍生類別物件來建立測試資料,例如假設測試資料表中的第一個欄位是整數型態,那麼TDbxDataGenerator便會呼叫我們在TDbxDataGenerator物件中加入的TDBXDataGeneratorColumn的衍生類別物件來自動產生整數值。也許在我們解釋了『欄位物件』方法是如何工作之後讀者會更瞭解這個工作原理。 下面即是『欄位物件』方法的實作程式碼,為了節省篇幅因此我只列出了測試資料表欄位使用的資料型態,由於測試資料表的欄位型態只使用了整數,UnicodeString和日期型態,因此在045~057的case敘述中我判斷這些資料型態,在實際的程式碼中是應該判斷所有DBX支援的欄位資料型態的。OK,現在就讓我們說明『欄位物件』方法是如何工作的。 在041行中呼叫『取得欄位物件』方法藉由TDBXMetaDataProvider物件來取得測試資料表中所有的欄位物件,接著進入while迴圈一一的判斷每一個欄位物件的資料型態來建立相對應的TDBXDataGeneratorColumn的衍生類別物件來產生欄位值,例如如果測試資料表的欄位型態是整數型態,那麼就在007行先建立TDBXInt32Column物件,再於009行建立TDBXInt32SequenceGenerator物件並且加入到TDbxDataGenerator物件中,因此稍後當我們使用TDbxDataGenerator物件來產生測試資料時,TDbxDataGenerator物件中就會在測試資料表的整數型態欄位中使用TDBXInt32SequenceGenerator物件來自動產生測試欄位值。相同的,015行的『建立字串欄位』函式是使用TDBXWideStringSequenceGenerator來建立UnicodeString欄位的測試值,027行的『建立日期欄位』函式是使用TDBXDateSequenceGenerator物件在日期型態的欄位中建立測試值。 001 procedure TForm5.加入欄位物件(aProvider : TDBXMetaDataProvider; DataGenerator: TDbxDataGenerator); 002 003 function 建立整數欄位(cName : string) : TDBXInt32SequenceGenerator; 004 var 005 col : TDBXInt32Column; 006 begin 007 col := TDBXInt32Column.Create(cName); 008 try 009 Result := TDBXInt32SequenceGenerator.Create(col); 010 finally 011 FreeAndNil(col); 012 end; 013 end; 014 015 function 建立字串欄位(cName : string; iLength : Integer) : TDBXWideStringSequenceGenerator; 016 var 017 col : TDBXUnicodeVarCharColumn; 018 begin 019 col := TDBXUnicodeVarCharColumn.Create(cName, iLength); 020 try 021 Result := TDBXWideStringSequenceGenerator.Create(col); 022 finally 023 FreeAndNil(col); 024 end; 025 end; 026 027 function 建立日期欄位(cName : string) : TDBXDateSequenceGenerator; 028 var 029 col : TDBXDateColumn; 030 begin 031 col := TDBXDateColumn.Create(cName); 032 try 033 Result :=TDBXDateSequenceGenerator.Create(col); 034 finally 035 FreeAndNil(col); 036 end; 037 end; 038 var 039 cols : TDBXColumnsTableStorage; 040 begin 041 cols := 取得欄位物件(aProvider, TESTTABLENAME); 042 043 while (cols.InBounds) do 044 begin 045 case cols.DbxDataType of 046 TDBXDataTypes.Int32Type : 047 begin 048 DataGenerator.AddColumn(建立整數欄位(cols.ColumnName)); 049 end; 050 TDBXDataTypes.WideStringType : 051 begin 052 DataGenerator.AddColumn(建立字串欄位(cols.ColumnName, cols.Precision)); 053 end; 054 TDBXDataTypes.DateType : 055 begin 056 DataGenerator.AddColumn(建立日期欄位(cols.ColumnName)); 057 end; 058 end; 059 cols.Next; 060 end; end; 現在我們已經建立了正確的TDbxDataGenerator物件之後,接著下來的工作就是完成上面的第3個步驟的工作,首先我們如第一章所述建立TDBXCommand物件來準備執行新增資料的SQL敘述,如下面001行所示。 001 Command := dbx4Conn.CreateCommand; 002 Command.Text := DataGenerator.CreateParameterizedInsertStatement; 003 DataGenerator.AddParameters(Command); 004 for iRow := 1 to RowCount do 005 begin 006 DataGenerator.SetInsertParameters(Command, iRow); 007 Command.ExecuteUpdate; end; 接著在在002行呼叫TDbxDataGenerator的CreateParameterizedInsertStatement方法在TDBXCommand物件中產生新增資料的SQL敘述,例如在這個測試資料表中產生如下的SQL敘述: INSERT INTO DBXTESTTABLE ("序號","研討會名稱","主講人","日期") VALUES (?,?,?,?) 013行呼叫AddParameters方法為TDBXCommand物件中的每一個以『?』代表的動態參數設定正確的資料型態。 最後004的迴圈就是重點了,我們在006行呼叫TDbxDataGenerator的SetInsertParameters方法來實際的設定每一個動態參數的測試值,而測試值則是由前面『加入欄位物件』方法在TDbxDataGenerator物件中加入的欄位物件來真正的產生。 執行上面的程式碼之後,我們可以在測試資料表TESTTABLENAME之中看到如下自動產生的測試資料: 圖3-2 TDbxDataGenerator在測試資料表中自動產生的測試資料 從上面的討論中我們可以瞭解了DBX框架中的TDbxDataGenerator類別提供了自動化的機制讓我們在開發資料庫應用程式時能夠把許多的工作自動化,讓我們在開發資料庫應用程式時有更多的機制幫助我們撰寫出品質更高的程式碼。 當然,我知道讀者現在會問TDbxDataGenerator產生的資料是沒有意義的,能夠使用TDbxDataGenerator產生更實際的測試資料嗎? 這是當然的,DBX框架提供的資料庫測試是提供一個可用的框架讓開發人員使用,開發人員在瞭解了這個機制之後當然可以使用這些相關的類別進行客製化的工作,也可以藉由掌握了這些原理之後來產生實際的測試資料。 例如筆者可以藉由稍後討論的TDBXDataGeneratorColumn衍生類別在測試資料表中自動產生客製化的測試資料,如下所示: 圖3-3 在測試資料表中自動產生客製化的測試資料 藉由TDBXDataGeneratorColumn衍生類別我們可以產生任何實際的測試資料,而且完全自動化,接著再結合稍後介紹的TDBXTestCase類別,開發人員可以在開發資料庫應用程式的同時使用TDD的開發模式。 3-3-2 使用TDbxDataGenerator驗證資料 『待續』 September 24 淺談如何使用Delphi 2009的泛型容器類別(續) 使用TDictionary<T,T>容器類別 Delphi 2009中的容器類別TDictionary<T,T>對於許多日常開發的工作中非常的有用,例如我們經常需要在應用程式中使用一個鍵值來搜尋相關的資料,在這種應用中TDictionary<T,T>容器類別便非常適合。 TDictionary<T,T>容器類別的第一個<T>即代表鍵值,第二個<T>代表這個鍵值相關的數值,由於TDictionary<T,T>是泛型容器類別,所以它的鍵值和數值都可以是任何的型態。現在就讓我們看看如何使用TDictionary<T,T>,我們仍然繼續使用前面討論的範例做為說明。 假設現在我們希望統計一下glProduct中Delphi和BCB的產品個數是多少,那麼我們可以使用TDictionary<T,T>來儲存<產品名稱, 個數>這樣的資訊,因此我們需要建立一個TDictionary<string, Integer>的物件來使用。 TDictionary<T,T>提供了許多不同的原型的建構函式,其中最簡單的建構函式擁有如下的原型宣告: constructor Create(ACapacity: Integer = 0); overload; 這個建構函式接受一個內定大小的參數ACapacity,這個參數是指TDictionary<T,T>物件在建立之後先在其中預先建立多少個元素。為什麼需要這樣?這是因為我們在建立了TDictionary<T,T>物件之後一定會在其中加入元素(不然為什麼需要建立TDictionary<T,T>物件呢?),由於TDictionary<T,T>比較複雜,因此如果我們預先在其中先建立一些元素空間以便稍後使用,這樣的執行效率會比較好,否則稍後當我們在TDictionary<T,T>物件中加入元素時TDictionary<T,T>物件就需要動態增加它的的元素空間大小,這樣會影響執行效率。 下面的程式碼就是實作統計glProduct中Delphi和BCB的產品個數是多少的程式碼,首先我們先建立TDictionary<string, Integer>物件aDic並且預先在其中建立可儲存5個元素大小的空間,接著我們藉由TEnumerator物件一一從glProduct中最出產品名稱,然後呼叫TDictionary<T,T>的ContainsKey方法來查詢這個產品名稱鍵值是否已經存在aDic之中,如果產品名稱鍵值已經存在,那麼就呼叫TDictionary<T,T>的AddOrSetValue方法增加這個產品名稱的個數,否則就在aDic中加入這個<產品名稱, 1>這對鍵值和數值,最後呼叫DisplayProductCount來顯示統計的數值。 procedure TForm17.btn統計產品總數Click(Sender: TObject); var aDic: TDictionary<string, Integer>; aEnum : TEnumerator<TProduct>; begin aDic := TDictionary<string,Integer>.Create(5); try aEnum := glProduct.GetEnumerator; while (aEnum.MoveNext) do begin if (aDic.ContainsKey(aEnum.Current.GetCategory)) then aDic.AddOrSetValue(aEnum.Current.GetCategory, aDic.Items[aEnum.Current.GetCategory] + 1) else aDic.Add(aEnum.Current.GetCategory, 1); end; DisplayProductCount(aDic); finally aDic.Free; end; end; DisplayProductCount接受型態為TDictionary<string, Integer>的參數,然後也是藉由TDictionary<T, T>的Enumerator來一一取出其中的元素來處理,不過TDictionary<T, T>提供了三種Enumerator,分別是TPairEnumerator,TKeyEnumerator和TValueEnumerator。從這三個Enumerator的名稱我們便可以知道,TPairEnumerator可以取出<T,T>這對<鍵值, 數值>的元素,而TKeyEnumerator則可取出<鍵值>元素,最後的TValueEnumerator則是取出<數值>元素。 由於TPairEnumerator,TKeyEnumerator和TValueEnumerator都是宣告在TDictionary<T, T>之中的內嵌類別,因此在宣告這三個Enumerator類別的變數時需要在之前加上TDictionary<T, T>。例如下面的程式碼中aEnum是宣告為TpairEnumerator,但是在這之前我們需要加入TDictionary<T,T>的宣告並且正確的帶入資料型態,因此aEnum的正確型態宣告是TDictionary<string, Integer>.TPairEnumerator。 TPairEnumerator的使用方法和TEnumerator是一樣的,但是TPairEnumerator的Current特性值的型態是: TPair<TKey,TValue> 而TPair只是鍵值和數值成對的記錄型態而已 TPair<TKey,TValue> = record Key: TKey; Value: TValue; end; TPair的目的是方便讓開發人員一次可以藉由Current取出對應的鍵值和數值。 procedure TForm17.DisplayProductCount(aDic: TDictionary<string, Integer>); var aEnum : TDictionary<string, Integer>.TPairEnumerator; begin Self.lb產品資料.Items.Clear; aEnum := aDic.GetEnumerator; while (aEnum.MoveNext) do begin Self.lb產品資料.Items.Add(aEnum.Current.Key + ' : ' + IntToStr(aEnum.Current.Value)); end; end; TCollectionNotifyEvent<T> elphi 2009的泛型容器類別也提供了通知事件,允許開發人員連結此事件以便撰寫程式碼來處理元素在容器類別中異動的情形。在Generics.Collections程式單元中定義了如下的通知事件: TCollectionNotification = (cnAdded, cnRemoved, cnExtracted); TCollectionNotifyEvent<T> = procedure(Sender: TObject; const Item: T; Action: TCollectionNotification) of object; 開發人員可以藉由實作型態為TCollectionNotifyEvent<T>的函式,再連結到容器類別物件即可在元素被加入,移除或是取出容器類別被容 器類別物件呼叫知會。 例如我們可以使用下面的程式碼連結glProduct容器類別物件: procedure TForm17.btn連結通知事件Click(Sender: TObject); begin glProduct.OnNotify := ProductNotifier; end; 而ProductNotifier就是實作TCollectionNotifyEvent<T>原型的方法,在這裡ProductNotifier會在容器類別物件中的元素異動時顯示簡單的訊息: procedure TForm17.ProductNotifier(Sender: TObject; const Item: TProduct; Action: TCollectionNotification); begin case Action of cnAdded: Self.lb通知事件.Items.Add('加入 : ' + Item.GetName); cnRemoved: Self.lb通知事件.Items.Add('移除 : ' + Item.GetName); cnExtracted: Self.lb通知事件.Items.Add('取出 : ' + Item.GetName); end; end; 深入討論匿名方法 前面討論了匿名方法,匿名方法除了可以和泛型一起使用之外,也可以使用在非泛型的應用程式之中。由於匿名方法是在程式碼中定義函式本身,因此匿名方法也需要一個定義的範圍和呼叫模式,否則匿名方法本身要儲存在那裡呢?因此現在讓我們稍微深入討論一下匿名方法的實作機制,讓各位對於匿名方法有更基礎的瞭解。 Delphi 2009是使用介面的方式來實作匿名方法,這樣做有許多的好處,主要的原因是使用介面可以進行型態檢查以及以及在Win32下缺少資源回收機制的環境中可以比較方便進行資源管理,此外Delphi(Object Pascal)又是強型程式語言,因此在編譯時期提供較為嚴格的檢查也和Delphi程式語言相當的契合。 讓我們看一個範例也許會讓讀者更為瞭解匿名方法的實作方式。假設我們有下面的程式碼: type TFuncOfInt = reference to function(x: Integer): Integer; function MakeAdder(addendum: Integer): TFuncOfInt; begin Result := function(x: Integer) begin Result := addendum + x; end; end; procedure Use; var f: TFuncOfInt; begin f := MakeAdder(20); Writeln(f(22)); // prints '42' end; begin Use; end. 在MakeAdder中定義了一個匿名方法,而且又把這個匿名方法當成MakeAdder的回傳參數,而這個匿名方法的原型則是由TFuncOfInt定義的。OK,那麼Delphi 2009是如何實作上面使用匿名方法的程式碼呢? 首先Delphi 2009的編譯器會把TfuncOfInt重新定義為如介面,並且在其中定義一個Invoke方法,類似如下: type TFuncOfInt = interface function Invoke(x: Integer): Integer; end; 接著在MakeAdder中實際定義了匿名方法,因此在MakeAdder方法中必須定義此匿名方法的程式區塊,因為匿名方法可以擁有它自己的參數,變數,堆疊資源等。因此下面是MakeAdder可能實作的程式碼: function MakeAdder(addendum: Integer): TFuncOfInt; type IClosure1 = interface function Invoke(x: Integer): Integer; end; TMakeAdderFrame = class(TInterfacedObject, IClosure1) addendum: Integer; function IClosure1.Invoke = Closure1; function Closure1(x: Integer): Integer; end; function TMakeAdderFrame.Closure1(x: Integer); begin Result := Self.addendum + x; end; 首先MakeAdder會宣告一個IClosure介面,其中定義Invoke方法,接著MakeAdder會宣告一個內嵌類別TMakeAdderFrame,這個類別將實作Closure介面並且把匿名方法拉出去成為內嵌類別的方法,接著把匿名方法的父方法的參數宣告為物件變數,如此一來就可以解決巢狀參數/變數的問題。 最後在MakeAdder方法的實作程式碼中,就建立內嵌類別物件,最後MakeAdder方法要回傳匿名方法時,這裡是稍微複雜的地方。 var frameInstance: TMakeAdderFrame; begin frameInstance := TMakeAdderFrame.Create; frameInstance.addendum := addendum; Result := reinterpret_cast<TFuncOfInt>(interface_cast<IClosure1>(frameInstance)); end; 首先MakeAdder必須把TMakeAdderFrame物件轉變型態為IClosure介面,這可以使用interface_cast來調整vTable的內容指標,接著使用reinterpret_cast強迫轉變型態為TFuncOfInt,也就是MakeAdder回傳的型態,如此一來就大功告成了。 procedure Use; var f: TFuncOfInt; begin f := MakeAdder(20); Writeln(f.Invoke(22)); end; begin Use; end. OK,我這篇淺談如何使用Delphi 2009泛型容器類別的文章也就到此結束了,希望對於想使用Delphi 2009泛型實體類別的讀者有一些基本的幫助。 Have Fun! September 23 淺談如何使用Delphi 2009的泛型容器類別Delphi 2009在Delphi程式語言方面加入了兩個主要的功能,一個是泛型程式設計(Generics Programming),另外一個就是匿名方法(Anonymous Method)。Delphi 2009在Win32加入了泛型程式設計之後,Delphi程式語言便可以同時在Win32,.NET平台下使用泛型程式設計。由於Delphi 2009在Delphi程式語言本身加入了泛型程式設計,因此在Delphi RTL中也加入了一些新的泛型容器類別(Generic Container Class)以方便開發人員使用於日常的開發程式碼中。本文將為讀者簡單的介紹如何使用使用這些新的泛型容器類別,希望能夠幫助讀者快速學習上手。 簡單的說,泛型程式設計允許開發人員撰寫可泛用的運算法則,這些運算法則能夠使用於各種不同的資料型態,因此一旦開發人員開發完成這些泛用的運算法則,其他的開發人員就可以根據需要的處理資料型態帶入泛用的運算法則來完成運算的工作。例如Delphi最早提供的TList容器類別在實作時由於寫死是使用TObject的資料型態(類別型態),因此開發人員只能在TList中使用TObject樣例。但是TList容器提供的運算法則其實是能夠應用於任何型態的,而且開發人員在許多情形中也希望能夠使用TList來暫時處理一些程式碼中的物件,而不只是TObject樣例。 因此所謂泛型的TList就是讓TList提供的運算法則能夠適用於各種不同的資料型態(類別型態),例如讓TList可以處理字串,整數,浮點數或是任何開發人員定義的類別型態。在一般的程式語言中泛型是使用<T>符號來代表的,其中的代表Type或是Template的意思,因此泛型的TList就是: TList + <T> = TList<T> 因此要讓TList處理字串,就使用TList<String>, 因此要讓TList處理TComponent,就使用TList<TComponent>, 以此類推,所以使用泛型程式設計並不困難,把<T>代換成你要使用的資料型態(類別型態)即可。 當然,在要使用泛型容器類別之前也是需要建立它們才能使用,在建立泛型容器類別時,記得也要代入你建立泛型容器類別之後要在其中使用的資料型態(類別型態),例如前面的例子中,要讓TList處理字串就必須如下的建立TList泛型容器類別物件: var ltstring : TList<String>; begin … tlstring := TList<String>.create; 讓TList處理TComponent必須如下的建立TList泛型容器類別物件: var tlcomponent: TList<TComponent>; begin … tlcomponent := TList<Tcomponent>.create; 以此類推。 下面列出了Delphi 2009中提供的泛型容器類別:
上面的泛型容器類別使用方法和前面介紹的TList差不多,只是不同的泛型容器類別提供了對於其中包含的元素不同的運算法則。 其中需要稍為解釋的是為什麼每一個泛型容器類別都有一個對應的Object泛型容器類別?例如TList<T>既然已經能夠處理任何的型態,包含了類別型態,那麼為什麼還需要TObjectList<T>呢? 沒錯TList<T>是能夠放入任何的資料型態/類別型態,而TList<T>和TObjectList<T>的差別是TObjectList<T>能夠自動管理在TObjectList<T>中包含的物件的生命週期,簡單的說TObjectList<T>能夠在本身的樣例釋放時自動釋放它管理的所有物件樣例。 例如TObjectList<T>有如下的建構函式原型: constructor Create(AOwnsObjects: Boolean = True); overload; 我們可以看到它的建構函式接受一個AOwnsObjects的參數,它的內定值為True,這代表一旦開發人員建立了TObjectList<T>物件之後,如果在其中放入物件樣例,那麼TObjectList<T>物件在被釋放時也會釋放它包含的所有物件樣例。例如假設現在我們有一個TProduct類別,如果我們使用TObjectList<T>來管理TProduct類別,那麼可以使用如下的程式碼: var aProduct: TProduct; ptList : TObjectList<TProduct>; begin … ptList := TObjectList<TProduct>.create; try ptList.Add(TProduct.create(‘Delphi’)); ptList.Add(TProduct.create(‘BCB’)); ptList.Add(TProduct.create(‘JBuilder’)); … finally ptList.Free; end; .. 那麼當上面的程式碼執行到ptList.Free時,ptList中包含的3個TProduct物件也會自動被釋放。當然,如果您不希望TObjectList<T>自動刪除其中管理的物件,那麼在建立時傳遞False給它的建構函式做為參數,或是在建立之後設定它的OwnsObjects特性值為False也可以。 有了這些基本的知識之後,讓我們使用一個簡單的範例來說明如何使用TObjectList<T>和TEnumerator<T>,TDictionary<T,T>以及TComparer<T>等泛型容器類別,在讀者瞭解了如何使用這四個類別之後對於使用其他的泛型容器類別應該就非常簡單了。 使用TObjectList和TEnumerator 為了說明起見,讓我們定義一個TProduct類別如下: TProduct = class private FName : string; FCode : String; FCategory : string; FVersion : double; public constructor Create(sName, sCode : string; dVersion : double; sCategory : string = 'Delphi'); destructor Destroy; override; function GetName : string; function GetCode : string; function GetCategory : string; function GetVersion : double; end; 接著我們建立一個TObjectList<TProduct>: procedure TForm17.FormCreate(Sender: TObject); begin glProduct := TObjectList<TProduct>.Create(True); end; Delphi 2009的泛型容器類別是定義在一個新的程式單元Generics.Collections之中,因此當然要記得先在uses句子中加入使用Generics.Collections。 接著其中建立一些範例TProduct物件: procedure TForm17.CreateTempProducts; begin glProduct.Add(TProduct.Create('Delphi 2009', 'Tiburon', 12.0)); glProduct.Add(TProduct.Create('Delphi 2007', 'Highlander', 11.5)); glProduct.Add(TProduct.Create('Delphi 2006', 'Dexter', 11.0)); glProduct.Add(TProduct.Create('C++Builder 2009', 'Tiburon', 12.0, 'BCB')); glProduct.Add(TProduct.Create('C++Builder 6', 'Riptide', 6.0, 'BCB')); glProduct.Add(TProduct.Create('C++Builder 5', 'Rampage', 5.0, 'BCB')); end; 在一般使用泛型容器類別時,最常應用的情形是需要從泛型容器類別中一一取出它包含的元素來處理。在這種應用中大都是使用TEnumerator物件來幫助開發人員存取其中的元素,一般來說泛型容器類別都會提供GetEnumerator方法讓開發人員取得泛型容器類別對應的TEnumerator物件,再藉由TEnumerator物件來一一存取其中的元素。 在Delphi 2009中Tenumerator<T>有如下的宣告: TEnumerator<T> = class abstract protected function DoGetCurrent: T; virtual; abstract; function DoMoveNext: Boolean; virtual; abstract; public property Current: T read DoGetCurrent; function MoveNext: Boolean; end; 其中開發人員藉由呼叫MoveNext方法來判斷是否還有未存取的元素,而Current特性可以讓開發人員取得目前的元素。 在TList<T>類別中也定義了GetEnumerator方法可以取得它相關的TEnumerator物件: function GetEnumerator: TEnumerator; reintroduce; 例如假設現在我們希望把剛才加入的所有TProduct物件顯示在TListBox中,那麼就可以使用如下的程式碼: procedure TForm17.btn使用EnumeratorClick(Sender: TObject); var aEnum : TEnumerator<TProduct>; begin aEnum := glProduct.GetEnumerator; DisplayProducts(aEnum); end; 首先我們呼叫TObjectList的GetEnumerator取得TEnumerator物件,由於這個TEnumerator物件是存取TProduct物件,因此它的型態定義是TEnumerator<TProduct>,也就是把<T>代換成<TProduct>。 接著DisplayProducts就藉由TEnumerator物件進入while迴圈,如果TEnumerator物件的MoveNext持續回傳True就代表尚有未存取的TProduct。在while迴圈中開發人員可以藉由TEnumerator物件的Current特性值取得目前要處理的TProduct物件再進行後續的處理工作。 procedure TForm17.DisplayProducts(aEnum: TEnumerator<TProduct>); begin lb產品資料.Items.Clear; while (aEnum.MoveNext) do begin lb產品資料.Items.Add(aEnum.Current.GetName + ' : ' + aEnum.Current.GetCode); end; end; 排序泛型容器類別 另外一個經常使用的情形是需要排序泛型容器類別之中的元素,由於泛型容器類別可以包含任何型態的物件,因此如何排序其中的元素物件應該是根據不同的型態而異的,因此開發人員必須提供如何排序的程式碼給泛型容器類別來呼叫以決定元素之間的次序。 TObjectList<T>提供了兩個排序方法: procedure Sort; overload; procedure Sort(const AComparer: IComparer<T>); overload; 其中第一個Sort是使用內定的排序方式,開發人員如果需要定義自己的排序法則就需要使用第二個Sort方法。第二個Sort方法接受一個IComparer<T>介面的參數,IComparer<T>定義如下: IComparer<T> = interface function Compare(const Left, Right: T): Integer; end; IComparer<T>介面定義了Compare方法,它回傳整數,-1代表小於,0代表等於而1代表大於。 因此開發人員需要實作一個實作IComparer<T>介面的類別,再把這個類別的物件傳遞給上面的第二個Sort方法來實際的排序。 實作IComparer<T>最簡單的方法是定義一個從TComparer<T>類別繼承下來的子類別,因為Delphi2 2009已經定義了如下的TComparer<T>類別: TComparer<T> = class(TInterfacedObject, IComparer<T>) public class function Default: IComparer<T>; class function Construct(const Comparison: TComparison<T>): IComparer<T>; function Compare(const Left, Right: T): Integer; virtual; abstract; end; 因此我們要排序glProduct之中的TProduct物件,讓我們定義TproductLineComparer類別,它從TComparer<T>類別繼承下,我們只需要複載實作Compare來撰寫如何排序TProduct物件。在下面的程式碼中我們先以TProduct的Category特性值來排序,如果Category特性值相同,就以TProduct的Version特性值來做子排序條件: type TProductLineComparer = class(TComparer<TProduct>) public function Compare(const Left, Right: TProduct): Integer; override; end; implementation { TProductLineComparer<T> } function TProductLineComparer.Compare(const Left, Right: TProduct): Integer; begin Result := 0; if (Left.GetCategory < Right.GetCategory) then Result := -1 else if (Left.GetCategory > Right.GetCategory) then Result := 1 else begin if (Left.GetVersion < Right.GetVersion) then Result := -1 else if (Left.GetVersion > Right.GetVersion) then Result := 1; end; end; 有了TProductLineComparer之後就可以使用下面的程式碼來排序glProducts之中所有TProduct物件的次序了: procedure TForm17.btn依產品線排序Click(Sender: TObject); var aPLC : TProductLineComparer; begin aPLC := TProductLineComparer.Create; try glProduct.Sort(aPLC); DisplayProducts(glProduct.GetEnumerator); finally aPLC.Free; end; end; 上面的程式碼先建立TProductLineComparer物件,傳遞給Sort做為參數即可。 下圖是先呼叫TEnumerator顯示glProducts中所有的TProduct物件,讀者可以看到物件是以我們加入glProducts之中的次序顯示,而呼叫依產品線排序之後TProduct物件就以正確的產品種類+產品版本的次序來顯示。 結合匿名方法 現在讓我們稍為展示如何結合泛型和匿名方法。 Delphi 2009新的程式語言功能之一就是匿名方法,匿名方法主要的功能是允許開發人員在程式碼中定義一些小而簡單的程式碼來使用,而這些小而簡單的程式碼不需要正式定義成方法,而是使用完之後就不再需要的情形之中。此外匿名方法可以和泛型程式設計一起使用。現在讓我們再對glProducts中的TProduct進行排序,但是我們只需要對Delphi的產品排序。 因此我們需要先過濾出Delphi的產品物件,再根據過濾出Delphi的產品物件進行排序,下面的程式碼就可以完成這個工作: 001 procedure TForm17.btn依Delphi版本排序Click(Sender: TObject); 002 var 003 aDelphiFilter : TFunc<TProduct, Boolean>; 004 ltDelphi: TList<TProduct>; 005 aEnum : TEnumerator<TProduct>; 006 aPLC: TProductLineComparer; 007 begin 008 ltDelphi := TList<TProduct>.Create; 009 try 010 aEnum := glProduct.GetEnumerator; 011 aDelphiFilter := function (aProduct : TProduct) : Boolean 012 begin 013 Result := False; 014 if (aProduct.GetCategory = 'Delphi') then 015 Result := True; 016 end; 017 018 while aEnum.MoveNext do 019 begin 020 if (aDelphiFilter(aEnum.Current)) then 021 ltDelphi.Add(aEnum.Current); 022 end; 023 024 aPLC := TProductLineComparer.Create; 025 try 026 ltDelphi.Sort(aPLC); 027 Self.lb產品資料.items.Add('========依Delphi產品線排序=========='); 028 DisplayProducts(ltDelphi.GetEnumerator); 029 finally 030 aPLC.Free; 031 end; 032 finally 033 ltDelphi.Free; 034 end; 035 end; 上面程式碼的邏輯很簡單,我們使用一個匿名方法從glProduct中過濾出Delphi的產品,放入另外一個TList<T>泛型容器類別之中,最後再使用前面介紹的TProductLineComparer來排序。 在上面的程式碼中,003行的aDelphiFilter宣告的型態是TFunc<TProduct, Boolean>,那麼是TFunc<TProduct, Boolean>呢? TFunc就是一個接受泛型的匿名方法,它定義在SysUtils程式單元中,它的原型如下: TFunc<T,TResult> = reference to function (Arg1: T): TResult; 事實上在Delphi 2009的SysUtils程式單元中已經定義了許多通用的泛型匿名方法可以讓開發人員使用,它們的定義如下: // Generic Anonymous method declarations type TProc = reference to procedure; TProc<T> = reference to procedure (Arg1: T); TProc<T1,T2> = reference to procedure (Arg1: T1; Arg2: T2); TProc<T1,T2,T3> = reference to procedure (Arg1: T1; Arg2: T2; Arg3: T3); TProc<T1,T2,T3,T4> = reference to procedure (Arg1: T1; Arg2: T2; Arg3: T3; Arg4: T4); TFunc<TResult> = reference to function: TResult; TFunc<T,TResult> = reference to function (Arg1: T): TResult; TFunc<T1,T2,TResult> = reference to function (Arg1: T1; Arg2: T2): TResult; TFunc<T1,T2,T3,TResult> = reference to function (Arg1: T1; Arg2: T2; Arg3: T3): TResult; TFunc<T1,T2,T3,T4,TResult> = reference to function (Arg1: T1; Arg2: T2; Arg3: T3; Arg4: T4): TResult; TPredicate<T> = reference to function (Arg1: T): Boolean; 回到前面的程式碼中,在011到016定義了匿名方法並且指定給匿名方法變數aDelphiFilter,接著藉由TEnumerator物件一一的存取glProduct中的元素,並且使用aDelphiFilter來過濾出Delphi的產品,最後在026行呼叫ltDelphi的Sort方法並且傳入TProductLineComparer做為參數。 下面是執行這個方法的結果畫面: OK,下次讓我解釋如何結合使用TDictionary<T>以及匿名方法更為技術面的內容,Have Fun!。 September 20 回覆RURU有關ODBC, OLEDB, ADO和dbExpress的問題 我看了你說的那篇文章以及那篇文章指到的另外一篇文章,老實說我在一看到那2篇文章的開始就和我的認知有差別,在我的認知中OLEDB應該不可能比ODBC快,為什麼?這是一個故事,也是ODBC,OLEDB,ADO和dbExpress各自發展的原因,因此我們需要對於這些資料存取技術的背景有一個簡單的瞭解。 ODBC當初是MS為了在Window下提供一個共通的資料存取技術而發展出來的,目的是突破當時三大資料庫廠商Oracle,Sybase和Informix對於MS的SQL Server的圍堵,此外MS也想藉由這個共通的資料存取技術幫助MS Access擊倒Lotus和Approach,因此最早的ODBC的策略是提供一個最小公約數API,讓一個標準可以存取所有的資料庫,因此在ODBC的初期ODBC的效率很緩慢,因為除了實作技術不成熟之外,當時MS SQL Server的功能也不好,因此以SQL Server為中心思想發展出來的最小公約數ODBC在存取其他資料庫時簡直慢的不想話,這也是為什麼後來Borland發展出了BDE/IDAPI之後在功能和效率方面都比ODBC好上一大截的原因。但是當MS Server SQL逐漸成熟,ODBC的實作技術也慢慢成熟之後,以最小公約數為發展中心的ODBC因為功能最簡單,因此負荷最小,到了最後ODBC的效率是最好的資料存取之一。 OLEDB是當時MS想把COM/DCOM/COM+技術推為Windows平台的唯一技術時發展出來的,如此一來在Window平台所有的通訊,資料存取和元件都以COM/DCOM/COM+為核心,因此一度Borland也準備以COM/DCOM/COM+發展IDE,資料存取等。MS當時為了這個原因,因此有了OLEDB,但是舊的ODBC怎麼辦? 因此又發展出了ODBC和OLEDB Adapter的技術,讓ODBC也可以執行在COM/DCOM/COM+的世界,讓資料存取端認為ODBC也是OLEDB。OLEDB為什麼不會比ODBC快? 因為OLEDB功能比較多,而且OLEDB是以Ole Automation封裝傳遞的資料,負荷比ODBC大,因此ODBC應該會比較快。 至於ADO則是後來MS為MS Access,MS SQL Server特別在Windows平台發展出來的存取技術,因為當時由於Internet/Intranet的沖擊,MS的COM/DCOM/COM+不適合使用在Internet/Intranet上,因此MS逐漸準備放棄COM/DCOM/COM+,這也是OLEDB的長日將盡之日,ADO接手演出之時。ADO和ODBC不同的地方是,ADO再也不是使用最小公約數為中心,而是完全以MS的資料庫為核心,這也是為什麼當ADO推出後其他廠商並不熱衷支援ADO,我記得當時Oracle拒絕推出ADO For Oracle,MS迫不得已自己為Oracle寫了ADO驅動程式,但是又慢臭蟲又多,而Borland也決定繼續發展BDE/IDAPI,只是為ADO做簡單的封裝處理,但Delphi/BCB仍然以BDE/IDAPI為核心資料存取技術。 由於ADO不是使用最小公約數為中心,因此功能比ODBC多太多了,例如資料連結池,執行緒池,可分段存取資料,資料存取到用戶端時不是把所有記錄中的資料存取到用戶端,而是只把Key存取到用戶端,一旦當用戶端實際需要整筆資料時才藉由Key把資料存取到用戶端。因此如果只是簡單的單一用戶端存取資料測試,ADO不一定會比ODBC快,但是應該差不多,不過在實際的應用中,ADO應該是比ODBC整體效率快,但是記得ADO是為了MS的資料庫而生的,因此如果是使用ADO在其他廠商資料庫上則不一定。這是為什麼一些資料庫管理工具如果是管理多種不同的後端資料庫,那麼大多仍然是使用ODBC,為什麼? 因為簡單,因此穩定一點,所以快一點。例如Embarcadero的產品就是使用ODBC來管理後端多種資料庫。 OK,討論到這裡你應該知道ODBC,OLEDB和ADO的使用時機了。 現在回到dbExpress,dbExpress是為了跨平台發展出來的資料存取技術,不是為了特定的資料庫,也不是只為了Window 32平台。dbExpress可以執行在Win32,.NET,Win64,Linux,可以存取MS,Oracle等以及CodeGear自己的InterBase和BlackfishSQL。而dbExpress是結合跨平台和一些目前用戶端先進的資料存取概念為中心,例如dbExpress也提供連結池,執行緒池等。
你是技術人員,由你來回答這個答案吧,Have Fun! September 15 Delphi 2009產品技術發表會圓滿完成從9月9日到9月12日一連4天的Delphi 2009產品技術發表會終於順利完成了,在這連續4天的活動中我看到了Delphi許久未有的盛況,因為台北,新竹,台中和高雄的參加人數都創下近幾年的新高,每一場活動都座無虛席,我也看到了許多已經很久未見的老Delphi客戶,看來Delphi 2009的確是受到許多Delphi客戶的重視,也希望Delphi在新東家的手中能夠有更好的發展。 下面的URL是Delphi 2009產品技術發表會使用的Slides和一些簡單的範例,有需要的朋友可以自行下載 http://www.sinter.com.tw/borland/download/Delphi2009PL.zip 最後我也要謝謝所有參加Delphi 2009產品技術發表會的朋友,Have Fun! August 27 Embarcadero正式發佈Delphi 2009/C++Builder 2009Embarcadero在美國時間8/25日正式發表了開發代號為Tiburon的Delphi 2009/C++Builder 2009,其實對於參加過多次Borland/CodeGear beta產品測試的我來說早就在數天前就大約猜到了Tiburon將在8月底左右正式發表,為什麼呢?因為在進入8月20日左右Tiburon的beta 版本就開始愈來愈頻繁,從早期大約每個星期一個版本,到2,3天一個版本,再到8月中左右開始每天一個版本,到8月20日左右一天2到3個版本就可以推測Tiburon離正式發表不久了,因為最後頻繁的beta版本都應該都是在修正必須修改的臭蟲。 有異於以往Delphi/BCB一前一後發表的模型,Tiburon這次同時公佈了Delphi 2009和C++Builder 2009,這代表了從Tiburon開始Delphi和C++Builder使用的核心RTL/VCL都已經同步化,在編譯器方面Delphi的編譯器和C++Builder的編譯器也進行了更緊密的結合和同步化,因此這也讓如何進行Delphi 2009和C++Builder 2009產品技術發表會產生了令人頭痛的問題,由於場地和時間的限制,因此Embarcadero決定先在9月進行Delphi 2009的產品技術發表會:
待10月初再進行C++Builder 2009產品技術發表會,如此一來可以讓不同的使用者參加最適合的產品技術發表會。有人可能會說為什麼不同時進行Delphi 2009和C++Builder 2009產品技術發表會呢? 這是因為這次雖然Tiburon同時發佈了Delphi 2009和C++Builder 2009,但是這次的Delphi 2009和C++Builder 2009雖然擁有許多相同的新功能,但是C++Builder 2009擁有更多獨特的功能,例如Together For C++,Cpp0x,支援ACE/Boost等,因此在發表會的內容和範例上都會和Delphi 2009有著許多的不同,因此Embarcadero才決定讓Delphi和C++Builder的使用者參加各自專屬的產品技術發表會,這樣會讓不同的工具使用者盡興而歸。 如果您想參加Delphi 2009的產品技術發表會,您可以藉由下面的URL報名參加: 如果您想參加C++Builder 2009的產品技術發表會,請您稍待一會,一有確定的訊息我就會公告出來。 Have Fun! |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|