如何深入Delphi編程

  
 如何深入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;
  
  
  在這個類中,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中,也是整個計算機領域中最大的應用之一。由於本期附錄中已有專文討論,本文就不擬詳細介紹了。
  隨着技術的不斷更新,C/S型的結構也開始逐漸被多層(Multi-Tiered)數據庫模型所取代。
  上面說到,C/S型數據庫程序由服務器和客戶機兩個部分組成,因此被稱爲雙層(two-tiered)模型。文件型數據庫程序則被稱爲單層(single-tiered)模型。單層模型是最原始的數據庫模型。後來程序員們將數據庫轉移到一個強大的中央服務器上,讓它爲多個功能較弱的客戶機提供服務,這樣雙層模型出現了。雙層模型在金融、電力、通信等領域被廣泛使用,極大地推動了網絡數據庫的發展。但是,雙層模型也逐漸暴露出其不足的一面。在這種情況下,出現了三層模型:應用程序中的數據模塊部分被分離出來,轉移到一個單獨的服務器上,成爲獨立的一層。三層和三層以上的模型,統稱爲多層模型。
  ` 簡言之,三層模型由以下三個層次組成:
  
  客戶機-應用程序服務器-數據庫服務器
  
  用戶的請求首先通過客戶機嚮應用程序服務器發出,應用程序服務器再向數據庫服務器發出具體的數據訪問命令(一般是SQL),數據庫服務器返回的數據被應用程序服務器重新組織之後返回給客戶機。
  可以看出,三層模型是雙層模型的擴展。目前我們無需瞭解三層模型的所有技術細節,以及它對於雙層模型的優勢,只需要大致理解這個模型的結構就可以了。
  B/S模型無疑是當前最爲流行的多層數據庫模型之一。也許你已經聽說過B/S這個名詞,它是Brower/Server(瀏覽器/服務器)的縮寫。Brower是指IE/Netscape這樣的瀏覽器,Server包括數據庫服務器和應用程序服務器。用戶通過瀏覽器發出某個請求,通過應用程序服務器-數據庫服務器之間一系列複雜的操作之後,返回相應的Html頁面給瀏覽器。
  是不是覺得十分熟悉?對了,其實這就是大家再熟悉不過的Internet上的WEB數據庫,當然它也可以用於局域網。它實際上可以說是一種最常見的多層模型。
  在對數據庫的發展進行回顧之後,我們終於趕上了最流行的步伐。但是,也許有人在這裏卻感到泄氣了。因爲他聽說現在的WEB數據庫編程,是Perl、ASP、PHP、JAVA這些語言的天下。難道我們一直忠實追隨的Delphi,在面對當代最流行的Web數據庫的時候,竟然面臨淘汰的命運?
  不,不是的。其實Delphi對Web數據庫的開發提供了非常良好的支持,特別是依據強大的數據庫支持和可視化編視的特點,它有着其他語言不可比擬的優勢。
  下面的內容將集中於用Delphi開發Web數據庫。
  首先要從Web本身講起。平時我們瀏覽的Web頁面,一般可以分爲兩種。一種是靜態頁面,這種頁面是文本格式的html文件。但是,要響應用戶的不同需求,從而反饋給用戶不同的頁面,就必須使用動態頁面技術了。例如,根據用戶輸入的名字,迅速在數據庫中查找到相應的數據,並動態生成一個頁面返回給用戶。
  怎樣實現動態頁面技術呢?最早的一種方法是CGI(Common Gateway Interface,通用網關接口)。這種接口允許瀏覽器發送和接收數據,並且基於應用程序的命令行進行輸入和輸出。當瀏覽器發出指向應用程序的請求時,Web服務器會自動啓動該程序,並在瀏覽器和應用程序之間傳遞輸入和輸出的數據。實現CGI的語言有很多種,其中比較流行的是Perl。另外,還有一種特殊的WinCGI。WinCGI與普通CGI的區別是,它通過INI文件來代替命令行參數作爲輸入輸出,這主要是爲Visual Basic設計的,因爲VB不能訪問環境變量。Delphi對這兩種CGI都提供了很好的支持,可以編寫出非常複雜的CGI程序。
  第二種方法是使用Web服務器內置的API接口。用這種接口編寫出來的Dll文件,被Web服務器裝載到自己的內存空間中,當服務器接收到相應的頁面請求時,它將啓動一個新的線程來執行Dll中的相應代碼。由於不需要執行外部的EXE文件,這種程序的速度非常快。這種API主要有ISAPI(Internet Server API)和NSAPI(Netscape Server API),其中前者已經成爲事實上的標準。Delphi對這兩種API也提供了很好的支持。提得一提的是,Delphi的C/S版提供了一個通用的類框架,消除了CGI、WinCGI和ISAPI之間的區別,這樣,我們可以輕鬆地將一個應用程序在這三者之間轉換。
  還有一種是Web服務器內置的腳本語言,可以被簡單地嵌入html文件,通過Web服務器的解釋執行來產生動態頁面。著名的ASP、PHP、JSP都屬於此類。這看起來和Delphi沒有什麼關係,但事實上,Delphi也能提供對ASP的強力支持!ASP的初學者可能會把注意力都集中在它的腳本語言上,其實ASP是由三部分組成的:腳本、組件和html。光是掌握腳本和ASP原有的組件特性是遠遠不夠的,只有自行開發組件,才能實現真正複雜、高效的應用。Delphi就非常適合開發這種組件。國內著名大富翁論壇,就是基於Delphi+ASP的成功例子,同時它還公佈了所有的源代碼,可供借鑑。
  讓我們先建立一個程序。這個程序和以往的Delphi不同,要求刪除所有的窗體和多餘的代碼,最後只剩下這麼一段:
  
  program CgiDate;
  
  {$APPTYPE CONSOLE} //這行編譯器指令表示這是一個控制檯程序,不需要窗體,在終端窗口中輸出
  
  uses
  sysutils;
  
  begin
  writeln('HTTP/1.0 200 OK');
  writeln('CONTENT-TYPE: TEXT/HTML');
  writeln;
  writeln('<html> <head>');
  writeln(' <title>Time</title>');
  writeln('</head><body>');
  writeln('<h1>Time at this site</h1>');
  writeln('<hr>');
  writeln('<h3>');
  writeln(formatdatatime('"Today is " dddd,mmmm d,yyyy,' + '"<br> and the time is "hh:mm:ss AM/PM',now));
  writeln('<h3>');
  writeln('<hr>');
  writeln('</body></html>');
  end;
  
  編譯後,將該程序置於Web服務器下的有scripts權限的子目錄中,並用瀏覽器訪問它,就可以得到一個關於當前時間和日期的頁面。
  分析一下這段代碼。格式很簡單,都是用writeln生成標準輸出。首先的兩行生成html的描述,中間空一行,接下來的代碼生成了一個完整的html文件。這個文件被返回給瀏覽器並顯示出來。與靜態頁面不同是,有一行html文本是通過formatdatatime函數動態生成的,因此根據當前時間的不同會有不同的顯示。
  這就是一個最簡單的CGI程序。雖然簡單,但已經可以看出CGI程序的基本工作原理:通過標準輸入輸出產生動態的html頁面。
  在這個基礎之上,可以方便地實現對數據庫的訪問,並生成相應的html。
  下面是一個例子:
  
  var:
  Table1:TTable;
  Begin
  Showheader; // Showheader過程生成html文件的頭部,代碼與上例相似
  Table1 := TTable.create(nil);
  Try
  Table1.Databasename := 'DBDEMOS';
  Table1.tablename := 'Employee.db';
  Table1.indexname := 'Byname';
  Table1.open;
  ShowTabindex; //顯示錶中的列
  Finally
  Table1.close;
  Table1.free;
  End;
  Writeln('</body></html>');
  End;
  
  這段代碼動態建立了一個Table對象,並將它與DBDEMOS數據庫中的表Employee.db相連(當然這些必須先在BDE中設置)。ShowTabindex過程顯示了該表中的兩個列:
  
  procedure ShowTabindex;
  begin
  table1.frist;
  writeln('<ul>');
  while not table1.eof do
  begin
  writeln(format('<li>s%s%',[table1.fieldbyname('FirstName').AsString, table1.fieldbyname('LastName').AsString]);
  Table1.Next;
  End;
  Writeln('</ul>');
  End;
  
  ShowTabindex函數遍歷了整個表,並將所有的FirstName和LastName字段打印出來。
  在此基礎之上,可以產生複雜的動態頁面,實現對數據庫的各種操作。當然,實踐中的問題決不僅僅這麼簡單,例如,讀取有關的環境變量,要使用特殊的GetEnvironmentVariable變量,生成相應的URL,需要相應的http知識,產生供用戶提交請求的頁面,需要了解html表單的格式……但是,本文的目的不在於探討技術細節,而是着重於基本概念和原理的瞭解,因此不再詳述,請讀者自己深入學習。
  上面已經說過,在Delphi中可以輕鬆地將一個應用程序在CGI、WinCGI和ISAPI之間轉換。上面這兩個例子生成的是EXE文件,因此是CGI(或WinCGI,取決於使用環境變量還是INI文件)程序。如果要改成ISAPI方式,除了要編譯成DLL文件之外,輸入輸出的部分也需要進行修改,實現三個特殊的函數:
  GetExtensionversion
  TerminateExtension
  HttpExtensionProc
  Web服務器在裝載DLL、卸載DLL和請求DLL時會分別調用這三個函數,程序可以通過它們與瀏覽器進行交互。具體的實現,這裏也不詳述了。
  
  
  通過以上幾個專題的學習,相信讀者對Delphi的瞭解更深了一層。本文旨在幫助讀者理解一些相對比較高級的概念和原理,很多地方只是介紹了最基礎的東西,沒有展開。如果深入下去,將會產生很多章龐大的內容。因此,讀者在入門之後,需要更多的自己的努力。
  
 還有就是CSDN,Delphibbs是個學習的好地方,其中Delphibbs的離線包能幫助你解決Delphi的很多問題。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章