所謂單例就是系統中只能存在某個類的一個例項,在現實中只能存在一個例項的物件是很常見的,比如系統配置物件只能有一個,
比如一個客戶端同伺服器的TCP/IP連線經常只允許有一個連線等等。下面是一個單例模式的UML圖:
單例模式的實現
那麼如何保證在系統中每時每刻只有一個類的例項存在呢,這可以通過靜態變數來實現,在呼叫GetInstance時判斷靜態變數是否為nil,
如果為nil表示系統中沒有類的例項,則構造物件,同時將類例項賦值給靜態變數,如果不為nil,則直接返回靜態變數對應的類的例項。
Java或者C++中,類中的變數可以修飾為Static表示該變數不依賴於類的例項而單獨存在,在Delphi中沒有類似的關鍵字,所以只能是
定義一個單元的私有變數來實現對類例項的引用計數。
同時,另外在Java中為了防止使用者使用類的建構函式來建立多個類的例項,需要將建構函式的存取屬性改為private,但是在Delphi中,
編譯器對建構函式的保護級別進行了特殊的處理,即便將Contructor方法設定為private存取許可權,編譯器仍然會將Contructor的保護級
別修正為public,因此將調整建構函式的保護級別防止多例的產生在Delphi中是行不通的。幸好TObject基類中定義了NewInstance方法,
這個方式是一個類方法,通過編譯器魔法系統在每次構造物件時都會呼叫這個類方法,那麼通過過載這個靜態方法,就可以實現對建構函式的控制了。
下面就是一個單例的配置類示意程式碼:
TSingleConfig=class(TObject)
private
FConfigPath: string;
procedure SetConfigPath(const Value: string);
public
class function GetInstance():TSingleConfig;
//系統配置路徑
property ConfigPath:string read FConfigPath write SetConfigPath;
//...省略
class function NewInstance: TObject; override;
procedure FreeInstance;override;
end;
implementation
var
GlobalConfig:TSingleConfig=nil;//單元內私有的靜態配置物件變數
{ TSingleConfig }
procedure TSingleConfig.FreeInstance;
begin
inherited;
GlobalConfig:=nil;
end;
class function TSingleConfig.GetInstance: TSingleConfig;
begin
if not Assigned(GlobalConfig) then
GlobalConfig:=TsingleConfig.Create();
Result:=GlobalConfig;
end;
class function TSingleConfig.NewInstance: TObject;
begin
if not Assigned(GlobalConfig) then
GlobalConfig:=TSingleConfig(inherited NewInstance);
Result:=GlobalConfig;
end;
procedure TSingleConfig.SetConfigPath(const Value: string);
begin
FConfigPath := Value;
end;
在靜態的類方法GetInstance
中,通過對靜態變數
GlobalConfig
對應的物件進行判斷來首先判斷
GlobalConfig
變數是否為
nil
,如果為nil
,則表明系統中還沒有初始化物件的例項,這時呼叫私有的建構函式來初始化物件,並將其賦值給
GlobalConfig
變數,如果不為
nil
,則返回已經建立的
GlobalConfig
物件。通過
GlobalConfig
的靜態變數,就可以保證物件的例項始終只有
一個(注意的是:GlobalConfig
物件需要宣告在
Implementation
部分,而不要宣告在單元
Interface
部分,這樣變數對單元外
的使用者是不可見的,這樣可以保護變數不會被使用者誤修改)。
此外,使用者可能會在使用完物件後,將其釋放,在
Delphi
中,一個物件被釋放後,它的例項對應的變數並不會自動設定為
nil
,如果之後使用者再次呼叫GetInstance
獲得全域性物件時,雖然物件已經被銷燬了,但是
Assigned(GlobalConfig)
仍然返回為真,
那麼GetInstance
就返回一個錯誤的指標,導致
AV
錯誤。為了避免這種情況,可以過載
FreeInstance
方法,該方法在物件被釋
放時總是會被呼叫的,在FreeInstance
方法中釋放物件後將
GlobalConfig
重新設定為
nil
就可以了。
VCL
中的單例
在
VCL
中也有很多的單例,比如剪貼簿類
TClipboard
類,在
Clipbrd.pas
單元中提供了類似於上面的例項控制技術,不過它是
通過函式Clipboard
來返回剪貼簿的唯一例項的,
function Clipboard: TClipboard;
begin
if FClipboard = nil then
FClipboard := TClipboard.Create;
Result := FClipboard;
end;
同樣的,它也在類的解構函式中將靜態變數設定為
nil
destructor TClipboard.Destroy;
begin
if (FClipboard = Self) then
FClipboard := nil;
inherited Destroy;
end;
不過同前面的過載
NewInstance
的方法相比,
VCL
中方法缺陷就是不能防止使用者多次建立使用者的例項,下面程式碼分別
呼叫TClipboard
類和
TSingleConfig
類的
Create
方法兩次,然後比較兩次呼叫後獲得的例項的記憶體地址,判斷類是
被建立了幾次:
procedure TForm1.btn1Click(Sender: TObject);
var
P1:Pointer;
P2:Pointer;
begin
P1:=TClipboard.Create();
P2:=Clipboard;
ShowMessage(IntToStr(Integer(p1)));
ShowMessage(IntToStr(Integer(p2)));
end;
procedure TForm1.btn2Click(Sender: TObject);
var
P1:Pointer;
P2:Pointer;
begin
P1:=TSingleConfig.Create();
P2:=TSingleConfig.Create();
ShowMessage(IntToStr(Integer(p1)));
ShowMessage(IntToStr(Integer(p2)));
end;
從執行結果可以知道
TClipboard
類的建構函式兩次呼叫後返回的地址不同,而
TSingleConfig
返回的地址則相同。
可以看出過載NewInstance
的方法更加嚴謹,不及出錯。