Delphi中兩個BUG的分析與修復 (轉)

amyz發表於2007-08-15
Delphi中兩個BUG的分析與修復 (轉)[@more@]

中兩個的分析與修復

  在使用Delphi 7進行三層開發時,遇到了兩個小問題,透過反覆試驗,終於找出了Delphi 7中的兩個小BUG並進行了修復(好像Delphi 6中也有相同的BUG),撰寫此文與大家一起分享成功的喜悅。我也是初學Delphi,文中一定存在不少說的不對的地方,還請各位朋友多多指正。

  BUG1.傳參時中文被截斷的問題:

  BUG再現的方法:

  後臺用 SERVER 2000,裡面有一個XsHeTong表用於試驗,您可以根據您的實際情況進行調整。

  先建立一個資料:新建專案,建立一個資料模組,上面放置ADOConnection、ADODataSet、DataSetProvr各一,並做好相應設定,其中ADODataSet的ComamndText留空,並把它的Option中的poAllowCommandText設定為True。編譯執行。

  再建立客戶端:新建專案,在窗體上放置DCOMConnection,連上前面上建立的資料伺服器,再放置一個ClientDataSet,把它的連線設成這裡的DCOMConnection,並設定它的ProviderName為上面的伺服器上的DataSetProvider的名字。最後放置Data和Grid各一併作相應設定用於檢視結果,再放置一Button用於測試。

  在Button的OnClick中寫下類似於下面的程式碼(這裡我用了XsHeTong的表和它的兩個欄位HTH(char 15)、GCMC(varchar 100),您可以根據你的實際測試情況進行調整):

  with ClientDataSet1 do
  begin
   Close;
   CommandText := 'Insert Into XsHeTong(HTH, GCMC) values(:HTH,:GCMC)';
   Params[0].AsString := '12345';
   Params[1].AsString := '會截斷的中文字';
   Execute;
   Close;
   CommandText := ' * from XsHeTong';
   Open;
  end;

  執行程式,點選按鈕,看到記錄被插入了,可惜結果並不正確,“會截斷的中文字”變成了“會截斷”,但沒有中文的“12345”倒是正確的插入了。

  BUG分析與修復:

  為了對照起見,我試著直接用一個ADOConnection和ADOCommand、ADOTable進行C/S構架測試,結果是正確的,中文字不會被切斷。這說明了此BUG只在三層構架上出現。

   用事件探查器探查提交到SQL Server上執行的語句,發現兩層構架與三層構架的情況有以下不同:

  兩層構架:
  exec sp_executesql N'Insert into XsHeTong(HTH, GCMC) values(@P1,@P2)', N'@P1 varchar(15),@P2 varchar(100)', '12345', '會截斷的中文字'

  三層構架:
  exec sp_executesql N'Insert into XsHeTong(HTH, GCMC) values(@P1,@P2)', N'@P1 varchar(5),@P2 varchar(7)', '12345', '會截斷

  顯然,兩層構架時,引數的長度是按實際庫結構傳的,三層構架時,引數長度是按實際引數的字串長度傳的,而實際字串長度又似乎是算錯了,沒有把一箇中文當兩個字元長度處理。

  沒有辦法只好進行跟蹤,為了除錯Delphi的VCL庫,需要在工程選項的“Compiler Options”中選上“Use Debug DCUs”。

  先跟蹤客戶端程式,ClientDataSet1.Execute後,先後經歷了TCustomClientDataSet.Exectue、TCustomeClientDataSet.PackageParams、TCustomClientDataSet.DoExecute等一系列,一直到AppServer.AS_Execute(ProviderName, CommandText, Params, OwnerData); 把請求提交到伺服器均沒有什麼異常情況,看來問題出在伺服器端。

  對伺服器進行跟蹤,反覆試驗後,我把重點落在了TCustomADODataSet.PSSetCommandText函式身上,經過反覆細緻的跟蹤,目標越來越精確:TCustomADODataSet.PSSetParams、TParameter.Assign、TParameter.SetValue、VarDataSize。終於找到了BUG的源頭:VarDataSize函式,下面是它的程式碼:

  function VarDataSize(const Value: OleVariant): Integer;
  begin
   if VarIsNull(Value) then
    Result := -1
   else if VarIsArray(Value) then
    Result := VarArrayHighBound(Value, 1) + 1
   else if TVarData(Value).VType = varOleStr then
    begin
     Result := Length(PWideString(@TVarData(Value).VOleStr)^); //出問題的行
     if Result = 0 then
      Result := -1;
    end
   else
    Result := SizeOf(OleVariant);
  end;

  就是在這個函式中計算實參的長度的,它把Value中的值取出地址,並把它作為一個WideString的指標去求字串長度,結果就導致了“會截斷的中文字”這個字串的長度變成了7,而不是14。

  問題找到了,解決起來也就不困難了,只要簡單的把
  Result := Length(PWideString(@TVarData(Value).VOleStr)^); //出問題的行
  改成
  Result := Length(PAnsiString(@TVarData(Value).VOleStr)^); //沒問題了
  就可以了。

  但是這樣就會導致求英文字串的長度時長度被加倍了,所以也可以把這一行改成:
  Result := Length(Value);

  這樣,不管是中文還是英文還是中英混合的字串就都可求得正確的長度了。這就我至今仍百思不解的問題,為什麼Borland要繞個圈子透過指標去求引數值的長度呢?哪位朋友知道的話還請給我解釋一下,非常感謝!

  有些朋友可能會有疑問,為什麼在不透過三層構架來做的時候不產生這個字串被截斷的問題呢?答案並不複雜,在直接透過ADOCommand來向SQL Server傳送命令時,它是按表結構來決定引數長度的。它會先向SQL Server發一條

  SET FMTONLY ON select HTH,GCMC from XsHeTong SET FMTONLY OFF

  來獲取表結構。而在三層構架下,TCustomADODataSet內部雖然也是用TADOCommand來發命令,但它卻在取得表結構的後,並不用這個值來作為傳參長度,而是重新去按實際引數來計算長度,結果就導致了錯誤。

 

  BUG2.ClientDataSet的Lookup欄位的問題:

  BUG再現的方法:

  新建工程,在上面放置兩個ClientDataSet,分別為cds1和cds2,它的資料來源任意,其中cds1為主資料集,在裡面增加一個新的Lookup欄位,這個Lookup欄位根據cds1中的一個字元型的欄位值到cds2中找出對應值來。

  執行程式,一般來說是正常的,但是一旦cds1的被Lookup欄位中的值出現了一個單引號"'"(您可以修改或新增一條記錄,輸入單引號試試),立即會導致出錯: Untenated string constant(未結束的字串常量)。

  BUG分析與修復:

  這個BUG的產生原因要比上一個明顯得多,一定是沒有正確處理單引號帶來的副作用引起的。

  同樣的,我們來跟蹤VCL的原始碼:

  執行程式,出錯時開啟Call Stack視窗(在View->Debug )選單中,檢視函式情況,前面的一些呼叫是顯而易見的,沒有問題,我們從跟Lookup有關的地方開始查原因,第一個與Lookup有關的函式呼叫是TField.CalcLookupValue,我們在這個函式中設定斷點,重新執行程式,中斷下來後,進行單步除錯。

  TCustomClientDataSet.Lookup->TCustomClientDataSet.LocateRecord

  經過上面的幾次函式呼叫,很快的,我們就把目標定在了LocateRecord過程中,在這個過程中,它根據Lookup欄位的設定情況,生成相應的過濾條件,然後到目標資料集中把對應的值找到,錯就錯在過濾條件的生成上了。比如,我們要按cds1中Cust欄位(假設是001)的值到cds2中按CustID欄位值找到對應的CustName欄位值。那生成的條件就應該是[CustID] = '001',但如果Cust的值是aa'bb,按生成的條件就會變成[CustID] = 'aa'bb',顯然導致了一個未結束的字串常量。

  通常我們解決單引號中又出現單引號的情況,只需把引號中的引號寫兩就行了,這裡也是一樣,只要讓生成的條件變成[CustID] = 'aa''bb'就不會出錯了。所以可以這樣修改:

  在LocateRecord過程中找到下面的程式碼:

  ftString, ftFixedChar, ftWideString, ftGUID
   if (i = Fields.Count - 1) and (loPartialKey in Options) then
    ValStr := Format('''%s*''',[VarToStr(Value)]) else
    ValStr := Format('''%s''',[VarToStr(Value)]);

  改成:

  ftString, ftFixedChar, ftWideString, ftGUID:
   if (i = Fields.Count - 1) and (loPartialKey in Options) then
    ValStr := Format('''%s*''',[ StringReplace(VarToStr(Value),'''','''''',[rfReplaceAll])])
   else
    ValStr := Format('''%s''',[ StringReplace(VarToStr(Value),'''','''''',[rfReplaceAll])]);

  也就是在生成過濾條件字串時把條件的過濾值中的單引號全部一個變兩。

  為了確保這樣修改的正確性,我檢視了TCustomADODataSet中的對應的LocateRecord過程(在用TADODataSet中的Lookup欄位時不會因單引號出錯,只在用TCustomClientDataSet時有這樣的情況),它的處理方法與TCustomClientDataSet稍有不同,它是透過GetFilterStr函式來構造過濾條件的,但在GetFilterStr中,它正確處理了單引號的問題。所以這樣來看,沒有在TCustomClientDataSet的LocateRecord中正確處理單引號的問題,確實是Borland一個不大不小的疏漏。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752019/viewspace-958233/,如需轉載,請註明出處,否則將追究法律責任。

相關文章