cnpack-group,您好!
昨天看代码看得头晕脑胀,后来遇见了一段垃圾代码,于是在CnPack的QQ群
上和大伙儿讨论了一阵子,沈龙强兄给出了不少有用的意见,下面总结一下。
问题全是由偶然碰到的一段古怪的垃圾代码引起的:
// 以下是古怪代码
var
Form1: TForm1;
procedure TForm1.FormClose(var Action: TCloseAction)
begin
Action := caFree;
FreeAndNil(Form1);
end;
// 以上是古怪代码
这段代码中,窗体关闭的时候,作者怕不能释放,除了写了个Action:=caFree外,
还特意FreeAndNil了Form1这个变量,——这里Form1是程序的主窗体。
代码不能这样写。—— 这似乎用不着讨论了,问题是这样写会出些什么毛病,以
及毛病是怎样出来的。
问题一:主窗体在OnClose中提前释放,会导致程序退出时失去响应。
“佛的光辉”兄说Form1是主窗体时,这样写会导致程序退出时失去响应。一测试
果然是这样,原因是啥?
看看TCustomForm的Close过程,前面是设置CloseAction,然后调用
DoClose(CloseAction);以触发OnClose事件,接着根据CloseAction进行窗体状态设置:
……
DoClose(CloseAction);
if CloseAction <> caNone then
if Application.MainForm = Self then Application.Terminate
else if CloseAction = caHide then Hide
else if CloseAction = caMinimize then WindowState := wsMinimized
else Release;
主窗体Close掉后程序会退出,这个现象可以用在if Application.MainForm =
Self then Application.Terminate 这段代码解释。但如果DoClose中调用的OnClose
的事件处理中把Form本身先释放了,那么Application的MainForm就变成了nil,再也不
等于Self了(释放Form1这个对象的实例不会影响到这个过程的Self指针,因此Self指针
不变,被“悬空”了)。所以Application没能Terminate,程序主窗体关掉了,
Application这个隐藏窗口却继续在进行消息循环,所以会造成失去响应的假像。
问题二:Form创建的过程以及OnCreate等事件的顺序问题。
这里只写写结论,讨论的过程太不好整理了。
Form的OldCreateOrder为True的时候,DoCreate在Create里头被调用,触发
OnCreate事件;DoDestory在Destroy中调用,触发OnDestroy事件。为False的时候,
DoCreate在AfterConstruction里头调用,DoDestory在BeforeDestruction里头调用;
但不管怎样,DoCreate的时候,子控件都已经通过InitInheritedComponent创建了;
DoDestroy的时候,子控件都没有被释放。所以我们能在OnCreate和OnDestroy的时候
放心地引用form上的控件。
但如果想在OnCreate事件里头引用Form1这样的变量,就需要注意了。
如果你是通过Form1 := TForm1.Create(nil)这样的形式来创建Form1的,那么Create
执行完毕后,Form1才会被赋值。
如果这个Form的OldCreateOrder偏偏又为True,那么OnCreate在Create里头被触发,
此时OnCreate里头千万不能引用Form1,因为它还没被赋值。
如果是通过Application.CreateForm(TForm1, Form1)这样使用的,则可放心,
CreateForm的代码中,NewInstance后,就会给Form1赋值:
Instance := TComponent(InstanceClass.NewInstance);
TComponent(Reference) := Instance;
看见了吧?是先分配的对象空间并赋引用值后再调用的Instance.Create(Self);
此时Create过程中,Reference也就是Form1已经被正确地赋予对象引用值了,
只是这个对象还没完成初始化而已。
如果使用Self,则不管啥情况下,都不存在上边的问题。
问题三:Form的OnCreate事件里头抛出异常时窗体不自动中止并销毁。
大伙儿可能都知道,一般一个对象在Create的时候如果抛出异常,则编译器会自动
调用其Destroy函数并FreeInstance,为的是保证内存不泄漏。我们把OldCreateOrder
设为True,在OnCreate里头手工raise一个异常,发现窗体仍然能正常创建并显示。
其实这个问题很简单,看看DoCreate的代码:
if Assigned(FOnCreate) then
try
FOnCreate(Self);
except
if not HandleCreateException then
raise;
end;
原来它早就写了一个try except,把OnCreate中的异常给抓住了。对于为什么要这
样写还不太清楚,可能就是为了防止小异常影响大窗体显示?
从这里头得到一个小教训就是,不是所有的DoXxxxx过程都是这样写的:
if Assigned(FOnXxxxx) then
FOnXxxxx(Self);
算是一点小经验吧。^_^
最后一个问题:S := TForm1(nil).Caption; 不会出错?
这个问题完全是调试上述问题时的副产品,当时用了个nil给Form1来引用它,
结果发现TForm1(nil).Caption这样的用法不会出内存访问冲突,为什么?
看看TControl.GetText过程就可以明白。
function TControl.GetText: TCaption;
var
Len: Integer;
begin
Len := GetTextLen;
SetString(Result, PChar(nil), Len);
if Len <> 0 then GetTextBuf(Pointer(Result), Len + 1);
end;
先GetTextLen,Len为0的话就啥都不做了。看看GetTextLen怎么做的。
function TControl.GetTextLen: Integer;
begin
Result := Perform(WM_GETTEXTLENGTH, 0, 0);
end;
那么又得看Perform方法了,里头最重要的三句是:
Message.Result := 0;
if Self <> nil then WindowProc(Message);
Result := Message.Result;
这里和Free一样会判断Self指针是否为nil,不为nil才调窗口过程,
为nil则直接返回,所以不出错。
问题总结完毕。累死了,手都酸了。
致
礼!
Passion passion@cnvcl.org
CnPack 开发组管理员
http://www.cnvcl.org
2004-05-18 |