在 Delphi 中使用 TObjectDictionary 时如何避免 EInvalidPointer 错误?

How can I avoid EInvalidPointer error when using TObjectDictionary in Delphi?

程序通过window消息接收product information datas。 在 TProductInstance.PutProductData 过程中处理的传入数据。

产品信息包含日期、名称、价格。 我想将数据存储为 TObjectDictionary。键是产品的相同日期,值是产品信息数据列表作为 TObjectList。 我也想只维护最近 7 天的数据。 顺便说一下,当我从 TObjectDictionary 中删除项目进行维护时,会出现如下错误。

First chance exception at 214598.
Exception class EInvalidPointer with message 'Invalid pointer operation'. Process product.exe (3848).

这是由FProductDictionary.Remove(StringKey);造成的。

如何避免 EInvalidPointer 维护最新 7 天数据的错误?

type
  TProductItem = class(TObject)
  private
    FDate: String;
    FName: String;
    FPrice: Integer;
    procedure SetDate(const value: String);
    procedure SetName(const value: String);
    procedure SetPrice(const value: Integer);
  public
    property Date: String read FDate write SetDate;
    property Name: String read FName write SetName;
    property Price: Integer read FPrice write SetPrice;
    constructor Create(const date, name: String; const price: Integer);
  end;

  TProductItemList = class(TObjectList<TProductItem>);

type
  TProductInstance = class(TObject)
  private
  public
    FLatestDate: String;

    FProductList: TProductItemList;
    FProductDictionary: TObjectDictionary<String, TProductItemList>;

    constructor Create;
    destructor Destroy; override;

    procedure PutProductData(var Data: LP_Data);
  end;

implementation

constructor TProductInstance.Create;
begin
  FLatestDate := '';

  FProductList := TProductItemList.Create;
  FProductDictionary := TObjectDictionary<String, TProductItemList>.Create([doOwnsValues]);
end;

procedure TProductInstance.PutProductData(var Data: LP_Data);
var
  StringKey: String;
begin
  if (Trim(LP_Data^.date) <> FLatestDate) then
  begin
    FProductDictionary.AddOrSetValue(Trim(LP_Data^.date), FProductList);
    for StringKey in FProductDictionary.Keys do
    begin
      if (GetDateToInt(Trim(LP_Data^.date)) - GetDateToInt(FLatestDate) > 7) then
        FProductDictionary.Remove(StringKey);
    end;
    FProductList.Free;
  end;
  FProductList.Add(TProductItem.Create(Trim(LP_Data^.date), Trim(LP_Data^.name), Trim(LP_Data^.price)));
  FLatestDate := Trim(LP_Data^.date);
end;

已更新

type
  TProductItem = class(TObject)
  private
    FDate: String;
    FName: String;
    FPrice: Integer;
    procedure SetDate(const value: String);
    procedure SetName(const value: String);
    procedure SetPrice(const value: Integer);
  public
    property Date: String read FDate write SetDate;
    property Name: String read FName write SetName;
    property Price: Integer read FPrice write SetPrice;
    constructor Create(const date, name: String; const price: Integer);
  end;


type
  TProductInstance = class(TObject)
  private
  public
    FLatestDate: String;

    FProductList: TObjectList<TProductItem>;
    FProductDictionary: TObjectDictionary<String, TObjectList<TProductItem>>;

    constructor Create;
    destructor Destroy; override;

    procedure PutProductData(var Data: LP_Data);
  end;

implementation

constructor TProductInstance.Create;
var
  LProductItem: TProductItem;
  LProductItemList: TObjectList<TProductItem>;
  LStringList: TStringList;
begin
  FLatestDate := '';

  FProductList := TObjectList<TProductItem>.Create;
  FProductDictionary := TObjectDictionary<String, TObjectList<TProductItem>>.Create([doOwnsValues]);
end;

procedure TProductInstance.PutProductData(var Data: LP_Data);
var
  StringKey: String;
begin
  FProductList.Add(TProductItem.Create(Trim(LP_Data^.date), Trim(LP_Data^.name), Trim(LP_Data^.price)));  
  if (Trim(LP_Data^.date) <> FLatestDate) then
  begin
    LProductItemList := TObjectList<ProductItem>.Create;
    for LProductItem in FProductList do
    begin
      LProductItemList.Add(LProductItem);
    end;

    FProductDictionary.AddOrSetValue(Trim(LP_Data^.date), LProductItemList);
    FProductList.Clear;

    LStringList := TStringList.Create;
    for StringKey in FProductDictionary.Keys do
    begin
      if (GetDateToInt(Trim(LP_Data^.date)) - GetDateToInt(FLatestDate) > 7) then
      begin
        LStringList.Add(StringKey);
      end;
    end;
    for StringKey in LStringList do
    begin
      FProductDictionary.Remove(StringKey);
    end;

    FreeAndNil(LStringList);
  end;
end;

更新代码发生 EInvalidPointer 错误 FProductDictionary.Remove(StringKey); 我错了什么?

您提供的代码不完整。您没有显示 TProductInstance 的析构函数。对于这样的问题,您应该始终提供一个简单的 MCVE。这在单个控制台 .dpr 文件中很容易实现。

看看我们能看到的,很明显代码中的生命周期管理被破坏了。让我们批评一下这个方法。

procedure TProductInstance.PutProductData(var Data: LP_Data);
var
  StringKey: String;
begin
  if (Trim(LP_Data^.date) <> FLatestDate) then
  begin
    FProductDictionary.AddOrSetValue(Trim(LP_Data^.date), FProductList);
    for StringKey in FProductDictionary.Keys do
    begin
      if (GetDateToInt(Trim(LP_Data^.date)) - GetDateToInt(FLatestDate) > 7) then
        FProductDictionary.Remove(StringKey);
    end;
    FProductList.Free;
  end;
  FProductList.Add(TProductItem.Create(Trim(LP_Data^.date), Trim(LP_Data^.name), 
    Trim(LP_Data^.price)));
  FLatestDate := Trim(LP_Data^.date);
end;

因为 FProductDictionary 拥有它的值,当您这样做时

FProductDictionary.AddOrSetValue(Trim(LP_Data^.date), FProductList);

然后 FProductDictionary 成为 FProductList 的所有者。这意味着你永远不应该破坏 FProductList。但是,您确实这样做了:

FProductList.Free;

所以你会多次破坏 FProductList 这是一个明显的错误。

接下来要做什么?你需要处理生命周期的问题。我无法从此处提供的代码中了解您要实现的目标,以及应如何管理生命周期。你需要弄清楚谁负责拥有什么,并确保你坚持明确的生命周期管理政策。

从表面上看,我最好的猜测是您需要删除 FProductList 字段。当您需要向 FProductDictionary 添加新项目时,实例化 TProductItemList 的新实例,填充它,并将其添加到字典中。那时字典控制了 TProductItemList.

的生命周期

作为最后一条评论,我建议 TProductItemList 类型毫无意义。我会删除它。使用 TObjectList<TProductItem> 使代码比 reader 更清晰。 reader 可以查看 TObjectList<TProductItem> 并立即知道它是什么,因为 TObjectList<T> 是一种无处不在的类型。