Unexpected failure of custom registered Reverter u

2019-05-22 12:03发布

问题:

The code below is from the JSonMarshall project in chapter 7 of Marco Cantu's Delphi 2010 Handbook. The source code is available from here http://cc.embarcadero.com/item/27600. I have made two changes to it:

  1. Add JSon to the implementation Uses clause to get it to compile.

  2. Added the line

    theName := 'XXX'; // added by me

to the TDataWithList.Create constructor to assist debugging

I am running the code in Delphi Seattle (without update 1)

The purpose of the project is to demo a custom converter and reverter for the TDataWithList declared type. The custom converter seems to work fine, judging by the result output to Memo1.

However, attempting to run the reverter results in a "Read of address 00000000" AV on the line

           sList.Add (Args[I]);

in btnUnmarshalReverterClick. The immediate cause of this is that contrary to what the author evidently intended, when the above line executes, sList is Nil.

My question is simply why is sList Nil and how to fix this problem?

I have tried, not entirely successfully, to trace through the DBXJSONReflect source to find out why.

After

  Obj := ObjectInstance(FRTTICtx, objType);

in function TJSONUnMarshal.CreateObject, TDataWithList(obj).theName is 'XXX' as I'd expect and TDataWithList(obj).theLList is an initialized, but empty, TStringList.

However, by the time the anonymous method in btnUnmarshalReverterClick is called, TDataWithList(Data).theList is Nil.

Update: The reason that TDataWithList(Data).theList (incorrectly, imo) becomes Nil is that it is set to Nil in TJSONPopulationCustomizer.PrePopulate by a call to PrePopulateObjField. So I suppose the question is, why does PrePopulate allow an object's field which has been initialized in its constructor to be overwritten as if it knows better that the object's constructor.

Update2:

There may be an additional problem, in that as far as I can tell, in TInternalJSONPopulationCustomizer.PrePopulateObjField, the assignment which overwrites TListWithData.theList with Nil, namely

rttiField.SetValue(Data, TValue.Empty);

does not seem to result in the TStringlist destructor being called.

Btw, I get the same error running the project in XE4, which is the earliest version I have which includes JSonUnMarshal.

Code:

type
  [...]

  TDataWithList = class
  private
    theName: String;
    theList: TStringList;
  public
    constructor Create (const aName: string); overload;
    constructor Create; overload;
    function ToString: string; override;
    destructor Destroy; override;
  end;

[...]

procedure TFormJson.btnMarshalConverterClick(Sender: TObject);
var
  theData: TDataWithList;
  jMarshal: TJSONMarshal;
  jValue: TJSONValue;
begin
  theData := TDataWithList.Create('john');
  try
    jMarshal := TJSONMarshal.Create(
      TJSONConverter.Create); // converter is owned
    try
      jMarshal.RegisterConverter(TDataWithList, 'theList',
        function (Data: TObject; Field: string): TListOfStrings
        var
          I: Integer;
          sList: TStringList;
        begin
          sList := TDataWithList(Data).theList;
          SetLength(Result, sList.Count);
          for I := 0 to sList.Count - 1 do
            Result[I] := sList[I];
        end);
      jValue := jMarshal.Marshal(theData);
      try
        Memo1.Lines.Text := jValue.ToString;
      finally
        jValue.Free;
      end;
    finally
      jMarshal.Free;
    end;
  finally
    theData.Free;
  end;
end;

procedure TFormJson.btnUnmarshalReverterClick(Sender: TObject);
var
  jUnmarshal: TJSONUnMarshal;
  jValue: TJSONValue;
  anObject: TObject;
begin
  jValue := TJSONObject.ParseJSONValue(
    TEncoding.ASCII.GetBytes (Memo1.Lines.Text), 0);
  try
    jUnmarshal := TJSONUnMarshal.Create;
    try
      jUnmarshal.RegisterReverter(TDataWithList, 'theList',
        procedure (Data: TObject; Field: string; Args: TListOfStrings)
        var
          I: Integer;
          sList: TStringList;
        begin
          sList := TDataWithList(Data).theList;
          for I := 0 to Length(Args) - 1 do
             sList.Add (Args[I]);
        end);
      anObject := jUnmarshal.Unmarshal(jValue);
      try
        ShowMessage ('Class: ' + anObject.ClassName +
          sLineBreak + anObject.ToString);
      finally
        anObject.Free;
      end;
    finally
      jUnmarshal.Free;
    end;
  finally
    jValue.Free;
  end;
end;

function TMyData.ToString: string;
begin
  Result := theName + ':' + IntToStr (theValue);
end;

{ TDataWithList }

constructor TDataWithList.Create(const aName: string);
var
  I: Integer;
begin
  theName := aName;
  theList := TStringList.Create;
  for I := 0 to 9 do
    theList.Add(IntToStr (Random (1000)));
end;

constructor TDataWithList.Create;
begin
  // core initialization, used for default construction
  theName := 'XXX';  // added by me
  theList := TStringList.Create;
end;

destructor TDataWithList.Destroy;
begin
  theList.Free;
  inherited;
end;

function TDataWithList.ToString: string;
begin
  Result := theName + sLineBreak + theList.Text;
end;

回答1:

rttiField.SetValue(Data, TValue.Empty); simply overrides the field value because as the name implies it's a field, not a property with get / set methods. The destructor of TStringList is not called due to simple pointer assignment.

The solution here is to declare a property:

TDataWithList = class
  ...
  strict private
    theList: TStringList;
    ...
  public
    property Data: TStringList read ... write SetData
    ...
end;

TDataWithList.SetData(TStringList aValue);
begin
  theList.Assign(aValue);
end;