您现在的位置:首页 >> 基础算法 >> window基础 >> 内容

Delphi 2009泛型容器的两个严重Bug及深入分析

时间:2011/9/3 15:32:44 点击:

  核心提示:1. 调用TObjectListT.Create(ownsObjects: Boolean)创建列表后可能报AV异常。代码示例: typeTPerson = classend;varlist: TOb...

1. 调用TObjectList<T>.Create(ownsObjects: Boolean)创建列表后可能报AV异常。

代码示例:

type
  TPerson = class
  end;
 
var
  list: TObjectList<TPerson>;
  bob: TPerson;
begin
  list := TObjectList<TPerson>.Create(False);
  bob := TPerson.Create;
  try
    if list.IndexOf(bob) = -1 then // Access Violation 异常
    begin
    end;
  finally
    bob.Free;
    list.Free;
  end;
end;

代码运行到红色处会报AV错误,为什么呢?追踪代码之后才发现FComparer居然为nil。为什么不是默认的比较器(TComparer<T>.Default)呢?打开源码来看看:

constructor TObjectList<T>.Create(AOwnsObjects: Boolean);
begin
  inherited;
  FOwnsObjects := AOwnsObjects;
end;

既然TObjectList<T>是继承自TList<T>的,那我们再看看TList<T>的代码:

constructor TList<T>.Create;
begin
  Create(TComparer<T>.Default);
end;

现在我肯定TList<T>.Create是正确的。那问题应该出在TObjectList.Create里面。(大家知道Bug在哪里了吗?) 仔细看看inherited这句代码,难道是因为后面没带标识符(identifier)吗? 我们先来看看Delphi的帮助文档:

When inherited has no identifier after it, it refers to the inherited method with the same name as the enclosing method or, if the enclosing method is a message handler, to the inherited message handler for the same message. In this case, inherited takes no explicit parameters, but passes to the inherited method the same parameters with which the enclosing method was called. For example,

inherited;

occurs frequently in the implementation of constructors. It calls the inherited constructor with the same parameters that were passed to the descendant.

这段帮助主要的意思就是当inherited后面没有带标识符时,它会调用父类里面参数签名相同的同名函数。那问题来了,父类TList<T>里面并没有Create(Boolean)形式的构造器,按道理编译器应该报错啊。带着这个疑问,我做了一个简单的试验:

program TestInherited;
 
{$APPTYPE CONSOLE}
 
uses
  SysUtils, Dialogs;
 
type
  TBase = class
  public
    procedure Call(value: Integer); overload;
    procedure Call(const value: string); overload;
  end;
 
  TDerived = class(TBase)
  public
    procedure Call(value: Boolean = False); overload;
  end;
 
{ TBase }
 
procedure TBase.Call(value: Integer);
begin
  ShowMessage('Integer');
end;
 
procedure TBase.Call(const value: string);
begin
  ShowMessage('string');
end;
 
{ TDerived }
 
procedure TDerived.Call(value: Boolean);
begin
  inherited;  { do nothing }
end;
 
begin
  try
    example := TDerived.Create;
    try
      example.Call;  { do nothing }
    finally
      example.Free;
    end;
  except
    on E:Exception do
      Writeln(E.Classname, ': ', E.Message);
  end;
end.

编译通过了,运行一下,发现它根本没有调用父类的方法(这也在预料之中)。那么如果TBase只有一个Call方法会怎么样呢?我们发现编译器会提示编译错误:“Incompatible types”。原因终于找到了,原来编译器碰到父类有多个同名重载函数的情况,采用的方法竟然是不执行。这点也可以从下图中得到证明:

600) this.width = 600;">

另外,我去Delphi 7下编译也是相同的结果。说明这个特性是和编译器版本无关的。

呵呵,我现在终于明白了为啥有人一直强调说,使用inherited要带完整的参数。

2. 第二个Bug就比较离谱了:

我在使用for-in语句遍历TObjectDictionary<TKey, TValue>.Values时居然报AV错误。追踪了代码才发现迭代的次数居然多于Values.Count!找到了源码看看:

function TDictionary<TKey,TValue>.TValueEnumerator.MoveNext: Boolean;
begin
  while FIndex < Length(FDictionary.FItems) do
  begin
    Inc(FIndex);
    if FDictionary.FItems[FIndex].HashCode <> 0 then
      Exit(True);
  end;
  Result := False;
end;

看到这两句,我彻底无语了,另外又发现了两处相同的写法:

function TDictionary<TKey,TValue>.TPairEnumerator.MoveNext: Boolean;
begin
  while FIndex < Length(FDictionary.FItems) do
  begin
    Inc(FIndex);
    if FDictionary.FItems[FIndex].HashCode <> 0 then
      Exit(True);
  end;
  Result := False;
end;

还有一处:

function TDictionary<TKey,TValue>.TKeyEnumerator.MoveNext: Boolean;
begin
  while FIndex < Length(FDictionary.FItems) do
  begin
    Inc(FIndex);
    if FDictionary.FItems[FIndex].HashCode <> 0 then
      Exit(True);
  end;
  Result := False;
end;

这种越界访问不一定能测试出来,因为它的结果取决于内存里的随机数据。

3. 修正方法

打开Generics.Collections单元,

#1.  将1679行 ”inherited;”改为: inherited Create;

#2.  修改三处(第1596行、第1631行、第1666行)“while FIndex < Length(FDictionary.FItems) do”为

while FIndex < Length(FDictionary.FItems) - 1 do

[2009-04-19 更新]:上述BUG应该会在Delphi 2009 Update 3中修正

作者:zuobaoquan 来源:转载
共有评论 0相关评论
发表我的评论
  • 大名:
  • 内容:
  • 盒子文章(www.2ccc.com) © 2022 版权所有 All Rights Reserved.
  • 沪ICP备05001939号