今天在使用Delphi2009(Update 2)的泛型容器时,突然发现了几个严重Bug,貌似以前也碰到过。今天就来一并解决掉。
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”)。原因终于找到了,原来编译器碰到父类有多个同名重载函数的情况,采用的方法竟然是不执行。这点也可以从下图中得到证明:
另外,我去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
作者:Zuo Baoquan (Bob)
Blog: http://baoquan.cnblogs.com/ 转载请保留此信息
|
请发表评论