2019年8月4日 星期日

Delphi 為何仍然是開發工具最佳選擇

前言– Delphi的興衰與奮起

2019,是Delphi問世之後的第26個年頭,IDREA推出了Delphi/C++ Builder/RAD Studio的第20個版本—Delphi XE 10.3.2,但截至筆者撰寫本文的這當口,全世界仍有很高比例的Delphi開發人員持續在使用Delphi 5-Delphi 7,無關工具本身的好壞,而是關於既有專案能否「無痛升級」。
筆者在Delphi開發人員當中屬於異類的少數,並非筆者的能力,而是開發的應用面向不同。傳統的Delphi使用者,大多集中在與資料庫相關的系統,例如ERP、財會、MES等類別,以文件與資訊交換為主要作業。
Delphi甫一推出,就憑著高效能、與多種資料庫(尤其是Oracle)的介接容易、在Windows作業系統中的高相容性,程式語言的完備度,博得了「VB殺手」的薄倖名。
然而從1993-1996年之間,稱譽資訊學界近20年(含AdaPascal)的Pascal程式語言,漸漸被物件導向概念夾擊,先有挾C語言廣泛性優勢的C++出現,到了1997又有號稱全物件導向、可以透過虛擬機跨越所有平台、以沙盒概念提升安全性的JAVA來襲,整個資訊學界言必稱JAVA,語必稱物件,傳統的Pascal失去了學界的關愛眼神,從1998年開始,全台再沒有學校教授Pascal
然而,Borland1994年推出了Delphi,以Pascal為基礎語言,更為Pascal加入了物價導向的概念,成為Object Pascal,整個Delphi的物件導向概念與視覺化元件不斷優化,到了Delphi 5的時候大致完備,當時是1996-1997年間,然而JAVA當時也還沒完備,除了文件得印上幾大本,當年連編譯器都還沒有出現,遑論虛擬機。
到了2000年,Borland 負責DelphiPascal的靈魂人物Anders Hejlsberg被挖角到微軟,把原本沒能在DelphiObject Pascal語言上實現的設計與理想用在微軟新一代的平台跟語言設計上,於是就誕生了.Net FrameworkC#語言。接著Borland策略錯誤,以為可以做出Delphi 8成為跨.Net與機器語言的工具,沒想到微軟四兩撥千斤,2004年以不授權.Net FrameworkBorland為戰技,一下就讓已經要上市的Delphi 8無法推出,Delphi只好回頭繼續改良原本的工具,但也就此流失了關鍵的3年,直到2005年初才又推出Delphi 2005,但效能不如Delphi 7,介面又大幅度改變,所以絕大多數的Delphi 7使用者都不升級,歷經Delphi 20062007,都一蹶不振,於是Borland把開發工具部門獨立出來,成立了CodeGearBorland則專注於軟體開發流程控管的產品,後來越發寂寥,直到現在已經很多年沒聽過Borland了。
Delphi 2009這個版本中,可以算得上是CodeGear發奮圖強的開始,這個版本是第一個全環境支援Unicode的版本,從元件到程式碼,都完全支援Unicode,換句話說,除了Object Pascal語言的關鍵語法,其他的變數、類別命名,都可以使用Unicode字元,如果你願意,可以把變數用中英文混合命名,但打字會麻煩很多,所以筆者雖在2009年就已知道有此一功能,卻從未使用過,因為對維護來說,這是一個很負面的作法!
Delphi 2009算的上是CodeGear努力的成果,但到了2009年,Windows應用程式已經不是市場主流了,取而代之的主流,是手機應用程式。
筆者也是在2009年開始轉向手機應用程式開發,從Objective-C (iOS)JAVA (Android),連後端的控制平台(PHP),沒有一樣不用重新學習。在筆者已經把Objective-C弄到滾瓜爛熟之後,到了2014年,手機應用程式的製作市場又飽和了,此時Delphi已經又推出了Delphi 2010, Delphi XE, Delphi XE2, Delphi XE3這幾個版本,而從Delphi XE2之後,這個工具已經不再只是原本的開發人員所熟悉的Windows應用程式開發工具,而是跨平台的開發工具了,在XE2的年代,Delphi用來開發MacOSiOSWindows 32bit, Windows 64bit這幾種平台的應用程式已經完全沒有問題了,唯一的問題是,原本的開發人員也已經大多轉向了相同設計的C#,而Delphi成為了歷史名詞,不再能夠在一堆免費的開發工具中受人矚目。
之後從XE5 (Android Ready)XE6 (各平台穩定)XE7 (iOS 64bit)XE 10.0, XE10.1, XE 10.2, XE 10.3,除了更為穩定、速度更快,也完成了MacOSX 64 bitLinux程式的編譯功能。
歷史總是有很多有趣的地方,也有很多令人感傷的地方,但大多數的演進都已經介紹過了,我們開始進入主題,從技術面介紹一下新的Delphi

VCL v.s. FireMonkey

Delphi 1.0開始,視覺元件架構就是VCL (Visual Component Library),這個架構從Delphi 1.0一直到目前的Delphi 10.3都是Delphi的重要核心。VCL提供了絕大多數我們能夠在Windows作業系統中看到的視窗元件,並且與時俱進,從Windows 3.1的版本一直到Windows 10的版本,都能直接以原生碼(Native code)的方式執行,無須另外裝.Net Framework,但有些Delphi的元件在動態連結時會需要使用附在Delphi系統中的BPL檔案,這樣的檔案可以橫跨Windows 3.1, Windows 95, Windows 98….. 一直到Windows 10,相信目前沒有任何其他工具可以做到。
Delphi XE2之後,所有跨平台的應用程式,都需要使用FireMonkey這個新的架構來製作,FireMonkey當中,提供了WindowsMacOSXiOSAndroidLinux五大平台的元件,而尤其值得一提的是『單一專案,可編譯五種平台程式,各種程式都是原生機器碼!』
VCL當中,雖然可以滿足所有Windows平台的視覺元件需求,但VCL還是有其功能上的限制,例如:
1.      VCL要製作元件,所有元件都必須透過畫布重繪來顯示、也都必須由開發人員自行控制當中的互動邏輯(VCL元件中不能內含其他元件)
2.      VCL 元件必須透過元件註冊才能分享給其他程式在設計階段使用。
跨平台的功能就完全別提了。
FireMonkey則有幾個好處:
1.      跨平台,而且自動套用各平台的視覺樣式
2.      元件可以內含其他元件,製作元件時可以省下很多時間
3.      畫布與圖片都支援32Bit Alpha圖片,也就是可以製作半透明的畫面
4.      FireMonkey的畫面與座標會自動依照螢幕解析度進行必要的調整,過去在Windows系統中,傳統Delphi程式會在Windows螢幕字形比例大於100%的時候出現畫面錯置的情況,在FireMonkey裡面不會發生。
5.      MDI的程式問題,在FireMonkey不再發生。
理論條列講的差不多了,讓我們舉幾個實例來作深入一點的說明。
以往在VCL的程式中,很難把所有的畫面都集中在同一個表單檔案裡面,但在手機App的概念中,並沒有多視窗的設計,因此FireMonkey也才採取了『所有元件都可以扮演Container角色』的設計,讓程式畫面可以集中在同一個畫面中。
這個設計從XEXE5不斷的被驗證與優化,在XE5之後,FireMonkey提供了TFrame這個元件,讓我們可以把一整個畫面的程式碼跟表單(其實是Frame, 不是Form)獨立為單獨的檔案,這樣一來,所有的畫面雖然保留在同一個Form上面,但不同的畫面呈現可以被儲存在不同的Frame裡面。
很多公司把單一檔案的Size做了限定,希望程式碼可以不要集中在某幾個檔案裡面,這固然可以降低單一檔案的Size,使得Windows 32bitDelphi 7的限制不被挑戰,但有些時候,設計上是無法避免一個Pas檔案上3萬或5萬行的。(筆者在2008-2009服務於美商,製作備份系統的時候,單一主畫面就已經到10萬行,也曾聽聞有到15萬行的)
程式設計應該遵守的準則不少,但也需要看專案的性質跟是否能夠被實現,盡信書不如無書,這句話在程式設計上更應該被重視,也是所有程式人員應該時時刻刻謹記在心的。
透過適當的物件設計,避免獨立、不屬於任何物件的Function與變數,能夠避免一些問題,這雖然是物件導向程式設計的基本,但筆者在替許多專案程式碼進行優化與修改的時候,發現到雖然現在已經是2010年代,即將進入2020年代,但很多程式人員的物件導向概念還停留在1980年代。常見的問題很多,我列出幾個很嚴重的問題,期望大家盡量不要明知故犯:
1.   盡量完全使用物件設計,避免全域變數、全域函式或Procedure,避免多個Unit裡面宣告了相同名稱的變數,卻沒有被發現,在編譯上雖然不會出錯,但執行時卻會發生完全無法預估的錯誤,而且也很難除錯。
2.   在畫面上要使用的資料,務必獨立由該單元檔或表單檔獨立控制,如果其他單元檔案需要處理不同單元檔的資料,一定要透過參數、函式進行傳遞與修改,不要直接抓畫面變數、資料,這會造成未來多個單元檔互相羈絆,無法獨立修改內容,甚至升級,系統寫到這個程度的話,只能說是病入膏肓,除了重新設計,很難有救活的機會,越大的系統難度越高。
3.   承第2點,畫面的設計、資料處理、儲存的方式,最好可以獨立分開設計與處理,不要把這三個主要的部分綁的死死的。例如畫面上的各個欄位可以有獨立的變數名稱,儲存時用內部物件來儲存,要計算的時候透過這個內部物件來處理,需要存檔或讀檔的時候,也透過內部物件來暫存,這樣一來,任何一部分作修改,都不用考量連鎖反應,因為不會有連鎖反應,程式的修改與未來的維護才會簡化人力,也就是簡化成本。這個作法,在設計模式上面,被稱為MVC Pattern.(MVC模式)
4.   處理同一種計算或作業的程式碼,最好可以用同一個Method來處理,未來有任何需要修改的時候,修改一個Method,在系統中所有的地方都一起修改好了。在設計模式上面,這個作法被稱為Factory Pattern.(工廠模式)
5.   在處理可能會被大量呼叫的Method時,務必採用效率最好的演算法與最適合的物件,在這種Method當中使用越多的迴圈,程式的效能就會越低落。
6.   盡量使用Thread以及Task來處理可以同時被處理的作業,這樣可以在多核心的機器上獲得最大的效能。
傳統的VCL有很多難以避免的問題,也不一定可以守的住上述的幾種規範,因此,如果舊系統已經如我所述的『病入膏肓』,重寫在所難免時,請先考量FireMonkey

Big5UTF8

在我所看過的,歷史在10年以上的Delphi程式,很少有當時就已經能夠相容於Unicode的程式,即使是我所提到的這些少數的程式碼,只要不是使用Delphi 2009以後的系統升級或處理過的,也只能透過TntWare系列的元件提供Unicode相容的功能。
因此,筆者這十年來被問最多次的問題,就是『要怎麼樣才能無痛把舊版Delphi程式升級到支援Unicode』,然而,這並不是一個單純的問題,我們最少需要考量三個層面:介面、程式資料處理、資料庫或網路通訊。
這三個層面說的輕巧,但是能夠精通的人並不多。而且我說的是『最少』,最嚴重的問題不只是這三個層面,還在於程式中是否使用了『第三方元件』,而這些『第三方元件』是否有支援Unicode的新版本?或者這些『第三方元件』是否還有人在維護?是否擁有這些元件的原始碼?
我們就這些環節一個一個來剖析:
第三方元件通常是最棘手的。很多案例中,使用了大量的第三方元件,可能從網路通訊、視窗外觀客製化、繪圖、加密、資料庫、條碼產生、報表繪製/預覽/列印、到晶片卡讀卡/簽章等功能,除了晶片卡之外,其他我提到的元件,在原廠FireMonkey都可以有原生方式可以解決。
介面:舊版的Delphi製作的介面是傳統的VCL元件,只要該元件還存在,沒有隨著Delphi的版本更迭而被停用,就可以用新版的Delphi開啟該表單檔案,基本上可以無痛升級,但如果有些自行安裝或撰寫的視覺元件是舊版的,就必須要把這些元件全數升級、安裝到新版Delphi,才能正確開啟該表單檔案。
程式資料處理:這部份相對麻煩一點,可以分成兩個部分來處理,第一部分是寫在程式碼裡面的文字訊息,因為檔案本身是ANSI編碼,中文資訊自然就是Big5編碼。此時,我們可以用新版Delphi開啟程式碼,如果該檔案是表單,也可以按F12切換畫面跟程式碼。切換到程式碼之後,在程式碼的任何一個地方點滑鼠右鍵,選擇File Format,將之設定為UTF8,就『大致』完成了。
這只是『大致』而已,因為如果在程式碼當中使用了檔案、網路介面傳遞資料,傳統的Delphi程式人員習慣會把String拿來當成Byte陣列指標,然後用陣列元素來存取當中的資料,如果有這樣的情形,請記得把這些資料的處理改用TRawByteStringTBytes來處理。
Delphi 2009之後,String型別預設就是WideString。而在Delphi 1Delphi 2007當中,String型別預設則是AnsiString,長度是不同的,所以用陣列元素來取資料,會發生全部錯誤的狀況。
資料庫元件:通常傳統的程式中,使用的資料庫連線可能是DBExpressBDE、也可能是ADODBODBC,但到了Delphi XE 10之後,全數資料庫元件都更新為FireDAC元件系列,表單畫面上一定會找不到這些元件,這部份的修改是最費時的。
接著如果要改寫成FireDAC,需要修改的應該只有Connection元件、連線設定、Table/Query/Transaction元件的更換,至於SQL指令則不太需要更改。
還是那句話,這些分析都是輕巧的幾句話,但真的在裡面修改、更換元件,可能會是嘔心瀝血的過程,遇到問題的話,大家可以分享、交流一下經驗。
許多公司仍舊堅持著使用Delphi,可能是因為過去的Code Base很大,無法很快換掉,也可能是因為發現了Delphi原來的優勢、未來的優點。不可諱言的,Delphi的程式人員不像過去那麼多了,但這也是好事,表示濫竽充數的人也少了,我們不用花費太多的成本去抓出這些人來Fire掉。人才很多,帶人帶心,如何找到適合的人才,讓他願意為公司服務、在公司成長的路上一起成長,是每個公司的課題。

這年頭,實習、培訓、找外包,方法多的是,如果不用Delphi的原因只是因為怕人不好找,那麼,這家公司的文化也應該會讓很多資深的人敬而遠之了,許多資深的人才並不怕苦,怕的是苦完了還要被精神剝削,那麼,誰也不願意留下來了……

2019年4月28日 星期日

使用 Delphi 的指令進行自動建置,並排除執行時可能遇到的問題

前言、CI簡單介紹

在專業的軟體公司中,需求分析、系統分析、系統設計、Coding、版本控制、Build 版本、測試,是每一天不斷在進行的過程。從 Delphi 還在 Borland 旗下的時候,就不斷在這些流程當中尋求優化。

2005年到2007年之間,Borland出色的版本控制系統 - Star Team 是我當時服務的公司非常倚重的核心版本控制軟體,只可惜 Star Team 的價格很高,無法普及,隨著時間的流逝,版本控制系統一度流行走過了 SVN,現在則是全球流行用 Git 來做版本控制。

目前最流行的免費 Git 版本控制系統提供商,絕對是 GitHub 與 GitLab,這兩個提供者之間的差別,坊間有許多的比較資訊,透過 Command Line,我們可以很快速的把專案透過 GitHub/GitLab 同步,讓許多開發人員同時一起對一個相同的專案進行程式碼的修改與新增。

GitLab/GitHub 要如何跟 Delphi 搭配做到 CI (Continue Integration),Embarcadero在過去兩年內,也和很多家 CI 的軟體進行搭配,簡單的 Google 一下,絕對可以找的到相關文章,我在這裡就不獻醜了。

這一篇文章要跟大家分享的,是我們自己土炮製作出自動/手動建置系統時,透過 Delphi編譯指令時,一定會遇到的問題,截至 2019/4/28 下午4:41,我還沒有回報問題給 Embarcadero,但即使回報了,也不見得所有開發人員都會更新到最新版的 Delphi,所以,如何在現狀中找到求生方法,就是這一篇要跟大家分享的主題了。

如何透過指令來建置 Delphi 專案

Delphi 的開發人員,對於在 Delphi 的 IDE 當中要如何編譯、建置專案,應該都不用我多說,無論是 F9, Ctrl + F9, 或是 Shift + F9, 都可以簡單的把 EXE 檔案生出來。

但到了 DOS 視窗裡面,要怎麼面對 Delphi 的 DProj 與 DPR 檔案,恐怕連資深的開發人員也要稍微想上一會兒,不囉嗦,我們直接開講:

開啟一個 DOS 命令提示字元視窗 -> 快速鍵 (Win + R, Cmd, Enter)
開啟 DOS 視窗之後,我先把路徑切到專案所在的目錄 (D:\XE10.3-Rio\jsonDefTest),如上圖所示。

從 Delphi 2007 開始,Delphi 不做自己的組建系統,改以使用 MSBuild 來建置 Delphi 的專案,專案檔仍是使用從 Delphi 2005 開始就沿用的 dproj 檔案格式。一般來說,如果沒有特殊的需求,直接鍵入 "MSBuild 專案名.dproj",就能把專案建置出來了,我們這就來試試看:
居然出現有錯,說是找不到 msbuild 這個指令?!!!

是的,Delphi 的編譯還需要不少環境變數的設定,所以在安裝有 Delphi 的系統中,都會在 Delphi 的目錄裡加入一個名為 rsvars.bat 的批次檔案,這個路徑已經在安裝 Delphi 的時候自動被安裝程式加到了我們的登入設定中。

所以要執行 MSBuild 之前,請先執行 rsvars 這個指令,就可以成功了:






在 Embarcadero 的說明網站中,也有介紹許多 MSBuild 對於 Delphi 專案的參數,大家可以先看一下這些參數,了解一下 MSBuild 的操作方法,最少我們會需要知道如何建立 Debug/Release 這兩種不同設定組態的執行檔,以下的指令就是建置 Release 組態的 EXE檔案,相當於我們在 IDE 裡面按下 Shift+F9.

MSBuild "D:\XE10.3-Rio\jsonDefTest\jsonDefTest.dproj" /t:Build /p:Config="Release"


這樣的建置,會把專案裡面用到的所有檔案都重新編譯,不會用到之前就已經建置好的 dcu 檔案 (當然 RTL 除外啦),這作法也是大多數的開發朋友們最常用來建立 EXE 檔案的作法。

Pre-build, Post-build Event

在 Delphi 使用了 MSBuild 之後,也把 MSBuild 的好處一起帶進到 Delphi 的編譯環境中,例如建置前、建置後要執行哪些作業,就可以在專案裡面設定好,設定的畫面如下:


這個功能,只有在比較複雜的專案中,才能發覺它的重要性,舉幾個目前我遇到使用這個功能的專案需要它的原因:
  • 專案常常需要變更 Output Dir,因為開發環境跟安裝環境的路徑中,有些檔案可能不一致,所以編譯完成後,要把產出的檔案複製到固定目錄,讓安裝程式找到的都是最新版本的檔案。
  • 多個專案需要使用到共用的資源檔案,為了避免這些資源檔案有版本不一致的機會,在編譯 EXE 檔案前要先編譯資源檔 (*.RES),編譯好了之後,再把編譯出來的 RES檔案複製到各個專案目錄中。

介紹到這裡,相信大家都已經可以回頭在自己的系統中試試看怎麼用指令方式建置自己的系統了吧?如果要整合 GitLab/GitHub,相信也並不複雜,真的有問題,可以詢問 Embarcadero 的代理商,說不定我就會出現在貴單位,協助建置自動建置系統了。

問題一:版號如何維持最新?要用什麼方法更換版號與Debug/Release組態?

這是一個好問題,但如果你是比較積極的 Delphi 開發人員,應該不用問這個問題,可能你已經找到解決方法了。

這個方法就是:在 MSBuild 執行之前,先把 dproj 檔案裡面的 FileVersion 跟 ProductVersion 更換為我們想要的內容,我已經把這些邏輯寫成以下的程式碼,開放給所有需要的開發人員,可以免費商用,但請在產品的說明文件中提及作者與原始網址(就是本篇文章的部落格網址),程式碼在本篇文章的最後。

問題二:多個Pre-build/Post-build指令在 dproj 當中沒有問題,但為何用 MSBuild 指令建置的時候,會回報錯誤?這問題要怎麼解決?  

這問題就是本篇文章最開始的時候提到的,問題是因為在 dproj 存檔的時候,是以XML作為檔案格式,而多行指令存檔的時候,會以&作為換行的記號。

但問題來了,&符號是 XML 裡面用來標註特殊符號的保留字,但存檔的時候,XML會自行對它做 Encode,所以 & 符號就成了 sLineBreak + &&

這麼一來, MSBuild 執行的時候,遇到 & 符號就沒辦法處理(因為它應該是換行符號啊.......)。所以,我們得在執行MSBuild之前,把這個符號先換成一個 & 這樣一來,MSBuild 才能正確處理它。

MSBuild 使用方法很重要,使用時會遇到的兩個問題也為大家提供了解決方法,解決這兩個問題的程式碼如下,公開給大家免費使用,如果這個小工具有幫助,記得使用的時候發個 Email 給我,告訴我一聲,如果有遇到問題,也歡迎跟我聯繫一下:


program changeProjVer;

////////////////////////////////////////////////////////////////////////////////
/// Created by Dennies Chang dennies@ms4.hinet, dennies226@gmail.com
///
///   If you need to use this utility, please refer the original URL:
///   https://firemonkeylessons.blogspot.com/2019/04/delphiBuildCommandAndTools.html
///
///   And do not remve these lines.
///   The code is opened for all Delphi programmers, you can use it as
///   commercial/non-commercial usage, what you have to do, is to have a notice
///   for the original author.
///
///   And send an Email to dennies@ms4.hinet.net to me, thanks.

{$APPTYPE CONSOLE}
{$R *.res}

uses
   System.SysUtils, IdGlobal, Classes;

var
   currentFile, tmpStr, completeStr, tmpMajor, tmpMinor, tmpRelease,
       tmpBuild, configName: String;
   lineIdx: Integer;
   src: TStringList;
   bDebug : boolean;
begin
   try
      { TODO -oUser -cConsole Main : Insert code here }
      if ParamCount < 2 then begin
         writeln('Usage: changeProjVer.exe dprojFileFullPath versionNo [Debug|Release]');
         writeln('versionNo should be contain 3 dots, e.g.,: 107.1.108.321');
         writeln;
         Readln;
      end
      else begin
         currentFile := ParamStr(1);
         tmpBuild := ParamStr(2);

         bDebug := False;
         if ParamCount >= 3 then begin
            configName := ParamStr(3);
            bDebug := configName.ToLower = 'debug';
         end;

         tmpMajor := Trim(Fetch(tmpBuild, '.'));
         tmpMinor := Trim(Fetch(tmpBuild, '.'));
         tmpRelease := Trim(Fetch(tmpBuild, '.'));
         tmpBuild := Trim(Fetch(tmpBuild, '.'));

         if FileExists(currentFile) then begin
            src := TStringList.Create;
            try
               src.LoadFromFile(currentFile, TEncoding.UTF8);

               for lineIdx := 0 to src.Count - 1 do begin
                  completeStr := src.Strings[lineIdx];
                  tmpStr := '';

                  if Pos('<VerInfo_MajorVer>', completeStr) > 0 then begin
                     tmpStr := Fetch(completeStr, '<VerInfo_MajorVer>');
                     tmpStr := #9 + #9 + '<VerInfo_MajorVer>' + tmpMajor +
                         '</VerInfo_MajorVer>';
                     // completeStr := tmpStr;
                  end
                  else if Pos('<VerInfo_MinorVer>', completeStr) > 0 then begin
                     tmpStr := Fetch(completeStr, '<VerInfo_MinorVer>');
                     tmpStr := #9 + #9 + '<VerInfo_MinorVer>' + tmpMinor +
                         '</VerInfo_MinorVer>';
                     // completeStr := tmpStr;
                  end
                  else if Pos('<VerInfo_Release>', completeStr) > 0 then begin
                     tmpStr := Fetch(completeStr, '<VerInfo_Release>');
                     tmpStr := #9 + #9 + '<VerInfo_Release>' + tmpRelease +
                         '</VerInfo_Release>';
                     // completeStr := tmpStr;
                  end
                  else if Pos('<VerInfo_Build>', completeStr) > 0 then begin
                     tmpStr := Fetch(completeStr, '<VerInfo_Build>');
                     tmpStr := #9 + #9 + '<VerInfo_Build>' + tmpBuild +
                         '</VerInfo_Build>';
                     // completeStr := tmpStr;
                  end
                  else if Pos('FileVersion=', completeStr) > 0 then begin
                     // FileVersion
                     completeStr := src.Strings[lineIdx];
                     tmpStr := '';
                     while Pos('FileVersion=', completeStr) > 0 do begin
                        tmpStr := Fetch(completeStr, 'FileVersion=');
                        tmpStr := tmpStr + 'FileVersion=' +
                            StringReplace(ParamStr(2), ' ', '',
                            [rfReplaceAll]) + ';';
                        Fetch(completeStr, ';');
                     end;

                     if Length(completeStr) > 0 then begin
                        tmpStr := tmpStr + completeStr;
                     end;
                  end;

                  // 這兩個會出現在同一行, 不要加 else
                  if Pos('ProductVersion=', completeStr) > 0 then begin
                     completeStr := tmpStr;
                     tmpStr := '';
                     // ProductVersion
                     while Pos('ProductVersion=', completeStr) > 0 do begin
                        tmpStr := Fetch(completeStr, 'ProductVersion=');
                        tmpStr := tmpStr + 'ProductVersion=' +
                            StringReplace(ParamStr(2), ' ', '',
                            [rfReplaceAll]) + ';';
                        Fetch(completeStr, ';');
                     end;

                     if Length(completeStr) > 0 then begin
                        tmpStr := tmpStr + completeStr;
                     end;
                  end;

                  if (tmpStr = '') and (tmpStr <> completeStr) then
                     tmpStr := completeStr;

                  src.Strings[lineIdx] := tmpStr;
               end;

               src.Text := StringReplace(src.Text, sLineBreak + '&amp;&amp;', '&amp;', [rfReplaceAll]);
               src.SaveToFile(currentFile, TEncoding.UTF8);
            finally
               src.Free;
            end;
         end;
      end;
   except
      on E: Exception do
         writeln(E.ClassName, ': ', E.Message);
   end;

end.

2018年7月14日 星期六

TStrings, TStringList, UTF8 與 BOM

在 Delphi 2009 推出以前的年代,要處理 Unicode 的資料只能透過 tntWare 的元件,但在 Delphi 2009 之後,從 VCL 到 RTL 全部都已經相容於 Unicode 了,甚至連變數跟 Class 的名字都可以用 Unicode (你喜歡繁中簡中日文夾雜也可以) 來命名,所以程式內部對於 Unicode 的顯示、處理完全沒有問題。

但Unicode 的影響真的是深入到每個細縫,否則我也不會跟它奮鬥了將近20年......

文字檔的處理

對於 Delphi 的資深使用者來講,尤其是越資深的程式人員越有可能使用了這樣的 Work around,也就是把文字檔當成資料庫來使用。

在沒有 SQLite 的 2000 前後,我們可以用 DBase, MDB (Access 的檔案格式),但這些都需要透過驅動程式或者 BDE 來處理。因此有更多的前輩使用的是固定長度文字作為模擬資料庫來使用,這是一把兩面刃,他處理問題、解決問題很快,但製造出來的問題更深更遠,尤其是歷史久遠的程式碼。

在 Delphi 3 到 Delphi 7 的年代,以及一直沒有升級到 Delphi 2009之後版本的前輩,除了會使用固定長度文字作為資料儲存之用,還有一個很糟糕的現象,很普遍,也很嚴重,就是會把 PChar 當成指標,透過 AssignFile 對檔案進行開啟與讀取,然後用 PChar 對檔案內容做 offset 的位置指定與讀取。

乍聽之下這好像沒有任何問題,但是這個『沒有問題』也僅限於 ASCII 與 Single Byte Char的文字檔,也就是在 Unicode 出現之前的年代。

在沒有 Unicode 的時候,全球的電腦系統發展並不是完全沒有進展的,當時我們已經有了 Unix, Linux, DOS, Windows 也到了 Windows 98,資訊界前輩們的努力是可歌可泣的。當時,使用羅馬字元的西方語系問題比較小,因為透過拼音文字,字母並不多。

但以方塊字為主的中文,以及從中文衍生出來的東亞語系文字,如日文、韓文,在資訊界暱稱為 CKJS (Chinese, Korean, Japanese, S我忘了是哪個語系了....),在顯示上面就不是一件簡單的事情了。

所以中文有 Big5, GB碼,日文有 Shift-JIS,各國都有自己的文字編碼,但主要都以 2 Bytes 的長度來定義字碼,再從系統中找到對應的字型加以顯示。

進入到 Unicode 之後,全球的所有知名的資訊廠商幾乎都加入了該聯盟(Unicode Consortium),因為涵蓋的語系非常多,所以兩個 Bytes 根本不夠用,在 Windows 作業系統中,核心的文字都是轉換為 UCS-4 來顯示的,不同的文字檔案或檔案來源,都有各自的轉換模組,所以 Windows 切換成各國文字來顯示都不成問題。

但是轉換為檔案儲存的時候,問題又來了,Unicode 的文字檔案編碼有 UTF-16 (BE), UTF-16 (LE), UCS-4, UCS-2, UTF8, UTF7 等等,讓人眼花撩亂。

目前比較常見的文字檔編碼,除了原本的非 Unicode 編碼,最常用的應該是 UTF-8了,因為 UTF-8 的編碼當中,在西方文字的編碼與 ASCII 完全相同,但是 CKJS 的文字就不是這樣囉,有的字在 UTF-8 當中用 2 bytes, 有的用 3 bytes 來儲存,但對於系統來講都算是一個字元。

TStrings 的更迭

前面提到過,使用 Delphi 撰寫的專案當中,有些比較有歷史的專案,會使用固定長度的文字檔來儲存資料,並搭配 AssignFile, PChar 來讀取文字,進行資料判別與讀取。這個作法在 Single Byte 的文字檔,以及 Single Byte 的 Delphi 當中,使用上是很方便的,但到了 Multi-Bytes Char 的文字檔,以及支援 Multi-Bytes 的 Delphi (Delphi 2009之後的所有版本),這個作法簡直成了惡夢。

在舊的環境當中,抓一個 Byte當一個字元,聽起來很直覺。但Single Byte 的 Delphi 搭配 UTF-8 的檔案編碼,這樣的讀取在中英文夾雜的資料就會出大問題,因為中文字在 UTF-8 檔案可能是兩個 Bytes, 也可能是 3 Bytes, 抓固定長度?鐵死的!

問題不只如此,寫入也是個大問題,以往的編碼法,例如Big5,可以用字元長度乘以2來當成資料長度,但改為用 UTF-8編碼的時候,哇~~~ 固定長度直接崩潰了!

因為以往定義的固定長度,其實當中有很大的問題,發生在名詞定義上的混淆。到底這裡所指的固定長度,是固定幾個字?還是固定幾個Bytes? 這一點就沒有釐清!而檔案編碼的不同,會讓剛提到的長度定義混淆更亂!

所以,在 Delphi 2009 之後,要正確的讀取文字檔案,我自己都養成習慣,一定使用 TStringList 來處理。

TStringList 在早期的 Delphi 當中,是唯一可用來建立 TStrings 物件的 Class,在 Delphi 2009 之後的版本中,它的 LoadFromFile, SaveToFile 方法,都加入了 Encoding 這個參數,我們可以透過參數的指定,直接用對應的編碼方法載入文字檔,只要編碼指定正確,讀入的文字就會是正確的了,寫法如下:

var
   tmpStrs : TStringList;
begin
    tmpStrs := TStringList.Create;
    try
         tmpStrs.LoadFromFile('要載入的文字檔', TEncoding.UTF8);
    finally
         tmpStrs.Free;
    end;
end;

BOM 的有無

在 UTF-8 的檔案儲存中,BOM (Bytes Order Mark) 是常被用來跟非 Unicode 編碼的檔案區分的符號。但 BOM 在很多場合中又會造成問題,例如 JSON 是不接受 BOM 的。

BOM 跟 TStringList 的處理,在 2010 年前後是很熱門的話題,但 Delphi 的 TStrings 已經加入了 WriteBOM 這個屬性,預設值是 True,所以以下這段程式碼:

var
   tmpStrs : TStringList;
begin
    tmpStrs := TStringList.Create;
    try
         tmpStrs.SaveToFile('寫入文字檔.txt', TEncoding.UTF8);
    finally
         tmpStrs.Free;
    end;
end;
產生出來的文字檔,預設是有 BOM 的,用文字編輯器打開,BOM 這三個 Bytes不會被顯示出來,但是透過 UltraEdit 或類似的可以在文字與16進位模式切換的編輯器,就會看到前面多了 3 Bytes。

想要把這 3 個 Bytes拿掉,只需要把 WriteBOM 設定為 False 即可:
var
   tmpStrs : TStringList;
begin
    tmpStrs := TStringList.Create;
    try
         tmpStrs.WriteBOM := False;
         tmpStrs.SaveToFile('文字檔沒有BOM.txt', TEncoding.UTF8);
    finally
         tmpStrs.Free;
    end;
end;
這樣產生出來的 UTF-8 編碼文字檔,就不會有 BOM 了,很簡單吧。

注意事項

如果我們的 TStringList/TStrings 內容是透過 LoadFromStream 取得的,而原來的 Stream 裡面又有不同的編碼方法,這時候 WriteBOM 就不一定能正常運作。

解決方法是:建立另一個 TStringList,透過 AddStrings 把剛剛使用 LoadFromStream 的 TStringList 的內容加過來之後再存,這樣就可以了:
var
   StrsFromStream, tmpStrs : TStringList;
begin
    StrsFromStream := TStringList.Create;
    tmpStrs := TStringList.Create;
    try
         .....
         StrsFromStream.LoadFromStream(其他的Stream Instance);

         tmpStrs.AddStrings(StrsFormStream);
         tmpStrs.WriteBOM := False;
         tmpStrs.SaveToFile('文字檔沒有BOM.txt', TEncoding.UTF8);
    finally
         StrsFromStream.Free;
         tmpStrs.Free;
    end;
end;
這樣就可以了,這問題煩了我一天,所以寫個筆記跟大家分享,也讓我自己做個記錄,因為最近記憶力變差了......


2018年6月22日 星期五

Generic in Delphi

Some History about Delphi 

Delphi has existed for about 23-24 years, in early version (Delphi 1-Delphi 7), Win32 is the only target Delphi shoots for. In year 2000-2003, Microsoft take .Net and C# as next star, Delphi also tried to get ahead, but failed in Delphi 8.

And in the next 10 years, Delphi 2005, 2006, 2007, CodeGear was spinned off from Borland, they tried to shoot for software lifetime management, so we had starteam boundled in these versions.

However, Windows didn’t get real improvement in those years, the only milestones we can see, could be multi-user, 64bit support, and UAC (User Access Control). And Delphi keep silent for years.

Delphi 2009 is a keystone for next couple versions, we have native Unicode support (RTL & components), Generic support, new TTask supports multi-core execution. These new features are great, and makes applications get remarkable performance improvement.

And Delphi XE series help programmers crossing to iOS, MacOSX, Android, and 64bit Linux server in “one code base”, with different compile configures, that’s awesome.

It’s a pity that the wonderful features are seldom used, it might because the sample codes are not enough in quantity or quality. So, let’s have a simple code sample to show you how to use “Generic” with Delphi.

Gernic? Template? What The Hell?

C++ is the very first language which makes object oriented concept well-defined. (C++ was defined earlier then JAVA, so we don’t discuss about the differences between C++ and JAVA in this article)

C++ defines multiple inheritance, Template, and several important concepts, it’s very powerful, and too powerful to implement in common codes.

Delphi takes object pascal as core language, and object pascal evolved for several times in paste decades with Delphi. Object pascal is single root concept, which means all classes in object pascal inherited from the same root class named “TObject”, no exceptions.

If we wish make a class with 2 classes, we need to implement the features with “interface”, and with well-defined interfaces, multiple features of specific classes can be adopted, or we can say to be implemented. Inheritance and interface are another long story, I will write another article to introduce.

What we say “Generic” or “Template”, is to replace specific type name with the keyword “<T>“ in the codes, with this modification, we can reduce the effort to adopt the class for other types.

If you do understand the above sentence, congratulations! You must be an experienced programmer, and you don’t need help from this article.

So, let’s speak English, with some sample codes, you might be understand the concept faster.

When I was a college student, once I was doing my homework from data structure class, or once I was trying to implement a "Stack" with Object Pascal, I will have the following thoughts:

  • Well, Stack, first in last out, or last in first out, so I need an array to store elements.
  • I have to define a push method to put data in.
  • I have to define a pop method to get data out.
 So, the following figure will be helpful to visualize the stack:




As a "stack for storing integer", the basic declaration will look like:

TMyStack = class (TObject)
private
   FElements: array[0..5] of Integer; // Array for storing integer.
public
   function push(element: integer) : integer; // the returned integer will indicate the
                                                                     // position of the pushed element.
   function pop: integer; // return last element, and remove it.

   constructor Create(); ovevrride; reintroduce;
   destructor Destory(); override;
end;

I skip the implementation codes, because there are more students searching the codes for their homework in recent years.

A stack class named "TMyStack" is declared in the above codes, but there are still a lot of problems in the above codes. For example, the above class can store only 6 integers, the number of element is limited, and as I mentioned, TMyStack is a "stack for integer", so the argument of push method must be "integer", and return type of pop method must be integer, too.

Let's resolve the limitation of element count first.

I don't remember which version of Delphi exactly, Delphi provide a feature named "variable length of array", we can change the length of an array with setLength function, the declaration of a variable length of array might like this:

var
   varLengIntArray : array of Integer;

and we can change the length of the array in run-time with the following codes:

setLength(varLengIntArray, 20);

The second argument "20", is the length we wish to make the array to be.


With this implementation, we can set the stack class free from the limitation of count of element:
TMyStack = class (TObject)
private
   FElements: array of Integer; // The array for storing elements.
   FElementCount: integer;
public
   function push(element: integer) : integer; // the returned integer will indicate the
                                                                     // position of the pushed element.
   function pop: integer; // return last element, and remove it.

   property count: integer; read FElementCount;

   constructor Create(); ovevrride; reintroduce;
   destructor Destory(); override;
end;

With the modification, push and pop methods will require some changes, constructor Create will require to initial the count:
==================================================================
constructor TMyStack.Create();
begin
   inherited Create();

  FElementCount := 0; // initialization, set the count of element as 0;
  setLength(FElements, 0); // initialization, set the element array as empty.
end;

function TMyStack.push(element: integer) : integer;
begin
    Inc(self.FElementCount); // Increasing the count of element by 1
    setLength(FElements, self.FElementCount); // Increasing the count of array by 1.

   self.FElements[self.FElementCount -1] := element; // saving the element in the
                                                                                    //  added position.
end;

function TMyStack.pop: integer;
begin
    Result := self.FElements[self.FElementCount -1]; // Returning the last element.

    Dec(self.FElementCount); // Decreasing the count by 1
    setLength(FElements, self.FElementCount); // Removing the last element of array.
end;
==================================================================

With the modification, the stack class was set free from limitation of count.

But, will we need a stack for integer only? Will we need a stack for string tomorrow? or will we need a stack for customized record or class?

If every time when we need a stack for particular type, we need to copy & paste the above codes, and then modify the all the "type" field, it will kill me, am I right?

What if customers require some "outstanding" features for particular types, we will need to modify all the stack classes one by one....... That will drive me crazy in second.

So, is there someway, we can make a stack for all type?

Dr. Alfred Lanning: *That*, Detective, is the right question. -- quoted from "I, robot".

"Generic" is the redemption.

For using the features of generic provided by Delphi, we need to use the "System.Generics.Collections" unit, there are so many types, functions ready for use.

First, let's modify the previous declaration, replace "array of integer" with "TArray<T>", hence, we can save any type of data in TMyStack class.

TMyStack<T> = class (TObject)
private
   FElements: TArray<T>; // New array type with element of all types.
   FElementCount: integer;
public
   function push(element: T) : integer; // the returned integer will indicate the
                                                                     // position of the pushed element.

   function pop: T; // return last element, and remove it.

   property count: integer; read FElementCount;

   constructor Create(); ovevrride; reintroduce;
   destructor Destory(); override;
end;


And the implementation should be modified as:
==================================================================
constructor TMyStack<T>.Create();
begin
   inherited Create();

  FElementCount := 0; // initialization, set the count of element as 0;
  setLength(FElements, 0); // initialization, set the element array as empty.
end;

function TMyStack<T>.push(element: T) : integer;
begin
    Inc(self.FElementCount); // Increasing the count of element by 1
    setLength(FElements, self.FElementCount); // Increasing the count of array by 1.

   self.FElements[self.FElementCount -1] := element; // saving the element in the
                                                                                    //  added position.

end;

function TMyStack<T>.pop: T;
begin
    Result := self.FElements[self.FElementCount -1]; // Returning the last element.

    Dec(self.FElementCount); // Decreasing the count by 1
    setLength(FElements, self.FElementCount); // Removing the last element of array.
end;
==================================================================

Oh my Goodness, that's all? Is that simple? And then? How can I use the class?
Just as this way:

var
   integerStack : TMyStack<Integer>;
begin
    integerStack := TMyStack<Integer>.Create;
    try
        integerStack.push(79);
        integerStack.push(7);
        integerStack.push(21);
        integerStack.push(13); 
    finally
         integerStack.Free;
    end;
end; 

In the above code, a stack will be built as the following figure:





And we can create a stack for saving strings:

var
   stringStack : TMyStack<String>;
begin
    stringStack := TMyStack<String>.Create;
    try
        stringStack.push('Woo');
        stringStack.push(Th');
        stringStack.push('at');
        stringStack.push('is'); 
        stringStack.push('sta');  
        stringStack.push('ck');  


        // p.s. I create the figure first, but the space is not long enough, so
        // I split the string into many pieces. 
    finally
         stringStack.Free;
    end;
end;



The built stack looks like the following figure:

With the above codes, the class implementation is not changed. What we do is to declare, create the instance of TMyStack<T>, then the class can be adopted with different data types, it's convenient, right?

There is a class named TList<T> in System.Generics.Collections. We have to create TObjectList to store any object derived from TObject, and we have to add additional codes to mapping different object instance.

With TList<T>, we can save the extended codes, works. In System.Generics.Collections, there are even TStack<T>, TQueue<T>, are you very interested in that unit now?

With a simple conclusion, Generic is to replace the data type with keyword <T> when we try to have a class implementation, and make it clear when we use the class in real execution codes. In this way, we will save so many time, and we, developers, will have extra time for our life. I HOPE SO.....

2018年6月20日 星期三

談一談從 Delphi 2009 之後就支援的重要功能 - 泛型 (Generic)

前言

在C++的語言基礎當中,除了物件導向、事件驅動的概念之外,模版設計(Template)也是非常重要的一環。然而,C++的開發人員能夠善用模版設計的並不多。模版設計這個好物,一般還有一個名稱,就是泛型 (Generic),這個好物在Delphi 2009 之後,也已經被加入到 Object Pascal裡面了,只是我們實在很少用到它。

然而,江湖一點訣,說破沒秘訣,大家對於泛型的少用,很多是因為不知道有這個功能,其次是知道有這個功能卻不知道怎麼使用。

所以,我們這一篇就來深入淺出的介紹一下『泛型』是什麼,順便用幾個簡單的範例來使用『泛型』吧。

泛型? 樣板? 揭起它的神祕面紗

所謂的泛型、樣板,其實就是在寫code的時候,把需要先定義好型別的宣告用一個關鍵字 <T> 來取代,未來真正在使用的時候,把T改成真正的型別,就可以讓這段code適用於多種不同的型別了。

這樣說明,如果您就聽懂了,那應該也不需要來看這篇文章,表示您的悟性頗高,屬於非常有能力的Programmer。(謎之音:喵的,聽的懂我跟你姓! 這不是跟我大學資料結構或物件導向程式設計老師說的一樣嘛?)

用實例來說明吧,我們說地球話,才不會被趕回火星..........

以前,寫資料結構作業的時候,或者用 Object Pascal 寫程式的時候,如果我們要用Delphi 來實作一個堆疊,我們通常會這麼想:

  • 堆疊嘛,資料要先進先出,所以要宣告一個陣列來儲存資料
  • 然後要定義一個 Push 方法,把資料放進去
  • 也要定義一個Pop方法,把資料取出來
  • 因為是堆疊,所以是後進先出 (最後放進去的要最先被取出,只有一個進出口)

可以用底下這張圖片來幫助思考:
以一個存放『整數』的堆疊來說,最最基本的宣告一般就會寫成這樣:
TMyStack = class (TObject)
private
   FElements: array[0..5] of Integer; // 用來存放元素的陣列.
public
   function push(element: integer) : integer; // 可以傳回 push進去的元素放在什麼位置.
   function pop: integer; // 直接傳回最後一個元素.

   constructor Create(); ovevrride; reintroduce;
   destructor Destory(); override;
end;

實作的程式碼我就不寫了,最近實在太多學生上網到處找作業的答案範本。

這段程式碼宣告了一個名為 TMyStack 的堆疊類別 (Stack Class),裡面是有很多問題的,例如 FElements 只能放 6 個整數,有元素個數的限制,因為我們前面說過這是一個存放『整數』的堆疊,所以 push 方法的參數是整數型別,pop 方法所回傳的資料也是整數型別。

先來解決資料長度限制的問題

我記不清是從 Delphi 5 還是 Delphi 7開始,Object Pascal就被賦予了可變長度陣列的功能,可以透過 setLength 來調整陣列的長度,宣告的寫法可以寫成:

var
   varLengIntArray : array of Integer;

調整長度的作法則是:
  setLength(varLengIntArray, 20);

後面的數字就是陣列調整後的長度。

這樣的作法,讓上面的整數堆疊陣列脫離了固定長度的限制,改寫過的 Class 宣告就會變成:
TMyStack = class (TObject)
private
   FElements: array of Integer; // 用來存放元素的陣列.
   FElementCount: integer;
public
   function push(element: integer) : integer; // 可以傳回 push進去的元素放在什麼位置.
   function pop: integer; // 直接傳回最後一個元素.

   property count: integer; read FElementCount;

   constructor Create(); ovevrride; reintroduce;
   destructor Destory(); override;
end;
這樣修改以後,push跟pop方法裡面也都要有相對應的程式修改,例如在 Create的時候,就要先對 FElementCount 做初始化,push 跟 pop 方法裡面,也得調整長度:

==================================================================
constructor TMyStack.Create();
begin
   inherited Create();

  FElementCount := 0; // 初始化,把元素個數設為 0;
  setLength(FElements, 0); // 初始化,把陣列長度也設為 0;
end;

function TMyStack.push(element: integer) : integer;
begin
    Inc(self.FElementCount); // 把元素個數加一
    setLength(FElements, self.FElementCount); // 把陣列長度也多加一個

   self.FElements[self.FElementCount -1] := element; // 把要 push 的元素放在新增的
                                                                                    //  陣列位置上
end;

function TMyStack.pop: integer;
begin
    Result := self.FElements[self.FElementCount -1]; // 把最後一個元素回傳.

    Dec(self.FElementCount); // 把元素個數減一
    setLength(FElements, self.FElementCount); // 把陣列長度也多減掉一個
end;
==================================================================

這樣修改完以後,整數堆疊就沒有長度限制了。

但是,我們只需要整數堆疊嗎? 會不會明天要一個字串堆疊? 後天會不會要一個自定 record 或者 class 的堆疊?

如果每次需要堆疊,就要重寫一次上面的程式碼,而要修改的地方只有型別,那不是煩死人了?如果又好死不死遇到堆疊裡面要加一些額外的功能(客戶的想像力永遠走在我們前面, #壽山, 你說是吧?),那所有堆疊的程式碼要一個一個去修改,光想像就很想對電腦下毒手.........

那有沒有什麼方法,可以讓我們寫一個堆疊,就可以存放所有型別?

當然有,泛型,就是我們的救贖啊........
要使用泛型,我們得在 use 區段裡面引入 System.Generics.Collections,這裡面有非常多的好物可以用。

我們首先把前面已經改過的類別宣告,再做一些小調整,使用TArray<T> 這段程式碼來取代 array of Integer,讓 FElements 可以容納各種型別的資料:

TMyStack<T> = class (TObject)
private
   FElements: TArray<T>; // 用來存放元素的陣列.
   FElementCount: integer;
public
   function push(element: T) : integer; // 可以傳回 push進去的元素放在什麼位置.
   function pop: T; // 直接傳回最後一個元素.

   property count: integer; read FElementCount;

   constructor Create(); ovevrride; reintroduce;
   destructor Destory(); override;
end;

實作的程式碼則需要修改為:
==================================================================
constructor TMyStack<T>.Create();
begin
   inherited Create();

  FElementCount := 0; // 初始化,把元素個數設為 0;
  setLength(FElements, 0); // 初始化,把陣列長度也設為 0;
end;

function TMyStack<T>.push(element: T) : integer;
begin
    Inc(self.FElementCount); // 把元素個數加一
    setLength(FElements, self.FElementCount); // 把陣列長度也多加一個

   self.FElements[self.FElementCount -1] := element; // 把要 push 的元素放在新增的
                                                                                    //  陣列位置上
end;

function TMyStack<T>.pop: T;
begin
    Result := self.FElements[self.FElementCount -1]; // 把最後一個元素回傳.

    Dec(self.FElementCount); // 把元素個數減一
    setLength(FElements, self.FElementCount); // 把陣列長度也多減掉一個
end;
==================================================================

我的老天鵝啊,這真是太方便了吧,程式碼這樣寫就好了? 那使用上要怎麼用?
就這樣:
var
   integerStack : TMyStack<Integer>;
begin
    integerStack := TMyStack<Integer>.Create;
    try
        integerStack.push(79);
        integerStack.push(7);
        integerStack.push(21);
        integerStack.push(13); 
    finally
         integerStack.Free;
    end;
end; 

上述這段程式碼,在 finally執行以前,就會建立出以下圖為範例的堆疊資料了:

我們也可以做字串堆疊:
var

   stringStack : TMyStack<String>;
begin
    stringStack := TMyStack<String>.Create;
    try
        stringStack.push('這');
        stringStack.push('就');
        stringStack.push('是');
        stringStack.push('泛'); 
        stringStack.push('型');  
        stringStack.push('啊');  
    finally
         stringStack.Free;
    end;
end;

上述這段程式碼,在 finally執行以前,建立出來的堆疊資料則如下圖:
這樣一來,程式碼都沒有變,我們只在使用 TMyStack<T> 這個 Class 的時候,在宣告、建立Class的時候指明要使用什麼型別,就能夠自由的把一份程式碼用在各種不同型別上了,是不是很方便?

System.Generics.Collections 裡面,TList<T>更是好用,以前我們得要自己做TObjectList,才能透過所有物件都是從 TObject 衍生出來的特性建立出可以儲存物件的List,而且每次使用的時候還得做型別轉換才能正確使用。

現在透過 TList<T>,這些額外的程式碼、型別轉換的工作就都省下來了,甚至連TStack<T>, TQueue<T>, 也都有提供,是不是也讓您想要玩玩看了呢?

泛型說穿了,就是把原本我們需要先寫明的型別,用<T>這個關鍵字取代掉,而改以在實際宣告、使用的時候才敘明型別,這樣一來,真的省下好多好多程式碼,也省下很多時間可以做其他更有意義的事情了,當然,這些事情還是要我們自己去發掘的,大家加油!
 

2018年6月15日 星期五

Delphi 開發手機 App 與其他工具之間的比較分析

寫在前頭

關於各種手機App開發的工具,從2010年前後到現在已經在很多不同的場合介紹過,在元智大學、中台科技大學、德霖科技大學等不同學校的講座、課程當中,都有類似的主題,所以對我來說,這個主題屬於駕輕就熟的範圍。

在 KTOP 的論壇當中,有很多前輩希望大家能夠貢獻所學,為資訊業共圖未來的榮景。當然,KTOP 的主題當中仍然是以 Delphi 為主軸,所以當 lazarus 前輩提了三個題目給我:
  1. Delphi/Lazarus INDY 網路程式設計
  2. Delphi 資料庫程式設計 (甚麼 FireMonkey 架構 , 工作上沒機會接觸, 完全不懂 ... 我用傳統連線方式一樣可以連資料庫啊 ...)
  3. Delphi 手機 APP 程式開發, 不知 Delphi 是否有比其他手機 APP 專用的開發工具有優勢 ..
這三個都是好題目,只是第一跟第二題需要比較多的篇幅來進行,所以我就先就第三題做了這篇文章。

第一題 Delphi/Lazarus Indy 網路程式設計,在我 2001 年的著述 - Delphi/Kylix Indy 網際網路程式設計當中已經寫了三百多頁,當年寫的是 Delphi 7 + Indy 8/9,歷經了17年,我在2009年曾經就內容版本做過一次更新,使用了Delphi 2009 與 Indy 10.0.52做了一次改版,但沒有出版社有興趣,所以只有 KTOP 副站長,也是 Embarcaderp MVP 的 GrandRuRu,以及少數幾位網友跟我購買了兩三個電子書的檔案,並沒有機會出版實體書,覺得有些遺憾。

第二題 Delphi 資料庫程式設計,從 Delphi 7 時期的 BDE,到 Delphi 2005-2009 時期的 dbExpress,再到 XE2-10.2 時期的 FireDAC/UniDAC,我自己在使用上,覺得除了 Connection 的設定上有所不同,其餘在 Table, Query 的使用上並沒有太大的改變,而且李維在為捷康所著的 Delphi Database 開發手冊當中已經有很詳盡的說明,所以我可能在接下來的幾篇文章當中,提供一些使用上的範例與心得,至於資料庫的使用細節,我就不敢斑門弄斧了。

現有的開發工具概觀

從2008年 iPhone 跟 Android 把行動裝置帶向了沒有微軟的世界之後,開發工具也跟微軟從此沒有那麼大的關係,以下,我把開發工具分成幾大類:
  • 原生工具
  • 網頁轉換工具
  • 其他需編譯的工具

原生工具

顧名思義,是由該作業系統的開發者所提供的開發工具,在 iOS 跟 Android 兩大陣營當中(抱歉,從2009至今,我從沒看到 Windows Phone 有任何復甦的可能,所以直接忽略不計),就是三種工具:
  • iOS: Xcode (包含 Objective-C 跟 Swift 兩種語言)
  • Android: Eclipse 跟 Android Studio (都是 JAVA 語言)
如果您對任何語言都不太熟悉,或者說沒有原有技能的包袱,學習原生工具自然是最跟的上作業系統更新的,因為每次作業系統一更新,都會或多或少有些變動,SDK也會跟著調整,這些調整都會直接包含在原生工具裡面,所以SDK跟的最為緊密,也不用擔心有程式碼或工具需要等開發工具的更新才能跟的上。

Objective-C 是從 2000 年前後就被蘋果作為官方程式語言,原本在 1996-1998之間,Object Pascal一度在蘋果的候選名單內,但後來我也不知道是什麼原因,蘋果還是決定自己定義 Objective-C 這個語言。

Objective-C 跟 C++, 跟 C 都完全不同,在語法上,使用中括號 [] 作為 method invoke 的符號,而使用 . 作為 property 的存取符號,也是因為 method invoke 的 statement 跟 C系列的語言完全不同,所以很多已經習慣 C 系列語言的開發人員完全跨不過去。

但從語言的核心來看,用中括號作為 invoke,以空格加上 prefix descriptor來描述每個參數,也很清楚易懂,可能人一習慣某種語法,就很難變化,所以 Objective-C 後來在推廣上也總是有難以突破的障礙,例如 C# 陣營的開發人員,就很排斥這種語法,所以蘋果後來在 2013 年前後開始推出 Swift 這個語言,語法就跟 C# 很相似,相信因此從微軟陣營轉向 Swift 的人不在少數。

Android 的開發工具則是在 2014 年作為一個分水嶺。
2014年以前,官方開發工具是 Eclipse,很多人會用,但沒有人敢說對 Eclipse 熟悉,因為 Eclipse 只要搭配不同的 SDK,就能變成完全不同的面貌,所以用 Eclipse 開發 Android 雖然是在 2014 年以前的唯一選擇,卻也是讓很多開發人員花費很多時間設定的工具。

2014年起,Google推出了 Android Studio,把很多之前難搞的SDK都整合在一個安裝檔裡面了,很好用,但跟之前用 Eclipse 的專案又有不同。前面我提到過,人習慣了某些東西以後就很難改變,所以目前 Android 的開發陣營裡,原生工具仍舊分為兩個不同的派別:Eclipse 跟 Android Studio.

原生工具的優缺點

原生工具的好處,是能夠跟作業系統緊密的結合,永遠不會落後,也不用等待開發工具的廠商更新什麼套件。但原生工具的致命缺點,就是兩個平台必須維護兩套 Source code,而且因為兩個平台工具上的差異,同一個 App 在兩個平台上的更新速度永遠不可能一樣,而且問題修正也不可能同步。

這就造成了原生工具開發的成本被墊高。一般客戶需要App的,分為兩種,一種是自己有養著開發人員,但開發人員對App不熟悉,所以先外包,再把Source code 接回來自己維護,這種客戶比較能從開發人員的角度想問題。

也就知道養著兩組人,就需要兩份成本,時間雖然可以接近,但兩組人的素質不可能完全相同,業界同時寫 iOS 跟 Android 的人不多,因為兩個平台的概念有不小的差距。

第二種客戶是行銷類的客戶,這類客戶會把兩個平台的 App 看成同一個東西,所以會要求用一份成本,做兩個平台的 App,維護時間也一樣,會要求同時產出、問題同時解決,時程要求短,成本要求低,品質要求高,而且不會有第二個案子,因為行銷類的客戶通常是為某個產品提案,效果沒有在短時間內看到,就不會有下一個案子。

就算有了效果,下一個案子的時間也是遙遙無期,而且會嫌成本太高,轉而用其他方式製作,例如 RWD 網頁。

網頁轉換工具

網頁轉換工具,則是前端工程師的最愛,這類工具只要把一個網站做好,所有網頁做成可以離線瀏覽,經由轉換包裹,就可以分別做成 iOS, Android 平台可以使用的 App,代表性的工具有三個,但其實都是同一個:
  • Adobe PhoneGap
  • Apache Cordova
  • IBM WorkLight
這三個都是同一個,最早都叫做 PhoneGap,但PhoneGap後來被 Adobe 買下,免費版的則轉由 Apache 基金會繼續維護下去,改了名字叫做 Apache Cordova。IBM 則在 2013年也用 Cordova 做了一個版本,給了新名字叫做 WorkLight。

這三個工具都是把網頁直接轉換成 App,也各自提供了很多 JQuery API,讓前端開發人員可以使用手機裝置的內建功能,例如GPS,電話功能、重力感應器功能等。

PhoneGap 有方便的 App 端工具,讓開發人員可以直接對包裹出來的頁面做直接互動式的測試。

Cordova 則是比較陽春,要先透過 Chrome, FireFox 先把內容測試好,再進行包裹。
WorkLight 則是提供了強大的後台,可以不透過 AppStore 直接置換掉網頁內容,換句話說,就是可以不透過 AppStore 的審查直接升級 App,所以國泰世華、中國信託的App後來都改用了 IBM WorkLight 來製作。

網頁轉換工具的優缺點

網頁轉換工具的好處是開發快速,前端設計師就能搞定一切,所以成本低,行銷類的客戶最喜歡這種工具。

缺點則是效能不彰,因為所有功能都是透過瀏覽器來顯示的,瀏覽器所有的限制、效能的侷限,都直接衝擊網頁轉換工具製作的 App,但是成本低,所以從 2015 年以後,大量行銷類的 App 都被使用這類工具的工作室或公司搶走了,行銷類的App用原生工具的比例也大為降低。

其他需編譯的工具

這類工具,通常是為了資深的開發人員而生的,包括有:
  • Delphi - 為了原來對 Delphi 熟悉的開發人員而生
  • Xamarin - 為了原來對 C# 熟悉的開發人員而生
這類工具,通常有自己的 IDE,也可以分別編譯 iOS, Android 的 App,編譯出來的執行檔效能也接近原生工具的效能,但仍舊分別有其優缺點:

Delphi 的優缺點:

先講缺點,Delphi製作App的缺點有兩個。

首先是 Foot Print太大,也就是檔案太肥。
因為 Delphi 製作 App 的時候是使用 FireMonkey 框架在 iOS 跟 Android 系統中產生兩個平台的介面,整個框架必須都編譯到程式中,所以檔案一定會很肥,這是無法避免的。

其次是所有第三方元件的原罪,就是當 iOS 一更新,有可能 Xcode 工具裡有修改功能,會導致 Delphi 需要等原廠出 patch 才能搭配新版的 Xcode 編譯,這個問題從 XE5到 10.2.3 都有,但這是原罪,沒辦法。

其次是優點,由於使用 FireMonkey 框架,所以所有的元件都不依靠作業系統的元件,在 iOS 跟 Android 平台的 App 可以使用同一套原始碼,有問題的時候也比較容易一起解決,兩平台的 App 可以由同一個人維護,成本比兩個人低一些。

Xamarin的介紹與優缺點

Xamarin是在2014年前後由一家獨立軟體公司開發出來的,後來在2016年被微軟收購,成為 Visual Studio的一部分。

Xamarin 的概念跟 Delphi 不一樣,這個工具是要讓所有寫 C# 的開發人員,可以用 C#開發 iOS 跟 Android 的 App。

但 C# 的控制,是在 iOS 上用 C# 操控 iOS SDK,在 Android 上用 C# 操控 Android SDK,因此造成了兩個平台的專案中,介面必須分別寫,然後共用的元件再另外寫,可是 iOS SDK 跟 Android SDK 的差別很大,結果就是使用 Xamarin 的工程師必須把兩個平台的 SDK 都要摸熟,而且一個 App 必須維護三種 code: iOS 介面, Android 介面, 以及核心邏輯程式。

對我們有其他選擇的人來看,這很浪費時間,但對只會 C# 的人來說,這完全得到了救贖。

所以,Xamarin 的優點跟缺點,必須從立場來看,對於不拘於只使用 C# 的人來說,Xamarin 要維護三套程式的作法,很蠢。

但對於只會 C# 的人來說,會了 C# 就可以暢行無阻,而且因為編譯都是用原生工具,效能跟原生工具的產出一樣好,且檔案也不會大到很可怕,所以這些都是優點。

結語

以上就是各種不同的開發工具的優缺點,跟大家分享。

我自己很熟悉 Objective-C跟 Delphi,JQuery也還可以,我不隱藏對JAVA的厭惡,但我也會 JAVA,所以我自己使用過 Delphi, Xcode, Apache Cordova, IBM WorkLight, Eclipse 開發過許多程式,或許這些心得算是開發人員可以有些共鳴的。

以我自己的喜好來看,我一個人要維護多個App的話,我會選擇 Delphi,因為程式邏輯跟介面都只需要一套,執行檔 (ipa, apk)是肥了一點,但效能還不錯,且在這些工具中,要跟資料庫連接的話,Delphi 是不二之選。

Objective-C要直接連 DB? 只有 SQLite,而且寫法挺煩的。透過 JSON 跟 Restful API 來處理還好一點。JQuery也只能透過後端工具來處理,我會選擇PHP。

但前台 SQLite 所有工具都一樣, 如果有需要直接連 MySQL, Delphi 會方便很多。
這些工具中,我獨漏了 Unity 3D,因為這工具我歸類是寫 Game 的,寫一般 App的話會很擾人且很惱人,所以沒有在此提及。