全國最多中醫師線上諮詢網站-台灣中醫網
發文 回覆 瀏覽次數:1745
推到 Plurk!
推到 Facebook!

深入Delphi編程

 
axsoft
版主


發表:681
回覆:1056
積分:969
註冊:2002-03-13

發送簡訊給我
#1 引用回覆 回覆 發表時間:2002-08-16 14:21:57 IP:61.218.xxx.xxx 未訂閱

深入Delphi編程

作者:左輕侯 資料來源: 無雙譜 剛剛接觸的Delphi的朋友,可能最感興趣的就是它豐富、強大的VCL(可視化構件庫)。僅僅向窗體上扔幾個構件,甚至不用動手寫代碼,就能很容易地做出一個有實用價值的程序,真是令人激動。但是,VCL只是Delphi的一小部分,Delphi的優秀遠遠不只是表現在VCL上。如果你僅僅停留在使用VCL這一階段,那麼你永遠也不可能成為一個真正的Delphi高手。記住,必須超越VCL,才能夠接觸到Delphi的核心。 那麼,在Delphi的VCL后面,到底隱藏著什麼呢?本文將討論兩個比較高級的Delphi主題:OOP和數據庫編程。 本文假定讀者已經具有Delphi編程的基礎知識,例如,熟悉Pascal語言的一般語法,掌握簡單的VCL使用,會使用Data-Ware構件編寫基本的數據庫程序,等等。本文不會重複VCL的使用這樣的問題。 1、OOP OOP的英文全稱是Object Oriented Programming,翻譯過來就是面向對象編程。OOP是一種非常重要的編程思想。也許你會對這種抽象的東西不感興趣,可是幾乎任何一個高手都會告訴你:“語言並不重要,重要的是編程思想。” 大家知道,Delphi的語言基礎是Object Pascal。這是Borland在傳統的Pascal語言中增加了面向對象的特性后發展而成,並且特地冠以Object的字樣,以示與傳統的Pascal語言的差別,可見面向對象技術對其影響之大。可以說,Delphi構建在Object Pascal的基礎之上,而Object Pascal構建在面向對象技術之上。 事實上,不但Delphi,OOP也是C 、Java等其他許多現代編程語言的基礎(Visual Basic不完全地支持OOP)。熟練地掌握OOP技術,是深入掌握Delphi的必要條件,是邁入高手境界的必經之路,也是一個成熟的程序員的標志之一。理解了OOP技術之后,很多以前令你困惑的東西會迎刃而解。 有趣的是,雖然Delphi是完全基于OOP的,但是一個完全不了解OOP的程序員也能夠使用Delphi編寫程序,因為Delphi會自動完成絕大多數的工作。當你開始學習Delphi的時候,你可能無法想象,當簡單地往窗體上添加一個按鈕時,Delphi會完成多麼複雜的工作吧!但是既然有志于深入Delphi的世界,成為一個真正的程序員,我們就應該對Delphi的細節具有好奇心。 這些理論可能會讓人覺得枯燥和望而生畏,其實當你掌握了它之后就會覺得沒什麼了。當然,你需要有足夠的毅力。 OOP有三個主要的特征: 1.1 數據封裝 讓我們先看一段代碼: type TDate = class Mouth,day,Year:Integer; procedure Setvalue(m,d,y:Integer); function LeapYear:Boolean; end; 我們首先會看到class關鍵字,它的中文譯名為“類”。類是一個非常重要的概念。根據權威的定義,類是:一種用戶定義的數據類型,它具有自己的說明和一些操作。一個類中含有一些內部數據和一些過程或函數形式的對象方法,通常來描述一些非常相似的對象所具有的共同特征和行為。 這個定義可能比較晦澀。你可以把類想象為一種特殊的Record類型,其中不但可能包含數據,而且可能包含函數和過程(在OOP中稱之為方法)。這些數據和方法被統稱為類的成員。 上面這個類很顯然是一個日期類型,它包括Mouth,Day,Year這三個數據成員,和Setvalue、LeapYear這兩個方法。順便說一句,在Delphi中,習慣以字母T作為每個類的前綴,就象Viusal C 中習慣以字母C作為每個類的前綴一樣。 Mouth,Day,Year這三個數據成員指定了該日期的年、月、日。Setvalue方法為這三個數據成員賦值,而LeapYear檢查當前日期所在的那一年是否是閏年。下面我們給出這兩個方法的實現部分:
 
  procedure TDate.Setvalue(m,d,y):Integer;
  begin
  Mouth := m;
  Day := d;
  Year := y;
  end;
  
  function TDate.LeapYear:Boolean;
  begin
  if (Year mod 4 <> 0) then
  LeapYear := False
  else if (Year mod 100 <> 0)
  LeapYear := True
  else if (Year mod 400 <> 0)
  LeapYear := False
  else
  LeapYear := True;
  end;
  
實現了這些定義之后,就可以這樣調用它們:
  var
  ADay: TDate;
  begin
  //建立一個對象
  ADay := TDate.create;
  //使用之
  ADay.Setvalue(1,1,2000);
  if ADay.LeapYear then
  ShowMessage('閏年:'   Inttostr(ADay.year));
  //釋放對象
  ADay.free;
  end;
  
我們來逐行解釋這些代碼的含義。var后面那一行聲明了一個TDate類的變量。 聲明了變量之后,我們怎麼使用它呢?使用TDate類的Create方法可以建立一個該類的對象,並將其賦予ADay變量。 現在我們又接觸到了OOP中另一個重要的概念:對象。什麼是對象?簡言之,對象就是類的實例,或者說,是類定義的數據類型的變量。當建立一個類的對象時,系統為它分配一塊內存。例如我們定義一個變量A為Integer類型,那麼,Integer是一個數據類型,A就是一個實例。類與對象的關系就類似于這兩者之間的關系。區別類和對象是非常重要的,甚至一些專業的程序員都往往將他們搞混。 細心的讀者可能注意到,在TDate類的定義中,並沒有Create這個方法。那麼這個Create方法是從哪里來的呢?Create方法是每一個Class都具有隱含的方法,它的作用是建立這個類的實例。請注意,在這里,類和其他的數據類型是不同的。其他的數據類型都是聲明了變量之后就可以直接使用,而類類型必須在使用Create方法創建它的實例(對象)之后才能使用。 事實上,在C 和其他大多數的OOP語言中,聲明一個類的變量就能夠同時建立起這個類的對象。而Delphi(包括它的孿生兄弟C Builder)在這方面與眾不同,必須要Create一下才能真正建立對象。同時,在這個對象不再需要時,必須要手工調用free方法釋放這個對象(當然,free方法也是每個類隱含的)。這和Delphi獨特的“對象引用模型”有關,有興趣的朋友可以查閱有關資料,我就不多說了。 這種情況造成了一個非常有趣的現象,那就是,編程的初學者往往忘記在使用對象之前create它,從而出錯,但從C 轉向Delphi的高手也常常犯同樣的錯誤…… 順便告訴大家一個訣竅,當編譯器出現“Read of Address: ffffffff”這樣的錯誤時,多半是因為在使用對象之前忘了Create,可以從這方面入手檢查代碼。另外,也千萬不要忘記在不需要它時使用free釋放掉,否則可能造成內存洩漏。 在建立和釋放對象的代碼的中間,是使用對象的代碼。訪問對象的數據成員非常簡單,和Record類型沒有什麼區別。可以點號表達式來訪問它們:
  ADay.Year := 2000;
  ADay.Mouth := 1;
  ADay.Day := 1;
  
同樣,也可以使用點號表達式來調用對象的方法。如果你閱讀了方法實現部分的代碼,你可以很容易地發現,ADay.Setvalue(1,1,2000)這一句分別為三個數據成員賦了值,而ADay.LeapYear調用則返回當前日期所在年是否為閏年。至此,整段代碼的意義也就清楚了。 然而,類不僅僅這麼簡單。上面這個例子是一個非常簡單的類,可以直接訪問它的任何成員(數據和方法)。但某些類的成員是不能被隨便訪問的。Delphi中用三個關鍵字區分這些成員的訪問權限: 表1 Private 該類型的成員只能在聲明類中被訪問 Public 該類型的成員可以被程序中的任何地方的代碼訪問 Protected 該類型的成員只能在聲明類以及聲明類的派生類中被訪問 Protected類型的成員以及什麼是“派生類”等問題我們留到以后再進行討論,現在我們將注意力集中在前兩者。 Public類型就是在上面例子中的那種類型,這個很好理解。而Private類型,根據表格中的簡單解釋,只能在該成員被聲明的那個類(也就是該成員所屬的那個類啦)中被訪問,越出這個界限,它就是不可見的。那麼,Private類型的成員將如何被使用呢?簡單地說,就是通過一個Public類的方法來訪問它。 讓我們看一個新的例子:
  type
  TDate = class
  private
  Mouth,day,Year:Integer;
  Public
  procedure Setvalue(m,d,y:Integer);
  function LeapYear:Boolean;
  function GetText:String;
  end;
  [/cpde]
  
  在這個類中,Mouth,Day,Year這三個成員被聲明為Private成員,因此它們在類以外的其它地方是不可訪問的。也就是說,如果你使用
  
  ADay.Year := 2000;
  
  這樣的代碼,那麼編譯器將會報錯。但是,我們可以照樣通過Setvalue方法為它們賦值:
  
  ADay.Setvalue(1,1,2000);
  
  這行代碼是合法的,因為Setvalue本身是TDate類的成員,而且它又是一個Public成員。而使用GetText方法則可以得到當前日期值(這也是得到當期日期值的唯一辦法)。
  這樣的設置使得類的一些成員被隱含起來,用戶只能用一些專門的方法來使用它們。那些可以被外部代碼訪問的成員稱之為類的接口。這樣做有什麼好處呢?首先,這讓類的作者可以檢測被賦值的內容。比如,用戶可能給一個對象賦予13月40日這樣的無效日期。而在隱含了一些成員之后,類的作者可以在方法的代碼中檢測這些值是否有效,從而大大地減少了產生錯誤的機會。其次,使用規範的類,作者可以隨時修改類內部的代碼,而使用該類的代碼卻無需任何修改!這樣使得代碼的維護成了一件輕松的事件,特別是對于多人協作的大型軟件而言。
  這就叫做數據的封裝(encapsulation)。這是OOP的第一個特征。一個優秀的OOP程序員,應該在設計類的時候,就確定將哪些重要的數據封裝起來,並給出一個高效率的接口。
  需要指出的一點是,表1中Private部分的論述對于“標准的”OOP語言(例如C  )是完全正確的,但對于Delphi有一個例外。在Delphi中,Private成員除了在聲明類中可以訪問外,在聲明類所在的單元(.pas文件)中的任何地方都能被訪問,不論這些代碼與聲明類的關系如何。嚴格來說,這是違反OOP的原則的,我不明白Borland為何要這麼做(據說是為了方便)。在關于Delphi的優劣性的討論中,這是常被涉及的一個問題。
  
  1.2 繼承與派生
  
  我們再來看一段代碼:
  

  type
  TNewDate = class(TDate)
  Public
  function GetTextNew:String;
  end;
  
  function GetText:String;
  begin
  return := inttostr(Mouth)   ':'   inttostr(Day)   ':'   inttostr(Year);
  end;      
可以看到,在class后面出現一個包含在括號中的類名。這種語法表示新的類繼承了一個舊的類。繼承了原有類的類稱之為派生類,也叫子類,被繼承的類稱之為基類,也叫父類。 派生類與基類之間是什麼關系呢?當派生類繼承自一個基類時,它自動具有基類的所有數據、方法以及其他類型,無須在派生類中再做說明。例如,可以象下面這段代碼這樣使用TNewDate類:
  var
  ADay: TNewDate;
  begin
  ADay := TNewDate.create;
  ADay.Setvalue(1,1,2000);
  if ADay.LeapYear then
  ShowMessage('閏年:'   Inttostr(ADay.year));
  ADay.free;
  end;
  
而且,派生類還可以在基類的基礎上加入自己的數據和方法。可以看到在TnewDate類中增加了一個新的方法GetTextNew。下面給出這個方法的實現部分:
  function GetTextNew:String;
  begin
  return := GetText;
  end;
  
然后調用它:
  ADay.GetTextNew;
  
這個新的方法工作得很好。 為什麼GetTextNew方法必須調用基類中的GetText方法,而不能直接使用GetText方法中的那些代碼呢?原因是,Mouth,Day,Year這三個成員被聲明為Private成員,因此它們即使在派生類中也是不能被訪問的,所以必須調用基類中的GetText方法,間接地使用它們。如果要直接使用它們的話,可以將這三個成員的屬性從Private改為Protected。在表1中可以看到,Protected屬性的成員可以在聲明類以及聲明類的派生類中被訪問,然而仍然不能被這兩種情況以外的其他代碼所訪問。現在我們終于可以理解了,這個特殊的屬性實際上提供了極大的方便:它使得類的成員被封裝,避免了混亂,同時又能夠讓派生類方便地使用它們。 (如果你是一個細心的人,你可能發現上面的話中間有一個小小的仳漏。當你真的在GetTextNew方法中訪問了基類的Private成員的話,你可能會驚奇地發現程序也能夠編譯通過而且正常運行!其實,這個問題和OOP本身沒有關系。上面我已經說過,在Delphi中,Private成員在聲明類所在的單元文件中的任何地方都能被訪問,因此如果TNewDate類和TDate類在同一個.pas文件中時,這種情況就不足為怪了。) 怎麼樣,是不是覺得非常奇妙?通過這種繼承的機制,類不再僅僅是數據和方法的封裝,它提供了開放性。你可以方便地繼承一個功能強大的類,然后添加進自己需要的特性,同時,你又不需要對基類進行任何的修改。相反,原作者對基類的任何改動,都可以在你的新類中立即反映出來。這非常符合代碼的重用要求。 這種繼承機制也非常符合現實世界中的情形。可以設想,一般意義上的“動物”是一個類,具有自己的一些特征(成員);而“狗”是“動物”的派生類,它具有動物的所有特征,同時還具有自己獨有的特征(四條腿,汪汪叫,等等)。而“狗”這個類可以繼續派生下去,例如“黑狗”“白狗”,它們除了保留狗的全部特征之外,還具有自己的特征(黑顏色,白顏色,等等)。而具體到一只活生生的狗,可以認為它就是“黑狗”或“白狗”(或其他什麼狗)的一個實例(對象)。 OOP這種對現實世界的模擬不僅極大地簡化了代碼的維護,而且使得整個編程思想產生了革命性的變化,較之模塊化編程有了飛躍的進步。 如果你曾經仔細閱讀過VCL的資料甚至它的源代碼,你就可以發現,整個VCL都是建立在這種強大的封裝-繼承的機制之上的。你可以看到一張詳細的VCL層次結構圖,就象是一個龐大的家譜,各種VCL構件通過層層繼承而產生。例如,一個簡簡單單的Tform類,就是許多次繼承之后的產物: TObject - TPersistent - TConponent - TControl - TWinControl - TScrollingWinControl - TCustomform - Tform 不但Delphi的VCL,Visual C 中的著名的MFC(Microsoft Foundation Class,微軟基本類庫),以及以前Borland C 中風光一時的OWL(Object Window Library,對象窗口類庫),都是建立在這種機制之上。所不同的是,對于前兩種語言,你要花上好幾個月的功夫去基本掌握那些繁複無比的類,才能寫出比較有實用價值的程序,而在Delphi中,大部分的工作Delphi都已經自動幫你完成了。例如,每次你向程序中加入一個窗體時,Delphi就自動為你從Tform派生一個新類(默認為Tform1),並且為這個新類創造一個實例。你對這個窗體的改動(添加構件和代碼之類),無非是為這個派生類加入一些新的特性而已;你再也用不著自己去處理最大化、最小化、改變大小這一類的情況,因為這些代碼都在基類中被實現,而被派生類所繼承了。這就是Delphi的偉大之處。當然,Delphi的VCL也絕不比MFC或OWL遜色(事實上它是由后者演變而來)。 (可能有人會問起VB的情況。VB不支持繼承,因此並沒有什麼複雜的類庫,它自己的控件也少得可憐,主要是使用ActiveX控件。)。 也許你已經若有所悟,為你的發現而心癢難騷了吧。但是,我們要討論的東西當然不會僅僅這麼簡單。 在1.1部分(“數據封裝”),我們講到了“Create方法是每一個Class都具有隱含的方法”。其實,這種說法是不准確的。事實是,在Delphi中,所有的類都默認繼承自一個最基礎的類TOject,甚至在你並未指定繼承的類名也是如此。Create方法是TObject類具有的方法,因此理所當然,所有的類都自動獲得了Create方法,不管你是否實現過它。想想看就知道了:如果沒有Create方法的話,怎樣建立一個對象呢? 你可能注意到了Create方法是一個特殊的方法。不錯,Create方法的確非常特殊,甚至于它的“頭銜”不再是function或procedure,而是Constructor(構造器)。你可以在VCL的源碼中見到這樣一些例子:
  Constructor Create;
  
構造器不僅是一個Delphi關鍵字,而且是一個OOP方法學的名詞。與之相對應的,還有Destructor(毀壞器)。前者負責完成創建一個對象的工作,為它分配內存,后者負責釋放這個對象,回收它的內存。要注意的一點是,Constructor的名字一般是Create,但Destructor的名字卻不是Free,而是Destroy。例如:
  Destructor Destroy;
  
那麼,在以前的代碼,為什麼又使用Free來釋放對象呢?二者的區別是,Destroy會直接釋放對象,而Free會事實檢查該對象是否存在,如果對象存在,或者對象不為nil,它才會調用Destroy。因此,程序中應該盡量使用free來釋放對象,這樣更加安全一些。(但要注意,free也不會自動將對象置為nil,所以在調用free之后,最好是再手動將對象置為nil。) 象對待一般的函數或過程那樣,也可以向構造器傳遞參數:
  type
  TDate = class
  private
  Mouth,day,Year:Integer;
  Public
  function LeapYear:Boolean;
  function GetText:String;
  Constructor Create(m,d,y:Integer);
  end;
  
  procedure TDate.Create(m,d,y):Integer;
  begin
  Mouth := m;
  Day := d;
  Year := y;
  end;
  
  調用它:
  
  ADay: TDate;
  begin
  ADay := TDate.create(1,1,2000);
  if ADay.LeapYear then
  ShowMessage('閏年:'   Inttostr(ADay.year));
  ADay.free;
  end;
  
這樣,在Create方法里就完成了對數據的初始化,而無須再調用Setvalue方法了。 接下來,我們將要涉及到另一個重要的、也是很有趣的問題:方法的虛擬與重載。 可能你已經有點暈了吧……還是先看一個新的例子:
  type
  TMyClass = class
  procedure One;virtual;
  end;
  
  type
  TNewClass = class(TMyClass)
  procedure One;override;
  end;
  
  procedure TMyclass.One;virtual;
  begin
  ShowMessage('調用了TMyclass的方法!');
  end;
  
  procedure TNewClass.One; override;
  begin
  Inherited;
  ShowMessage('調用了TNewClass的方法!');
  end;
  
可以看到,從TMyClass派生了一個新類TNewClass。這兩個類都聲明了一個名字相同的方法One。所不同的是,在TMyClass中,One方法后面多了一個Virtual關鍵字,表示這個方法是一個虛擬方法(Virtual Method)。而在TNewClass中,One方法后面多了一個Override關鍵字,表示該方法進行了重載(Override)。重載技術能夠實現許多特殊的功能。 讓我們來仔細分析它們的實現部分。在TMyclass.One方法的實現部分,調用ShowMessage過程彈出一個對話框,說明該方法已被調用;這里沒有任何特別的地方。在TNewClass.One方法中,出現了一條以前從未出現過的語句:
  Inherited;
  
這個詞的中文意思是“繼承”。我們暫時不要去涉及到太過複雜的OOP概念,只要知道這條語句的功能就是了。它的功能是調用基類中相當的虛擬方法中的代碼。例如,你如果使用以下代碼:
  var
  AObject: TNewClass;
  begin
  AObject := TNewClass.create;
  AObject.One;      AObject.free;
  end;
  
那麼程序將彈出兩次對話框,第一次是調用TMyclass類中的One方法,第二次才是TNewClass.One方法中的代碼。 重載技術使得我們不但可以在派生類中添加基類沒有的數據和方法,而且可以非常方便地繼承基類中原有方法的代碼,只需要簡單地加入Inherited就可以了。如果你不加入Inherited語句,那麼基類的相應方法將被新的方法覆蓋掉。但是必須注意,重載只有在基類的方法被標志為Virtual時才能進行,而且重載的方法必須具有和虛擬方法完全相同的參數類型。 虛擬方法還有一種特例,即抽象方法:
  procedure One;override;abstract;
  
在One方法后面,不但有override關鍵字,還多了一個abstract關鍵字(意為抽象)。這種方法稱為抽象方法(在C 中稱為純虛擬函數)。含有抽象方法的類稱為抽象類。抽象方法的獨特之處在于,它只有聲明,而根本沒有實現部分,如果你企圖調用一個對象的抽象方法,你將得到一個異常。只有當這個類的派生類重載並實現了該方法之后,它才能夠被調用。(在C 中,甚至根本就不能建立一個抽象類的實例。) 既然如此,那麼這種抽象方法又有什麼用呢?這個問題我們將在接下來的“多態”部分進行討論。 1.3 多態 多態相對來說比較複雜一點。不過不要擔心,它的內容比較少,而且如果以前的知識掌握得比較穩固的話,多態的概念是水到渠成的。 先來討論一下類型的兼容性問題。下面是一個例子:
  type
  TAnimal = Class
  Procedure Voice;virtual;
  ...
  end;
  
  TDog = Class(TAnimal)
  Procedure Voice;Override;
  ...
  end;
  
  implementation
  
  Procedure TAnimal.Voice;virtual;
  Begin
  PlaySound('Anim.wav',0,snd_Async);
  End;
  
  Procedure TDog.Voice;virtual;
  Begin
  PlaySound('Dog.wav',0,snd_Async);
  End;
  
TDog類繼承了TAnimal類,並重載了其中的Voice方法。PlaySound是一個WIN API函數,可以播放指定的wav文件。(這個函數的定義在MMSystem.pas文件中可以找到。) 先看這段代碼:
  var
  MyAnimal1, MyAnimal2: TAnimal;
  Begin
  MyAnimal1 := TAnimal.Create;
  MyAnimal2 := TDog.Create;
  ...
  
在實現部分的第一行中,建立了一個TAnimal類型的對象,並將其賦予TAnimal類型的變量MyAnimal1。這是很正常的事。但在第二行中,建立了一個TDog類型的對象,並將其賦予了TAnimal類型的變量MyAnimal2。這看上去令人吃驚,但這些代碼是完全合法的。 眾所周知,Pascal以及Object Pascal是一種類型定義嚴格的語言,你不能將某個類型的值賦予不同類型的變量,例如將一個整型值賦予布爾型變量,將會導致出錯。但是,這個規則在涉及到OOP領域時,出現了一個重要的例外,那就是:可以將一個子類的值賦予一個父類類型的變量。但倒過來卻是不行的,一個父類的值決不能賦予一個子類類型的變量。 如果將這個原則放到現實世界中,那就很容易理解了:“狗”繼承自“動物”,因為狗也是一種動物。所以可以將一個“狗”類型的值賦予“動物”類型的變量,因為“狗”具有“動物”的一切特征。但反過來,“動物”不具有“狗”的所有特征,因此反向賦值是不行的。 那麼,這種兼容規則在編程中究竟有什麼用處呢? 請注意下面這段代碼:
  var
  MyAnimal1, MyAnimal2: TAnimal;
  Begin
  MyAnimal1 := TAnimal.Create;
  MyAnimal2 := TDog.Create;
  MyAnimal1.Sound;
  MyAnimal2.Sound;
  ...
  
MyAnimal1和MyAnimal2都是TAnimal的變量,而且都調用了Sound方法。但是,執行的結果是完全不同的:前者執行的是TAnimal.Voice的代碼,而后者執行的是TDog.Voice的代碼!其原因很簡單,因為MyAnimal1被賦予了TAnimal類型的對象,而MyAnimal2被賦予了TDog類型的對象。也就是說,一個TAnimal類型的變量,當它調用Sound方法時,所執行的代碼是不確定的:可能執行TAnimal.Voice的代碼,也可能執行的是TDog.Voice的代碼,取決于它當時引用的是一個什麼樣的對象。 再看:
  MyAnimal1 := TAnimal.Create;
  MyAnimal1.Sound;
  MyAnimal1.free;
  MyAnimal1 := TDog.Create;
  MyAnimal1.Sound;
  ...
  
同一個變量MyAnimal1,在第一次調用Sound方法時,執行的是TAnimal.Voice的代碼,在第二次時執行的是TDog.Voice的代碼。MyAnimal1.Sound這行代碼不需要變化,程序可以根據不同的情況賦予該變量不同的對象,從而使它執行不同的代碼。這就是多態的定義。 這個非常重要的特點大大地增加了代碼的可複用性。如前所述,只需要簡單地寫下一行代碼,就可以讓程序執行不同的功能,因為這個虛擬方法同TAnimal的任何派生類都是兼容的,甚至連那些還沒有編寫出來的類也是一樣。而程序員並不需要了解這些派生類的細節。利用多態性寫出來代碼,還具有簡潔和維護性好的特點。 現在我們可以回到本文的1.2節結尾處的問題了。抽象方法本身不能夠做任何事情,必須在子類中被重載並實現,才能夠完成有意義的工作。但抽象方法的存在,相當于為父類留下了一個接口,當程序將一個子類的對象賦予父類的變量時,父類的變量就可以調用這個方法,當然此時它運行的是相應的子類中重載該方法的代碼。如果沒有這個抽象方法,父類的變量就不能調用它,因為它不能調用一個只在子類中存在、而在父類中不存在的方法! 關于OOP的介紹就到此這止。在以上這些篇幅里,介紹的只是OOP最基本的一些概念,讓讀者對OOP有一定的系統認識,也為下文的討論打好基礎 。更多、更深入的東西等待著你自己去發掘。 本文已經多次強調OOP的重要性,這里還要強調一次:對OOP的掌握程度,在某種意義上決定著你對Delphi世界的理解能力。 2、數據庫 在相對枯燥的理論之后,我們終于要開始接觸到一些比較激動人心的實際應用了。 數據庫編程是Delphi最強大的優勢之一,恐怕也很少有Delphi程序員沒有接觸過數據庫編程的。Delphi獨特的Data-Aware構件,讓很多初識Delphi的人為之目瞪口呆。不需要寫任何代碼,在幾分鐘之內就可以做出一個相當精巧的數據庫程序,而且在開發期就可以看到運行期的結果,這真是不可思議啊!但是,Delphi強大無比的數據庫開發能力,決不僅僅限于用幾個構件操縱一下DBF或是Access數據庫而已。你所看到只是冰山一角。讓我們仔細說來。 數據庫雖然家族龐大,但一般來說可以分為兩種:文件型數據庫和C/S型數據庫。下面分別討論。 2.1 文件型數據庫 所謂文件型數據庫,顧名思義,是基于文件的(file-based),數據被按照一定格式儲存在磁盤里,使用時由應用程序通過相應的驅動程序甚至直接對數據文件進行讀取 。也就是說,這種數據庫的訪問方式是被動式的,只要了解其文件格式,任何程序都可以直接讀取,這樣就使得它的安全性相當糟糕。同時,在蓬勃興起的網絡應用,文件型數據庫更是難有用武之地:效率低下,不支持很多SQL命令,不支持視圖、觸發器、存儲過程等高級功能,等等。這些特點決定了它不適合大型的工程。 最為大家所熟悉的文件型數據庫可能就是DBF(DBase/Foxbase/Foxpro)數據庫,在DOS時代風靡一時,相信很多人都有過抱著一本手冊苦背Foxbase命令的回憶吧!其特點是,每個Table或Index都是一個獨立的文件,使用相當簡單,性能還可以,安全性非常的差,但應用非常廣泛(主要是DOS時代遺留下來的,哪個單位沒有兩個用這種東東編出來的老古董程序呢?)。它在今天還能占有一席之地,其主要原因之一是,正因為簡單和使用廣泛,使得對它的訪問是最容易的,甚至根本無需第三方的接口,就可直接對其進行字節級的讀取 。 除此之外,還有著名的Access數據庫。這是MS Office里的構件之一,和DBF數據庫不同,所有的文件都被整合在一個.mdb文件中,這樣就避免了數據庫變大之后管理上帶來的麻煩。同時它還提供密碼保護功能,安全性比DBF數據庫要好很多。Access數據庫除了一般的文本數據之外,還擅長于對多媒體數據的處理,在對聲音、圖像乃至基于OLE的對象進行處理時,令DBF數據庫望塵莫及。隨著微軟戰略的步步勝利,Access數據庫也不斷發展,憑借著優秀的性能和與MS Office的無縫結合,早已超越DBase系列,成為現今最強大的文件型數據庫了。 Delphi中附帶的Paradox也是一種文件型數據庫。它是Inprise公司自己的產品。因此和Inprise的系列開發工具配合得很不錯。它支持密碼保護,支持標准的SQL,性能也還不錯,但是應用就不那麼廣泛了。和DBF數據庫一樣,它的每一個Table都是一個獨立的文件,因此也有同樣的管理問題。 上文說到可以對文件型數據庫直接讀取,但實際編程中很少有人這麼做。因為再簡單的數據庫其實也是相當複雜的,一步步分析它的格式,從底層實現所有的數據庫應用,如果都要程序員去寫的話,可能會把人累死。所以數據庫的開發商將這些訪問代碼封裝起來,向程序員開放,程序員只需要調用相應的接口就可以了。 以DBF為例,使用DBase/Foxbase/Foxpro系列開發工具,可以用它自己的語法開發出應用程序。其中對DBF文件的具體操作被封裝了。對于Access數據庫,微軟公布了一個DAO(Database Access Object),由一系列的DLL文件組成,封裝了對.mdb文件的訪問。使用VB的讀者可能對DAO比較熟悉,只要在VB中嵌入DAO對象,就可以非常方便地訪問Access數據庫了。ODBC(Open DataBase Connection,開放數據庫互連)也是一種封裝,用意在于向開發人員提供一個統一的接口,通過這個接口可以訪問任何支持ODBC的數據庫,只要該數據庫提供了相應的ODBC驅動。從這一點上來說,ODBC是一種更加高級的封裝。目前幾乎所有的主流的數據庫都能被ODBC所支持。打開你的Windows的控制面板,就可以看到ODBC的圖標。 用Delphi寫數據庫程序的人免不了要同BDE打交道。BDE(Borland Dasebase Engine,Borland數據庫引擎)是一個和ODBC類似的東西,Borland/Inprise本來企圖用它來統一數據庫接口。但后來Inprise在和微軟的戰爭中敗下陣來(ODBC是微軟搞出來的),它又不肯放棄BDE,而是將其捆綁在Delphi/C Builder系列開發工具中,結果好象變成這些開發工具的一種附屬品了。 用BDE開發數據庫程序相當容易。許多Delphi教科書在寫到數據庫開發這一章時,總是告訴你先在BDE中為某個DBF或Paradox數據庫設置一個別名,然后往窗體上放一個TTable構件,然后將其DatabaseName指向相應的別名……然后,這個數據庫中某個表的內容就在相應的Data-Aware構件中顯示出來了。但是它們具體是怎麼工作的呢? Delphi對數據庫進行訪問時,事實上通過了很多層次的連接。如下圖: 圖1 DataAware構件-DataSource構件-DataSet構件-BDE-數據庫 從這個圖可以看出,BDE負責與具體的數據庫打交道,而Dataset構件與BDE相連,DataSource構件與Dataset構件相連,最后才連接到顯示具體數據的Data-Aware構件。在Delphi的構件面板上,Data Access頁面中的構件一般屬于DataSet構件,例如TTable、TQuery,只要指定它們的DatabaseName屬性,就可以將它們與某個數據庫建立連接。在Data Control頁面中的構件一般是Data-Aware構件,例如TDBGrid,TDBEdit,TDBImage。它們的作用看上去與一般的Delphi構件相似,不同之處在于,可以通過一個DataSource構件作為中介,與DataSet構件相連,並自動顯示相應的數據。 用Delphi的數據庫構件建立一個應用程序是如此之方便,但是如果深入下去,會發現事情並不簡單。你可以嘗試自己編寫代碼,訪問數據庫中字段,而不是通過Data-Aware構件由用戶來編輯。如何做到這一點呢?秘密在于Field構件。 可以說,Field構件是Delphi數據庫應用程序的基礎 。當打開一個DataSet構件時,相應的數據會被讀取,並儲存在TTable或TQuery構件的Fields屬性中。這個屬性被定義為Field數組。通過直接訪問數組,可以使用它們,例如:
  Table1.Fields[0].AsInteger;
  
這段代碼訪問了Table1中當前記錄的第一個字段,該字段的類型為Integer。 也可以通過使用FieldbyName屬性來使用它們:
  Table1.FieldbyName('Last Name').AsString;
  
這段代碼訪問了Table1中當前記錄的名為Last Name的字段,該字段的類型為String。 事實上,Data-Aware構件就是通過訪問DataSet構件的Fields屬性來使用數據的。弄明白了這一點之后,你自己也可以嘗試改寫一個常規的顯示構件,使之具有Data-Aware的性質。其實,大多數使用Delphi的數據庫高手並不喜歡使用Data-Aware構件,因為Data-Aware構件遠不用常規的構件來得靈活。DataSet構件除了Fields屬性之外,還具有數目眾多的特殊屬性、方法和事件,足以應付從小型文本數據庫到大型網絡數據庫的所有應用。本文不擬一一討論它們,如果讀者能將它們的運用爛熟于心的話,可以說應付數據庫編程就不會有多大問題了。 請將注意力再次集中到圖1。在圖1的最后一環,可以看到BDE連接到了具體的數據庫。其實,在這一環中,也是有幾個層次的。理論上來說,BDE可以連接任何類型的數據庫。對于一些比較簡單的數據庫,例如ASCII(純文本型的數據庫)、dBase以及Delphi自己的Paradox,BDE可以直接訪問。另外它也可以通過一些相應的驅動,訪問特定的數據庫,例如通過DAO訪問Access數據庫。對于不能直接支持的數據庫,BDE還可以連接到ODBC,通過ODBC進行訪問,雖然這樣效率比較低。 這種性質決定了BDE是一個相當龐大的東西。使用了BDE的Delphi程序,必須有BDE才能工作,所以必須同BDE一起發布。這樣往往造成這樣一種情況:只有幾百K的應用程序,在將整個BDE加入之后,體積將近10M!這對于以輕薄短小為長的文件型數據庫,簡直是一個致命的弱點。而且由于BDE要兼容太多的數據庫,本身也有不穩定的毛病,往往出現令人頭疼的問題。同時,通過安裝程序安裝BDE驅動和設置數據庫別名也是一件很麻煩的事情,這一切使得BDE在Delphi程序員中很不受歡迎。在網上的Delphi技術論壇里,經常可以看到對BDE的一片咒罵之聲……那麼,有什麼辦法可以繞過BDE嗎? 有的。目前來說,至少有以下三種方法: (1) 使用第三方構件。 Inprise自己也很早就意識到了BDE的問題,雖然他們不肯放棄BDE,但是從Delphi3起,仍然對程序員提供了一個不錯的選擇:創建自定義的DataSet構件。Delphi的開發者們把所有有關BDE的東西從TDataSet類中移走,放入了新的TBDEDataSet類(TBDEDataSet類是TDataSet類的子類)。TDataSet類被重新構造,其核心功能被虛擬化。因此,你只需要從TDataSet類派生一個自己的新類,並重載一些指定的虛擬方法(用以訪問具體的數據庫),你就可以得到一個自己的DataSet構件。它與BDE完全無關,但可以象Delphi自己的DataSet構件一樣被使用,例如,訪問其Fields屬性,乃至與Delphi的Data-Aware構件一起工作! 于是出現了大量的第三方構件,它們可以訪問某種特定的數據庫。下面是一些比較常見的訪問文件型數據庫或ODBC的第三方構件: 表2 名稱 支持的數據庫類型 Diamond Access Halcyon DBase/Foxpro Apollo DBase/Foxpro mODBC 任何ODBC數據庫 ODBC Express 任何ODBC數據庫 這些控件被廣泛使用,在國內,就作者所知,財智家庭理財軟件使用了Diamond,而“追捕”(一個顯示指定IP的地址位置的共享軟件)使用了Halcyon。在使用這些第三方構件之后,軟件終于可以“輕裝上陣”,再也不用為BDE頭疼了。 (2) 使用ADO。 在Delphi5中,Inprise終于提供了一個比較徹底的解決方法,那就是ADO構件。從原理上來說,ADO與上述的第三方構件並無多大區別,只是它是Inprise官方開發的;同時,它連接的不是某個具體的數據庫,而是微軟提供的ADO對象。 ADO(ActiveX Data Object,ActiveX數據對象)是微軟提出的新標准,從理論上來,能夠支持任何類型的數據庫(甚至包括流式數據)。微軟力圖將它樹為新的統一數據庫接口,吹噓了它的許多優點。Inprise一直是微軟不共戴天的競爭對手,對微軟的標准嗤之以鼻(BDE即是一例),但是由于種種原因,Inprise終于承認了ADO。平心而論,用ADO來取代BDE的確是一個不錯的解決方案,而且在Delphi中使用ADO也相當方便。從形勢看,ADO應該是未來的方向。但是,ADO本身也是相當大的。 (3) 從最底層開發一個完整的數據庫引擎。 這是最徹底的辦法。徹底拋棄Delphi的數據庫支持,從字節開始,開發自己的數據庫。這種方法有其好處:第一,不用考慮兼容性問題,例如不用去考慮用戶的數據庫文件是Access 97格式還是Access 2000格式的;第二,可以在性能上達到最充分的優化,因為不需要通過任何通用接口,而是直接對磁盤文件進行操作,這對于一些對性能要求苛刻的程序是很有用的;第三,能夠最大限度地減少冗余代碼,因為這種數據庫往往是特定格式的,而且只需要執行一些特定的操作,訪問代碼當然要比通用數據庫精簡得多。但這種方法的負面問題也顯而易見,那就是龐大的工作量。再簡單的數據庫也是相當複雜的,從最底層實現一個完整的數據庫引擎,往往需要幾千行代碼,以及耐心和經驗。 雖然聽起來有些極端,但這樣做的也不乏其人。著名的Foxmail就是使用了自定義的數據庫格式來儲存信件、地址本等有關信息。另一個共享軟件“電子書庫”也使用了自定義的.srm格式。作者開發的iCompanion(網絡伴侶)也是使用自定義格式來儲存網絡記錄的。 限于篇幅,這里就不再對具體的程序進行詳細的分析了。要補充的一點是,作者曾使用Diamond開發過Rich Explorer,這是一個專門用于瀏覽著名的大富翁論壇的離線數據庫(Access格式)的閱讀器。在作者的主頁上,可以找到Rich Explorer的全部源代碼,它完整地展示了一個使用第三方構件訪問特定數據庫的程序(沒有使用Data-Aware控件),代碼也比較簡單,適合于初學者分析,有心的讀者不妨作為參考。 2.2 C/S型數據庫 C/S(Client/Server,客戶機/服務器)型數據庫是當前數據庫應用的主流。 與文件型數據庫不同的是,C/S型數據庫應用程序由兩個部分組成:服務器和客戶機。服務器指數據庫管理系統(Database Manage System,DBMS),用于描述、管理和維護數據庫的程序系統,是數據庫系統核心組成部分,對數據庫進行統一的管理和控制。客戶機則將用戶的需求送交到服務器,再從服務器返回數據給用戶。 C/S型數據庫非常適合于網絡應用,可以同時被多個用戶所訪問,並賦予不同的用戶以不同的安全權限。C/S型數據庫支持的數據量一般比文件型數據庫大得多,還支持分布式的數據庫(即同一數據庫的數據庫位于多台服務器上)。同時,C/S型數據庫一般都能完善地支持SQL語言(所以也被稱作SQL數據庫)。這些特性決定了C/S型數據庫適合于高端應用。 常見的C/S型數據庫有著名的Oracle, Sybase, Informix, 微軟的Microsoft SQL server, IEM的DB2,以及Delphi自帶的InterBase,等等。 C/S型數據庫涉及到非常多的高級特性,是Delphi中,也是整個計算機領域中最s
adonis
高階會員


發表:140
回覆:258
積分:159
註冊:2002-04-15

發送簡訊給我
#2 引用回覆 回覆 發表時間:2002-08-16 15:09:08 IP:163.15.xxx.xxx 未訂閱
謝謝您的用心。
------
我也在努力學習中,若有錯謬請見諒。
系統時間:2024-05-16 9:07:09
聯絡我們 | Delphi K.Top討論版
本站聲明
1. 本論壇為無營利行為之開放平台,所有文章都是由網友自行張貼,如牽涉到法律糾紛一切與本站無關。
2. 假如網友發表之內容涉及侵權,而損及您的利益,請立即通知版主刪除。
3. 請勿批評中華民國元首及政府或批評各政黨,是藍是綠本站無權干涉,但這裡不是政治性論壇!