程式碼的樂趣2-好玩又要跑的快

程式語言好玩的地方就是會不斷的進化, Delphi雖然這幾年在語言本身改變的比較慢, 但也是有進步並且吸收其他語言的特點並融入Pascal原本較正式的語法結構中, 這次讓我們討論一下目前比較流行的鏈式寫法.

所謂鏈式寫法的意思是指藉由物件導向語言可回傳本身(Self/this)而能夠在一行程式碼中不斷的呼叫方法或是存取特性完成工作, 而不需要像以往要拆分成多行程式碼的寫法, 也許讓我們用一個例子來說明會讓您更容易瞭解. 由於我個人認為鏈式寫法特別適合使用在結構資料和像Pascal語言這種有嚴謹語法架構的應用, 因此就讓我們使用JSON來說明.

假設我們想產生如下的簡易JSON資料, 其中只有JSON物件, JSON陣列和JSON Pair架構形成的資料:

{
     "版本": {
         "RAD Studio 10": "Seattle",
         "功能": [
             "支援大型記憶體模式",
             "支援Clang 32編譯器"
         ]
     },
     "版本": {
         "RAD Studio 10.2": "Tokyo",
         "功能": [
             "Linux 64 (Delphi)",
             "Android原生元件"
         ]
     }
 }

那麼在Delphi中有好幾種方法可以完成這個工作, 在本文中就讓我們使用TJSONObjectBuilder類別.下面就是使用TJSONObjectBuilder來產生如上資料的程式碼:

procedure TForm12.btnJSON1Click(Sender: TObject);
 var
   AWriter : TJsonTextWriter;
   sw : TStringWriter;
   jObjBuilder : TJSONObjectBuilder;
   jPair : TJSONCollectionBuilder.TPairs;
   jElement : TJSONCollectionBuilder.TElements;
 begin
   sw := TStringWriter.Create;
   AWriter := TJsonTextWriter.Create(sw);
   AWriter.Formatting := TJsonFormatting.Indented;
   jObjBuilder := TJSONObjectBuilder.Create(AWriter);
   try
     jObjBuilder.BeginObject;
     jObjBuilder.ParentObject.BeginObject('版本'); 
     jObjBuilder.ParentObject.Add('RAD Studio 10', 'Seattle'); 
     jObjBuilder.ParentObject.BeginArray('功能'); 
     jObjBuilder.ParentArray.Add('支援大型記憶體模式'); 
     jObjBuilder.ParentArray.Add('支援Clang 32編譯器'); 
     jObjBuilder.ParentArray.EndArray; 
     jObjBuilder.ParentObject.EndObject; 
     
     jObjBuilder.ParentObject.BeginObject('版本'); 
     jObjBuilder.ParentObject.Add('RAD Studio 10.2', 'Tokyo'); 
     jObjBuilder.ParentObject.BeginArray('功能'); 
     jObjBuilder.ParentArray.Add('Linux 64 (Delphi)'); 
     jObjBuilder.ParentArray.Add('Android原生元件'); 
     jObjBuilder.ParentArray.EndArray; 
     jObjBuilder.ParentObject.EndObject; 
     jObjBuilder.ParentObject.EndObject; 
     
     mmResult.Text := sw.ToString;
 finally
     jObjBuilder.Free;
     AWriter.Free;
     sw.Free;
   end;
 end;

上面的寫法是正確而且正式的寫法, 我想大多數程式師也是使用這樣的寫法, 即使Delphi的函式庫本身也是這樣寫, 所以沒什麼問題, 但我們可以使用鏈式寫法來改善它, 讓它看起來更具階層架構, 更像JSON本身的資料架構.

仔細看看上的程式碼,

  1. 它先在最外層建立一個JSON物件,
  2. 再於其中為每版Delphi建立一個版本JSON物件,
  3. 在版本JSON物件中再建立JSON Pair,
  4. 建立JSON陣列,
  5. 最後於JSON陣列中建立功能元素.

如果我們檢查上面jObjBuilder.BeginObject方法原型, 可以看到如下的程式碼:

function TJSONObjectBuilder.BeginObject: TJSONCollectionBuilder.TPairs;

BeginObject就回傳TPairs物件, 那我們不是剛好可以使用此回傳TPairs物件完成上面3的步驟嗎?

而TPairs的Add方法又回傳本身的TPairs:

function TJSONCollectionBuilder.TPairs.Add(const AKey, AValue: string): TPairs;

因此我們可以呼叫TPairs的BeginArray方法完成上面4的步驟, 而BeginArray方法又回傳TElements物件, 又剛好可以使用來完成上面5的步驟:

function TJSONCollectionBuilder.TPairs.BeginArray(const AKey: string): TElements;

由於TJSONCollectionBuilder這種使用階層又回傳本身物件的設計方法, 因此我們可以把上面正確但是有點繁瑣的寫法改寫成如下的語法:

001    procedure TForm12.Button3Click(Sender: TObject);
 002    var
 003      AWriter : TJsonTextWriter;
 004      sw : TStringWriter;
 005      jObjBuilder : TJSONObjectBuilder;
 006      jPair : TJSONCollectionBuilder.TPairs;
 007      jElement : TJSONCollectionBuilder.TElements;
 008    begin
 009      sw := TStringWriter.Create;
 010      AWriter := TJsonTextWriter.Create(sw);
 011      AWriter.Formatting := TJsonFormatting.Indented;
 012      jObjBuilder := TJSONObjectBuilder.Create(AWriter);
 013      try
 014        jObjBuilder.BeginObject
 015    
 016          .BeginObject('版本')
 017            .Add('RAD Studio 10', 'Seattle')
 018              .BeginArray('功能')
 019                .Add('支援大型記憶體模式’)
 020                .Add('支援Clang 32編譯器’)
 021              .EndArray
 022          .EndObject
 023    
 024          .BeginObject('版本')
 025            .Add('RAD Studio 10.2', 'Tokyo')
 026            .BeginArray('功能')
 027              .Add('Linux 64 (Delphi)')
 028              .Add(' Android原生元件')
 029            .EndArray
 030          .EndObject
 031    
 032        .EndObject;
 033    
 034        mmResult.Text := sw.ToString;
 035      finally
 036        jObjBuilder.Free;
 037        AWriter.Free;
 038        sw.Free;
 039      end;
 040    end;

仔細看一下上面014~032的程式碼的寫法架構是不是和前面JSON資料本身非常的類似? 而且和Pascal使用的嚴謹架構語法也非常的契合, 看起來非常的舒服又直覺.

但更重要的是第2種寫法會讓你的程式跑起來更快. 為什麼?

這需要一點組合語言的基礎. 簡單說明如下:

  1. 在var中宣告的變數和物件都是儲存在記憶體中
  2. 當使用物件呼叫方法/存取變數時需要將物件移到暫存器(register)中, 由於eax暫存器是最快的所以通常儘可能的是使用(移到)eax
  3. 物件在呼叫方法/存取變數之後再移回原本的記憶體中

掌握了上面3個原則之後讓我解說一下上面寫法1和寫法2的差別.

看看這2行程式碼:

001 jObjBuilder.BeginObject;
002 jObjBuilder.ParentObject.BeginObject('版本');

001使用jObjBuilder呼叫BeginObject, 所以先把jObjBuilder移到eax, 再呼叫BeginObject方法. 由於下一行又是使用jObjBuilder存取ParentObject特性, 因此eax暫時不用再移回jObjBuilder原本的記憶體, 可以立刻使用在002, 而002中的ParentObject又需要使用eax, 所以再次要執行上面3個原則, 所以如果我們看看真正執行的組合語言程式碼, 可以下面的結果:

001    uMainForm.pas.122: jObjBuilder.BeginObject;
002    00663400 8B45EC           mov eax,[ebp-$14]  原則1,2
003    00663403 E88C79FEFF       call TJSONObjectBuilder.BeginObject
004    uMainForm.pas.124: jObjBuilder.ParentObject.BeginObject('版本');
005    00663408 8B45EC           mov eax,[ebp-$14]  原則1,2
006    0066340B E8185FFEFF       call TJSONCollectionBuilder.GetParentObject
007    00663410 8945E0           mov [ebp-$20],eax  原則3
008    00663413 BA24366600       mov edx,$00663624
009    00663418 8B45E0           mov eax,[ebp-$20]
010    0066341B E8D46FFEFF       call TJSONCollectionBuilder.TBaseCollection.BeginObject
011    00663420 8945DC           mov [ebp-$24],eax  原則3 

瞭解了之後我們就可以開始比較了, 看看寫法1加入JSON Pair的方式:

jObjBuilder.ParentObject.Add('RAD Studio 10', 'Seattle');

它的組合語言如下:

001    uMainForm.pas.125: jObjBuilder.ParentObject.Add('RAD Studio 10', 'Seattle');
002    00663423 8B45EC           mov eax,[ebp-$14]  原則1,2
003    00663426 E8FD5EFEFF       call TJSONCollectionBuilder.GetParentObject
004    0066342B 8945D8           mov [ebp-$28],eax  原則3
005    0066342E B938366600       mov ecx,$00663638
006    00663433 BA54366600       mov edx,$00663654
007    00663438 8B45D8           mov eax,[ebp-$28]  原則1,2
008    0066343B E8E076FEFF       call TJSONCollectionBuilder.TBaseCollection.Add
009    00663440 8945D4           mov [ebp-$2c],eax  原則3

而鏈式寫法:

  .BeginObject('版本')     
    .Add('RAD Studio 10', 'Seattle')

它的組合語言如下:

001    0066349A E88176FEFF       call TJSONCollectionBuilder.TBaseCollection.Add
002    0066349F 8945E4           mov [ebp-$1c],eax        原則1,2
003    006634A2 BA2C366600       mov edx,$0066362c
004    006634A7 8B45E4           mov eax,[ebp-$1c]        原則3

你可以看到鏈式寫法的最終組合語言被高度的最佳化, 大量的簡少了mov/call的次數, 所以鏈式寫法的執行速度當然會快一點.

所以如何利用鏈式寫法? 很簡單, 在你的類別的方法儘可能回傳Self/this. 最後讓我們使用一個小範例來說明. 下面是TFunClass, 它定義了數個方法.

TFunClass = class(TObject)
     private
       FSalary : double;
       FTaxRate : double;
     public
       constructor Create(const dSalary, dTaxRate : double);
       function RaiseSalary(const dRate : double) : TFunClass;
       function DeductTax : TFunClass;
       function AmIRich : Boolean;
   end;

我們可以在實作方法時讓方法回傳Self, 這是為了儘可能讓物件停留在eax中而不要移回記憶體:

function TFunClass.RaiseSalary(const dRate: double): TFunClass;
 begin
   FSalary := FSalary * (1 + dRate / 100.0);
   Result := Self;
 end;

現在讓我們使用2種寫法來測試, 第一種是一行呼叫一個方法的正式寫法:

procedure TForm12.Button4Click(Sender: TObject);
 var
   aFunObj : TFunClass;
 begin
   aFunObj := TFunClass.Create(32000.00, 0.2);
   try
     aFunObj.RaiseSalary(20.0);
     aFunObj.DeductTax;
     if (aFunObj.AmIRich) then
       mmResult.Lines.Add('終於達到了')
     else
       mmResult.Lines.Add('尚待努力!');
   finally
     aFunObj.Free;
   end;
 end;

它的組合語言使用了3個move:

001    uMainForm.pas.218: aFunObj.RaiseSalary(20.0);
 002    00663FA2 6800003440       push $40340000
 003    00663FA7 6A00             push $00
 004    00663FA9 8B45F8           mov eax,[ebp-$08]
 005    00663FAC E8EFF3FFFF       call TFunClass.RaiseSalary
 006    uMainForm.pas.219: aFunObj.DeductTax;
 007    00663FB1 8B45F8           mov eax,[ebp-$08]
 008    00663FB4 E8B7F3FFFF       call TFunClass.DeductTax
 009    uMainForm.pas.220: if (aFunObj.AmIRich) then
 010    00663FB9 8B45F8           mov eax,[ebp-$08]
 011       00663FBC E81BF3FFFF       call TFunClass.AmIRich
 …

第2種是鏈式寫法, 在一行中呼叫我們需要的方法, , 直覺又像Pseudocode:

procedure TForm12.Button1Click(Sender: TObject);
 var
   aFunObj : TFunClass;
 begin
   aFunObj := TFunClass.Create(32000.00, 0.2);
   try
     if (aFunObj.RaiseSalary(20.0).DeductTax.AmIRich) then
       mmResult.Lines.Add('終於達到了')
     else
       mmResult.Lines.Add('尚待努力!');
   finally
     aFunObj.Free;
   end;
 end;

它的組合語言只使用了1個move, 成功的完成我們把物件儘量留在eax中的目的而加快執行速度:

001    uMainForm.pas.121: if (aFunObj.RaiseSalary(20.0).DeductTax.AmIRich) then
002    006637E6 6800003440       push $40340000
003    006637EB 6A00             push $00
004    006637ED 8B45F8           mov eax,[ebp-$08]
005    006637F0 E8ABFBFFFF       call TFunClass.RaiseSalary
006    006637F5 E876FBFFFF       call TFunClass.DeductTax
007    006637FA E8DDFAFFFF       call TFunClass.AmIRich

如何? 鏈式寫法產生的組合語言都更直覺, 更容易瞭解和更快.

如果您暫時無法消化上面的內容或是不熟悉組合語言的話, 沒有關係, 您可以稍後再回頭想想. 目前您可以記得的是使用鏈式寫法配合Delphi語法不但可以讓程式碼看起來更具架構性, 更酷, 最重要的是會讓您的程式跑起來更快速, 一舉3得, 那麼何樂而不為呢?

寫到最後又讓我回想起Zen of Assembly Language這本書, 它是我年輕時最喜歡的書籍之一, 我的很多組合語言知識都是從這本書學到的.

思緒在瞬時回到clock cycles之間. Have Fun!

1 則迴響

程式碼的樂趣

在我手機中最常使用的下載APP除了Line之外應該就是BBC Leaning English了, 因為除了可以聽英國腔之外還可以複習一些早已遺忘的文法. 我們日常的工作也是一樣, 除了努力寫程式碼完成工作之外, 有時看看別人的程式碼可以讓我們溫故知新, 也可以增加自身的寫碼水準.

我們在使用某些程式碼或是函式時由於太常使用而成習慣, 因此也把它們視為理所當然, 但其實其中有許多細節值得我們思考, 也考驗我們對程式碼的瞭解程度, 就讓我們看看10.4版中的FreeAndNil這個函式.

在10.4版之前FreeAndNil的原型宣告如下:

procedure FreeAndNil(var Obj); inline;

它的實作如下:

procedure FreeAndNil(var Obj);
 {$IF not Defined(AUTOREFCOUNT)}
 var
   Temp: TObject;
 begin
   Temp := TObject(Obj);
   Pointer(Obj) := nil;
   Temp.Free;
 end;
 {$ELSE}
 begin
   TObject(Obj) := nil;
 end;
 {$ENDIF}

10.4之前的FreeAndNil是可以正常工作的, 但問題是由於它的原型是宣告成未指定的參數型態, 因此我們也可以使用下面的程式碼讓它產生錯誤:

Var
   iValue : Integer;
 begin
   …
   iValue := 293849034;
   FreeAndNil(iValue); //產生存取錯誤
 End;

由於未指定參數型態, 因此Compiler不會檢查傳入的參數是否是TObject和衍生物件, 而它的實作程式碼卻把參數當成TObject及衍生物件釋放, 因此當然出錯.

那麼看看10.4改的原型宣告:

procedure FreeAndNil(const [ref] Obj: TObject); inline;

從10.4宣告的原型, 請各位想想
1. 參數const [ref] Obj: TObject到底是什麼意思?
2. FreeAndNil函式的功能是把傳入參數的物件釋放再把參數指標設定為Nil;但再回頭看看它使用了const 編譯指令, 代表參數obj是不能改變的, 那為什麼FreeAndNil可以把它設定為Nil而不會產生編譯錯誤呢?

也許各位可以停下來想一下, 上面問題的原因和答案.

如果您實在不記得或是無法回答上面2個問題的答案, 也沒問題, 反正Delphi的好處就是提供原始程式碼, 讓我們看看它的實作:

001    procedure FreeAndNil(const [ref] Obj: TObject);
 002    {$IF not Defined(AUTOREFCOUNT)}
 003    var
 004      Temp: TObject;
 005    begin
 006      Temp := Obj;
 007      TObject(Pointer(@Obj)^) := nil;
 008      Temp.Free;
 009    end;
 010    {$ELSE}
 011    begin
 012      Obj := nil;
 013    end;
 014    {$ENDIF}

關鍵答案就是在007行. 先讓我們回答上面的問題1, 在Delphi程式語言中物件參數是passed by reference, 因此正常上當傳遞物件時是不需要加上[ref]編譯指令的, 但如果在物件參數之前加上[ref]指令則代表是傳遞指到物件reference的reference(即C/C++的**ptr), 因此const [ref] Obj: TObject是指一個指到物件reference的reference參數, 而此物件reference是常數(不可改變).

OK, 現在回答問題2, 由於const [ref] Obj: TObject中的Obj是不可改變的, 但FreeAndNil又需要把Obj設定為nil, 那麼怎麼解決這個困境? 答案就是007行, FreeAndNil使用了程式技巧解決這個困境, 007行中的@Obj先取得Obj物件的指標(TObject**), 再用(Pointer(@Obj)把此指標轉為一般的指標(Ptr**), 再使^運算元取得一般的指標的內容(ptr*), 最後外面的TObject()再把一般的指標的內容(ptr*)轉為TObject*, 即物件的reference, 最後把它設它為nil而成功的把數參Obj設定為Nil. 如果您想問為什麼007行不直接使用:

Obj := Nil;

這是因為const [ref] Obj: TObject中的const不允許您改變Obj的內容, Obj是一個常數參數. 因此如果使用Obj := Nil;編譯器會產生錯誤:

[dcc32 Error] E2064 Left side cannot be assigned to

嚴格來說FreeAndNil違反了const [ref] Obj: TObject宣告語義, 因為它的確改變了常數參數的內容, 其實一個比較符合Delphi語義的FreeAndNil應該是如下:

001    procedure FreeAndNilNew(var Obj : TObject);
 002    {$IF not Defined(AUTOREFCOUNT)}
 003    var
 004      Temp: TObject;
 005    begin
 006      Temp := Obj;
 007      Obj := Nil;
 008      Temp.Free;
 009    end;
 010    {$ELSE}
 011    begin
 012      Obj := nil;
 013    end;
 014    {$ENDIF}

var代表參數Obj可以改變(需要設定為Nil), 此版又和10.4版前的原型類似. 但此版的問題是var Obj : TObject代表也是要傳遞一個指到物件reference的reference參數,因此如果你使用下面的程式碼呼叫上面版本的FreeAndNilNew:

procedure TForm12.Button3Click(Sender: TObject);
 var
   ss : TStringStream;
 begin
   ss := TStringStream.Create('A String is created!');
   FreeAndNilNew( ss);
 …

那麼會得到如下的編譯錯誤:

[dcc32 Error] E2033 Types of actual and formal var parameters must be identical

問題3 :為什麼? 如何修正?

其實答案在前面已經說了, 此版的FreeAndNilNew需要一個指到物件reference的reference參數, 而上面是傳入ss, 它是一個指到物件(TStringStream物件)的reference而不是指到物件reference的reference. 因此要修正編譯錯誤, 我們必須使用如下的程式碼:

procedure TForm12.Button3Click(Sender: TObject);
 var
   ss : TStringStream;
 begin
   ss := TStringStream.Create('A String is created!');
   FreeAndNilNew( TObject(ss) );
 …

這樣就可以正確編譯和執行了, 但為什麼? 因為現在傳給FreeAndNilNew的參數是一個指到TStringStream物件的reference(ss), 而TObject(ss)又是一個TObject的reference, 而TObject在做為參數時又是pass by reference, 因此TObject(ss)做為FreeAndNilNew的參數就成為一個正確的指到物件reference的reference參數.

因此10.4在一個有點違反語義但可以使用簡潔語法的FreeAndNil, 因為它可以讓我們使用:

  FreeAndNil(ss);

以及一個語義正確但語法比較麻煩的

FreeAndNilNew( TObject(ss) );

10.4選擇了前者, 當然如果您沒有想過這些細微差異的話, 你可能從沒想過這些問題, 但其中的樂趣就在於這些細節之中, 而這些細節又包含了一些進階的程式碼技巧.

到此這篇文章已經有點長了, 因此也該結束了, 為了提供結尾的樂趣, 您可以想想下面的FreeAndNil2版本又和前面有什麼不同嗎? 差異是什麼?

001    procedure FreeAndNil2([ref] Obj: TObject);
 002    {$IF not Defined(AUTOREFCOUNT)}
 003    var
 004      Temp: TObject;
 005    begin
 006      Temp := Obj;
 007      Obj := nil;
 008      Temp.Free;
 009    end;
 010    {$ELSE}
 011    begin
 012      Obj := nil;
 013    end;
 014    {$ENDIF}

最後如果是使用下面的版本, 又有什麼不同? 執行正確嗎? 為什麼會正確或不正確呢? 這就留給各位動動腦吧.

procedure FreeAndNil3( Obj: TObject);
 {$IF not Defined(AUTOREFCOUNT)}
 var
   Temp: TObject;
 begin
   Temp := Obj;
    Obj := Nil;
   Temp.Free;
 end;
 {$ELSE}
 begin
   Obj := nil;
 end;
 {$ENDIF}

最後再回到BBC, 前幾天有一篇討論下面2句的差異:
I forget to lock the door.
I forget locking the door.

天啊, 我真的已把國中英文文法忘記了, 歲月不饒人記憶力真的衰退了.

發表留言

RAD Studio企業元件包中Winsoft的JSON處理效率

在最近RAD Studio的優惠活動中有贈送企業元件包, 其中包含了Winsoft的元件組. 由於去年曾使用過Winsoft的Camera for Android元件,其效率遠勝過FireMonkey的Camera元件, 使用的非常滿意, 因此在看到Winsoft也有JSON的類別庫時我也很好奇其執行效率如何, 特別是10.4.x的TJSONReader/TJSONWriter已經大幅改善了執行效率.

因此我去Winsoft的網站下載了其JSON元件庫試用版, 來試試它和10.4.x的TJSONReader/TJSONWriter差異. 我沿用上次文章的例子改用Winsoft JSON類別來處理. 相對來說Winsoft的TJsonReader類別在使用上設計的非常簡單和直覺, 我個人覺得設計的很好, 它可以藉由一個repeat迴圈自然的處理整個JSON文件:

procedure TForm11.DoJSONRead;
var
  JsonItem: TJsonItem;
  Prefix: WideString;
begin
  with TJsonReader.Create(Memo1.Lines.Text) do
  try
    repeat
      JsonItem := Read;
      case JsonItem of
        itNumber:
          begin
            if (MemberName = 'latitude') then
            begin
              sl.Add('LineNumber : ' + Row.ToString);
              sl.Add('latitude = ' + NumberValue.ToString);
            end;
            if (MemberName = 'longitude') then
              sl.Add('longitude = ' + NumberValue.ToString);

          end;
      end;
    until JsonItem = itEof;
  finally
    Free;
  end;
end;

在使用相同的JSON文件資料的情形下Winsoft的JSON的類別庫的確比10.4.x的TJSONReader/TJSONWriter更快, 其處理效率達到了令人非常滿意的程度.在下面的執行結果畫面中我再次執行了10.3.3, 10.4.1和Winsoft JSON類別庫的執行結果, 各位可看到是01:682比00:022比00:016.

我不知道RAD Studio贈送的企業元件包有沒有包含Winsoft JSON類別庫的原始程式碼(因為我沒有此企業元件包), 如果沒有的話我也建議有興趣的朋友可以購買它的原始碼版, 如此一來就可以在日後昇級RAD Studio之後繼續使用它了.

1 則迴響

RAD Studio的JSON處理效率

在數月前發表RAD Studio 10.4時看著英巴的slides, 心理知道那些slides是屬於銷售的內容, 那些是屬於行銷的而那些是屬於產品技術的. 在最近幾年英巴年的產品發表中最常看到的就是又有多少品質改善又有多少效率改善, 因此當我看到10.4 slides中有提及到JSON的效率改善就比較注意, 這是因為我本身也需要經常處理大量的JSON文件.

RAD Studio 10.4的slides中提及TJsonReader的速度提昇了50%以上, 因此我很好奇這是真的嗎? 我會好奇是因為在數年前當RAD Studio對JSON開發支援之後主要是以TJSONAncestor為主的類別群來處理JSON資料, 但坦白說RAD Studio的TJSONAncestor類別群其執行效率只是堪用而已, 說不上快. 後來RAD Studio又加入了TJSONReader/TJSONWriter等新類別來提供JSON支援, 但其執行效率也是稍微好一點, 達不到業界中等效率. 但是在RAD Studio的其他類別也開始大量使用JSON處理資料之後, 例如WebClient, REST等, RAD Studio必須要改善JSON的處理效率, 否則其他相關RTL的功能都快不起來.

為了證實10.4處理JSON真的比較快, 我看了一下10.3.x和10.4的相關原始碼, 果然10.3.x的TJsonReader和10.4的TJsonReader類別有所不同, 10.4加入了一個buffer類別TJsonFiler:

RAD Studio 10.3.xRAD Studio 10.4.x
TJsonReader = class(TJsonLineInfo)TJsonReader = class(TJsonFiler)
TJsonLineInfo = class(抽象類別)TJsonFiler = class(TJsonLineInfo)
TJsonLineInfo = class(虛擬類別)

之後我隨手寫了一個使用TJSONReader的小測試程式, 我從網路上產生了一個比較複雜的JSON類別, 再使用TJSONReader來分析和擷取其中的部分資料:

procedure TForm11.DoJSONRead;
var
  ss : TStringStream;
  streamreader: TStreamReader;
  jsonreader: TJSONTextReader;
begin
  ss := TStringStream.Create(Memo1.Lines.Text);
  streamreader := TStreamReader.Create(ss, TEncoding.UTF8);
  try
    jsonreader := TJSONTextReader.Create(streamreader);
    try
      while jsonreader.Read do begin
        case jsonreader.TokenType of
          TJsonToken.Float:
            begin
              if jsonreader.Path.EndsWith('latitude', true) then
              begin
                sl.Add('LineNumber : ' + jsonreader.LineNumber.ToString);
                sl.Add('latitude = ' + jsonreader.Value.AsExtended.ToString);
              end;
              if jsonreader.Path.EndsWith('longitude', true) then
                sl.Add('longitude = ' + jsonreader.Value.AsExtended.ToString);
            end;
        end;
      end;
    finally
      jsonreader.Free;
    end;
  finally
    streamreader.Close;
    streamreader.Free;
    ss.Free;
  end;
end;

分別以10.3.3和10.4編譯執行得到下列的執行結果:

Wow, 從這個測試來看10.4的TJSONReader果然比10.3.3快了許多倍, 令人印象深刻, 看來新加入的TJsonFiler緩衝類別發揮了作用, 讓RAD Studio使用TJSONReader處理JSON讀取的速度到達了業界中上的水準了.

我也順便再檢查了TJsonWriter類別, 也看到10.3.3和10.4.x的差別:

RAD Studio 10.3.xRAD Studio 10.4.x
TJsonWriter= classTJsonWriter= class(TJsonFiler)
TJsonFiler = class(TJsonLineInfo)
TJsonLineInfo = class(虛擬類別)

由此可推10.4.x使用TJsonWriter處理JSON寫入的速度也將快上許多.

如果您的程式/系統有處理大量的JSON資料, 改用10.4.x的TJSONReader/TJSONWriter會讓您值回花費的更改時間. 更不用說如果您使了DataSnsap/RAD Server處理大量的JSON資料, 那更是強烈建議您改用10.4.x的TJSONReader/TJSONWriter, 把舊的TJSONObject, TJSONArray等類別改成使用TJSONReader/TJSONWriter來處理JSON資料吧.

發表留言

英巴新消息更新

從10.4推出以及C++Builder的PM寫了有關C++Builder未來的發展方向後我的確被很多C++Builder的朋友轟炸, 但我也沒法說什麼, 因為那畢竟是英巴的官方聲明. 幾天前英巴又公告了C++Builder最新的發展, 請C++Builder的朋友一定要去看看英巴釋出的資訊:

此外英巴最近又將舉辦一系列有關技術的線上活動, 有興趣的朋友可以參考下面的資訊:

發表留言

RAD Studio 釋出10.4.1版

今天一早打開信箱就看到英巴釋出了10.4.1版, 看看他的說明一共改正了1000多個問題, 我就深深的呼了一口氣. 由於10.4.1是一個完整的更新版, 因此在下載完10.4.1的ISO之後就迫不及待再次進行解除安裝/安裝的流程. 完成安裝之後執行10.4.1果然比10.4反應快多了, 而我個人在10.4中遇到的幾個重大問題, 例如在10.4除錯時有時會跳出IDE, Code Compete的錯誤以及不正常的消耗記憶體, C/C++ 除錯器不正常等問題都更正了, 也讓我不再需要回到10.3.3版. 因此對於現在使用10.4版的朋友, 請儘快昇級到10.4.1. 10.4.1是一個必須昇級的版本. 詳情請參考英巴的官方說明:

發表留言

英巴推出最大手筆的第3方元件促銷

昨天收到英巴的通知, 如果是舊版RAD Studio企業版/架構師版昇級到10.4版的話, 除了可以享有優惠折扣外, 更可以限時得到第3方企業元件包. 我以前沒看過什麼是第3方企業元件包, 因此去看了一下它的內容, 才知道其中包含了許多知名的第3方元件, 例如IPWorks, IntraWeb, 其中居然也包含了數10個WinSoft的元件. 我用過幾個WinSoft的元件, 給我的使用經驗非常好, 特別是幾個Android上的元件更是開發Android App不可或缺的元件.

此外第3方企業元件包中也有TMS的Aurelius元件, 這又讓我想起了以前的ECO, 時間真是過的太快了. Woll2Woll’s的FirePower X也是很多人用來開發APP的UI元件. 這個元件我沒有用過, 但我的一位好朋友對它很滿意, 因此如果您對這些元件有需要的話, 不要錯失了這次昇級的好機會.

最後只想小聲的問一下英巴, 可不可以也送我一份第3方企業元件包啊, 哈哈幻想中…

2 則迴響

Delphi記憶體漏失檢查工具

RAD Studio 10.4的Delphi和C++Builder終於在移動平台上把ARC拿掉了, 因此如果您是想開發多個平台共用一份程式碼的話, 那麼不管在任何平台您最好都忘掉ARC, 簡單的說就是不管是Delphi或是C++Builder 任何您Create()/new的物件和資源您都應該記得把它Free/delete掉, 以免造成記憶體漏失的情形.

 

但是寫過大型軟體的開發人員都知道, 在程式碼中記憶體漏失的情形似乎是不可避免的, 因此需要良好的開發紀律來儘量避免這種情形的發生. 但是除了開發紀律之外, 使用適當的工具來檢測也是非常有用的. 在C/C++的領域中這種工具非常的多, 在Delphi領域中則相對較少, 當然這也反應出了不同程式語言的特點差異. 但是在Delphi移除了ARC之後偵測記憶體漏失的工具也就重要了起來, 在Delphi領域中也有一些不錯的工具,例如數年前Delphi整合的AQTime, 它是一個非常好的記憶體檢測工具, 只可惜現在Delphi沒有整合了,

https://smartbear.com/product/aqtime-pro/overview/

 

另外madExcept也是可是提供偵測記憶體漏失的工具

http://madshi.net/

 

EurekaLog也是相關的工具:

https://www.eurekalog.com/

 

FastMM也可以提供偵測記憶體漏失的資訊.

 

不過在本文中我將介紹另一個開源的Delphi框架Delphi LeakCheck, 這個框架在10.4的產品介紹中EMBT也有推薦過. Delphi LeakCheck可以偵測程式碼中記憶體漏失的狀況,它也支援多平台的開發. 要使用Delphi LeakCheck, 請到這下載的它的原始碼:

 

https://bitbucket.org/shadow_cs/delphi-leakcheck/src/master/

 

在Delphi LeakCheck,的文件中雖然說它只支援到XE7, 但是我在10.4中使用它沒有問題. Delphi LeakCheck在使用上非常的簡易, 它也可以和DUnitX 整合使用.在本文中將簡易的說明如何上手Delphi LeakCheck, 您可以參考它的文件和原始碼瞭解更詳細的使用說明.

 

1. 下載Delphi LeakCheck, 建壓縮到一目錄, 並把此目錄加入Delphi的搜尋路徑中.

2. 為了展示它支援多平台, 在Delphi中建立一個Multi—Device應用程式, 在專案中加入如下的3個.inc檔:

3. 開啟主程式碼, 在uses的第一行加入LeakCheck程式單元, 再加入LeakCheck.Utils,LeakCheck.Setup.Trace,的2個程式單元, 最後在begin區塊的第一行加入ReportMemoryLeaksOnShutdown := True;

4.撰寫如下的測試程式碼, 建迸一個TButton物件, 再特意把它的Parent關係移除造成主表單無法釋放此TButton物件, 看看Delphi LeakCheck能不能找到:

 

procedure TForm8.Button1Click(Sender: TObject);

var

aButton : TButton;

begin

aButton := TButton.Create(Nil);

aButton.Text := ‘沒有釋放的按鈕’;

aButton.Parent := Self;

aButton.OnClick := RemoveParent;

end;

 

procedure TForm8.RemoveParent(Sender: TObject);

begin

(Sender as TButton).Parent := Nil;

end;

 

5. 執行此範例:

 

在範例程式結束時就可以看到Delphi LeakCheck報告了有一TButton物件沒有釋放:

接著筆者測試了沒有釋放TForm物件, 沒有釋放配置的記憶體, Delphi LeakCheck都可以找到, 這代表對於一般的應用程式Delphi LeakCheck已經可以非常稱職的幫你偵測記憶體漏失的錯誤.

procedure TForm8.Button2Click(Sender: TObject);

var

aForm : TfrmSecond;

begin

aForm := TfrmSecond.Create(Nil);

aForm.Show;

end;

 

procedure TForm8.Button3Click(Sender: TObject);

var

ptrByte: pointer;

begin

GetMem(ptrByte, 10240);

end;

 

您也可以開啟.map檔案功能配合Delphi LeakCheck, 那麼Delphi LeakCheck可以提供更詳細, 準確的記憶體漏失資訊. 對於有此方面需求的開發人員, 祝您使用愉快.

發表留言

又回到了原點

記得以前我就在WordPress時不時的撰寫部落格. 但後來英巴要求要到英巴本身的網站撰寫, 因此就沒有再於WordPress撰寫了. 後來英巴網站再次改版而不知為何我再也沒有權限在英巴的部落格撰寫文章, 因此好長一段時間也沒有再更新我的部落格. 沒想到最近又收到英巴的要求希望我能寫寫部落格, 但我沒有英巴部落格撰寫文章的權限, 因此想想還是回到以前我的部落格吧, 經過了數年又回到原點, 但人事似已全非.

 

匆忙間要交差寫部落格也不知如何下筆, 那就來聊聊最近遇到的一些技術支援的例子吧. 第一個比較簡單, 是幾間還在使用C++Builder 6的公司, 可能是新進員工要接手舊的C++Builder 6專案, 因此找上我們希望能夠幫助他們昇級到最新的C++Builder 10.4. 其實這個支援例子的基本困難點是C++ Builder 6已經太久遠了,因此新的C++Builder 10.4已經無法直接開啟舊專案而自動昇級專案. 因此解決方法很簡單, 就是在10.4中建立一個VCL專案, 把其中的主表單和C/C++原始檔案刪除, 再把舊的C++Builder 6專案中的表單和C/C++原始檔案加入此新的專案, 儲存之後即可以重新編譯. 另一個問題就是這些舊的6專案中使用了一些C++Builder 6的元件而10.4已經沒有這些元件了. 解決方法也很簡單, 把舊專案中使用這些元件的表單打包成dll或是COM/DCOM/COM+, 再讓10.4新的專案呼叫使用即可. 但我知道其中的問題是新一代的程式師似乎已經不瞭解COM/DCOM/COM+技術, 所以只能使用dll了.

 

第2個支援案例比較複雜, 是一位Delphi的客戶求救要求. 他的問題是他需要接手一個C#的Web案子其中的加密和解密資料的部份, 但他不熟悉C#, 因此求救詢問是否可以使用Delphi來完成. 我第一個回答也是使用COM就可以讓C#呼叫Delphi的程式碼, 但這位年輕工程師也不熟悉COM/DCOM, 所以又要回到dll的解決方案. 因此解決方案就變成:

 

C#加密資料 à Delphi dll解密C#資料, 再進行資料加工運算, 回傳C# à C#解密

 

首先讓C#能夠呼叫Delphi dll如下:

 

[DllImport(“MyDelphi.dll",

CallingConvention = CallingConvention.StdCall,

CharSet = CharSet.Ansi)]

 

public static extern bool

EncodeMyData(string inputString, string sPassword,

int outputStringBufferSize, ref string outputStringBuffer);

 

其中要注意的地方就是呼叫慣例必須使用stdcall, 而且要注意參數傳遞的型態宣告必須正確(in, out, ref…).

 

第2步是在Delphi這邊要宣告對應的函式型態而且也要使用stdcall, 參數型態的對應(C# string -> Delphi PAnsiChar, C# ref string -> Delphi var PAnsiChar)

 

function EncodeMyData(const sData, sPassword : PAnsiChar; const outputStringBufferSize : Integer; var outData : PAnsiChar) : WordBool; stdcall; export;

var

 Codec1: TCodec;

 CryptographicLibrary1: TCryptographicLibrary;

 sTempData : String;

begin

  try

    Codec1 := TCodec.Create( nil);

    CryptographicLibrary1 := TCryptographicLibrary.Create( nil);

    try

      Codec1.CryptoLibrary  := CryptographicLibrary1;

      Codec1.StreamCipherId := uTPLb_Constants.BlockCipher_ProgId;

      Codec1.BlockCipherId  := ‘native.AES-128’;

      Codec1.ChainModeId    := uTPLb_Constants.CBC_ProgId;

      Codec1.Password := String(sPassword);

      Codec1.EncryptString(String(sData), sTempData, TEncoding.ASCII);

      StrLCopy(outData, PAnsiChar(AnsiString(sTempData)), outputStringBufferSize-1);

    finally

      Codec1.Free;

      CryptographicLibrary1.Free;

    end;

    Result := True;

  except on ex : Exception do

    Result := False;

  end;

end;

 

在Delphi這邊我選擇使用TurboPower的LockBox來加密和解密資料.

最後我寫了一個C#和Delphi的dll範例來驗證可行性,從下圖可以看到C#程式把加密資料傳遞給Delphi, 再由Delphi解密回傳. C#和Delphi兩邊互相加/解密資料完全沒有問題, C#也可以順利呼叫Delphi程式碼並取得運算結果.

202008171

發表留言

使用C++Builder Berlin Update 2開發BeaconFence 應用程式

BeaconFence在數個C++Builder/Delphi版本中就推出了, 我記得也在數年前的產品發表會中介紹過,當時Beaconfence身價不菲, 我也沒機會用. 但從Seattle版本後Embarcadero大幅降低 Beaconfence的價格, 而且提供開發人員版,所以才有機會試用它.

 

日前一位使用C++Builder的朋友向我抱怨Beaconfence只有Delphi的範例, 沒有C++Builder的範例, 他問我到底能不能用C++Builder開發Beaconfence? 能不能幫忙用C++Builder做一個Beaconfence的POC? 因此筆者特別商借了3個Beacon, 一個是插電式, 2個使用電池的Beacon:

upload1

upload2

upload3

藉由TBeaconMapFencing元件載入POC地點的佈建架構圖, ,再分別把3個Beacon設置在書房, 客廳和玄關處:

upload4

Berlin Update 2版Beaconfence我最喜歡的一點就是為TBeaconMapFencing元件加入了Extended Mode, 這個模式可以同時掃瞄iBeacon,AltBeacon和Eddystone 3種不同型式的Beacon, 又方便而且又穩定:

upload5

之後我們就可以藉由下面的C++程式碼根據使用者的喜好設定TBeaconMapFencing元件的地圖設定值:

void TfmMainForm::ChangeOption(TFencingMapOption AOption, bool ShouldInclude)

{

  TFencingMapOptions LOp = BeaconMapFencing1->MapOptions;

  if (ShouldInclude)

  {

  LOp << AOption;

  }

  else

  {

  LOp >> AOption;

  }

 

  BeaconMapFencing1->MapOptions = LOp;

}

最後實作TBeaconMapFencing元件的On PositionEstimated事件處理函式:

void __fastcall TfmMainForm::BeaconMapFencing1PositionEstimated(TPointF &AEstimatedPoint, TPointF &APointToPath)

{

  ShowStatus(AEstimatedPoint, APointToPath);

}

 

void TfmMainForm::ShowStatus(TPointF AEstimatedPoint, TPointF APathPoint)

{

  String LMapsInfo = “";

 

  for (int I = 0; I <  BeaconMapFencing1->ProjectInformation.Maps.Length; I++)

  {

  LMapsInfo = LMapsInfo + " Map:" + IntToStr(I) + " Act:" + IntToStr(BeaconMapFencing1->ProjectInformation.Maps[I].GetActiveBeaconsCount() );

  if (BeaconMapFencing1->ProjectInformation.Maps[I].NearestBeacon().BTBeacon != NULL)

    LMapsInfo = LMapsInfo + " dis:" + FormatFloat(“0.00″, BeaconMapFencing1->ProjectInformation.Maps[I].NearestBeacon().BTBeacon->Distance);

  }

 

  LbStatus->Text = IntToStr(GTimes) + " at " +

  FormatFloat(“0.00″, AEstimatedPoint.X) + " , " + FormatFloat(“0.00″, AEstimatedPoint.Y) +

    " (" + FormatFloat(“0.00″, APathPoint.X) + " , " + FormatFloat(“0.00″, APathPoint.Y) + “)" +

  " " + LMapsInfo;

  GTimes++;

}

把此範例App部署到筆者的HTC手機中執行並且在POC地點中行走時就可以看到此App能精確的在室入定位筆者的位置, 例如下圖是筆者在客廳中移動, 在接近設置在客廳的Beacon設備時BeaconFence的Beacon便變成綠色, 代表筆者就在此Beacon設備附近0.5公尺之內:

upload6

下圖則顯示筆者移動到書房時也能立刻且精準的定位筆者:

upload7

本文說明了使用C++Builder不但能完全沒問題的開發BeaconFence的應用, 而且充滿了樂趣.

2 則迴響