动态创建组件:该做的和不该做的 (0分)

  • 主题发起人 主题发起人 飞骐
  • 开始时间 开始时间

飞骐

Unregistered / Unconfirmed
GUEST, unregistred user!
这是我在CodeRush的公司网站上看到的好文章,希望对大家有帮助!
http://www.eagle-software.com/

[h1][blue]Creating Components Dynamically[/blue][/h1]
Copyright &copy
1998, by Mark Miller

[brown]Contents
Overview
Class Methods and Object Methods
The "AOwner" Parameter Demystified
Dynamic Component Creation
Dynamic Form Creation
Summary[/brown]


--------------------------------------------------------------------------------

[brown][h3]Overview[/h3][/brown]
Most often when programming in Delphi you don't need to dynamically create a component. If you drop a component on a form, Delphi handles the component creation automatically when the form is created. This paper will cover the correct way to programmatically create components at runtime.

Contents White Papers Home Page


--------------------------------------------------------------------------------

[h3][brown]Class Methods and Object Methods[/brown][/h3]
The Create constructor is a class method, as opposed to virtually all other methods you’ll encounter in Delphi programming, which are object methods. A class method is a method of the class, and appropriately enough, an object method is a method that can be called by an instance of the class. This is best illustrated by an example, with classes and objects highlighted in red for clarity:

myCheckbox := TCheckbox.Create(nil);
Here, the call to Create is preceded by the class name and a period ("TCheckbox."). It's a method of the class, commonly known as a constructor. This is the mechanism by which instances of a class are created. The result is an instance of the TCheckbox class. These instances are called objects. Contrast the previous line of code with the following:

myCheckbox.Repaint;
Here, the Repaint method of the TCheckbox object (inherited from TWinControl) is called. The call to Repaint is preceded by the object variable and a period ("myCheckbox.").

Class methods can be called without an instance of the class (e.g., "TCheckbox.Create"). Class methods can also be called directly from an object (e.g., "myCheckbox.ClassName"). However object methods can only be called by an instance of a class (e.g., "myCheckbox.Repaint").

Behind the scenes, the Create constructor is allocating memory for the object (and performing any additional initialization as specified by TCheckbox or its ancestors).

--------------------------------------------------------------------------------

[brown][h3]The "AOwner" Parameter Demystified[/h3][/brown]
Whenever memory is allocated for an object, it must eventually be freed (it’s a good thing to do). But with components, there is often little mention of the need to do this. Delphi must be destroying the objects, but how does it know when we’re through with them? Here's a hint. TComponent declares the Create constructor as follows:

constructor Create(AOwner: TComponent)
virtual;
The answer is in the "AOwner" parameter to the Create constructor. The Create constructor is virtual. Every component that resides on the palette inherits this constructor. Inside the constructor, Delphi creates the component, and if an owner is specified (if AOwner is not nil), Delphi adds the newly-created component to the Owner's internal owned component list. This internal list is consulted by Delphi for three important events that can affect an owner (e.g., any TComponent descendant):

A new component is created and added to the internal component list. When this happens, every component on the internal list is notified by calling the virtual method, Notification, passing a reference to the new component and specifying opInsert as the Operation. The Notification method is critical to support component linking (see the Reuse through Inheritance and Composition online paper for more information on component linking).

A new component is destroyed and removed from the internal component list. When this happens, every component on the internal list is notified by calling the virtual method, Notification, passing a reference to the new component and specifying opRemove as the Operation.

The owner component is destroyed. When this happens, every owned component on the internal list is destroyed (and any components owned by these components are also recursively destroyed). That’s why you don’t need to explicitly destroy components that are owned, and this is part of what makes programming in Delphi so easy.

Contents White Papers Home Page


--------------------------------------------------------------------------------

[brown][h3]Dynamic Component Creation[/h3][/brown]
There are two ways to dynamically create components. One way is to make a form (or some other TComponent) the owner of the new component. This is a common practice when building composite components where a visual container creates and owns the subcomponents (see the Reuse through Inheritance and Composition online paper for more information on building composites). Doing so will ensure that the newly-created component is destroyed when the owning component is destroyed.

[brown][h3]Dynamic Creation with Owners[/h3][/brown]
Here's an example of dynamic creation, where Self is a TComponent or TComponent descendant (e.g., an instance of a TForm):

with TTimer.Create(Self) do
begin
Interval := 1000;
Enabled := False;
OnTimer := MyInternalTimerEventHandler;
end;
Dynamic Creation with an Explicit Call to Free
The second way to create a component is to use nil as the owner. Note that if you do this, you must also explicitly free the object you create as soon as you no longer need it. Here's an example of using nil as the owner:

with TTable.Create(nil) do
try
DataBaseName := 'MyAlias';
TableName := 'MyTable';
Open;
Edit;
FieldByName('Busy').AsBoolean := True;
Post;
finally { wrap up }
Free;
end
{ try/finally }
Dynamic Creation and Object References
It is possible to enhance the two previous examples by assigning the result of the Create call to a variable local to the method or belonging to the class. This is often desirable when references to the component need to be used later, or when scoping problems potentially caused by "With" blocks need to be avoided. Here's the TTimer creation code from above, using a field variable as a reference to the instantiated TTimer object:

FTimer := TTimer.Create(Self);
with FTimer do
begin
Interval := 1000;
Enabled := False;
OnTimer := MyInternalTimerEventHandler;
end;
In this example "FTimer" is a private field variable of the form or visual container (or whatever "Self" is). When accessing the FTimer variable from methods in this class, it is a very good idea to check to see if the reference is valid before using it. This is done using Delphi's Assigned function:

if Assigned(FTimer) then
FTimer.Enabled := True;
Dynamic Creation and Object References without Owners
A variation on this is to create the component with no owner, but maintain the reference for later destruction. The construction code for the TTimer would look like this:

FTimer := TTimer.Create(nil);
with FTimer do
begin
...
end;
And the destruction code (presumably in the form's destructor) would look something like this:

FTimer.Free;
FTimer := nil;
Setting the object reference to nil is critical when freeing objects. The call to Free first checks to see if the object reference is nil or not, and if it isn't, it calls the object's destructor Destroy.

[brown][h3]Dynamic Creation and Local Object References without Owners[/h3][/brown]
Here's the TTable creation code from above, using a local variable as a reference to the instantiated TTable object:

localTable := TTable.Create(nil);
try
with localTable do
begin
DataBaseName := 'MyAlias';
TableName := 'MyTable';
end;
...
// Later, if we want to explicitly specify scope:
localTable.Open;
localTable.Edit;
localTable.FieldByName('Busy').AsBoolean := True;
localTable.Post;
finally { wrap up }
localTable.Free;
localTable := nil;
end
{ try/finally }
In the example above, "localTable" is a local variable declared in the same method containing this code. Note that after freeing any object, in general it is a very good idea to set the reference to nil.

[blue][h2]A Word of Warning[/h2][/blue]
[red]IMPORTANT[/red]: [red]Do not mix a call to Free with passing a valid owner to the constructor.
All of the previous techniques will work and are valid, but the following should never occur in your code:[/red]

[red]with TTable.Create(self) do
begin
try
.......
finally
free

end;
end;[/red]

Figure 1. A bad code example of having both an owner and
an explicit call to Free in a dynamically-created component

The code example above introduces unnecessary performance hits, impacts memory slightly, and has the potential to introduce hard to find bugs. For more information on exactly why this is bad, click here.


[h1][blue]Dynamic Component Creation Gotcha (Don't Do This)[/blue][/h1]
Copyright &copy
1998, by Mark Miller

[brown][h3]Contents
Overview
Side-Effects
Summary[/h3][/brown]

[brown][h3]Overview[/h3][/brown]
In the Dynamic Component Creation paper we discussed the following code sample that demonstrated the wrong way to dynamically create and free a TComponent descendant that is only needed within a single method (created and freed in the same block of code). In the paper a warning was issued against dynamically creating components with valid owners and then explicitly freeing the new objects afterwards. The following is an example of this (the TTable class is used for this example, but the issues raised in this paper concern the dynamic creation of any TComponent descendant):

[red]with TTable.Create(self) do
begin
try
.......
finally
free

end;
end;[/red]
Figure 1. Don't do this!

One argument given for this technique is essentially that the "Self" parameter, when passed as the owner will ensure the dynamically created table will freed, in the event the programmer forgets to explicitly call Free later on. In other words, this code is sometimes promoted as a redundant safety net to guard against programmer error.

Looking at the code above, the "Free" statement will always be called as long as the TTable creation is successful, which means the Create class method returns without raising an exception. Because of this, the TTable component, no matter what, will be alive (instantiated, assigned, and valid) only within the try/finally code block above.

Unfortunately, this "redundant safety net" can introduce the following undesirable side-effects:

Side-Effects:
[h4]Conflicting and Confusing Code[/h4]
[h4]Expensive Code Maintenance[/h4]
[h4]Performance Hit on Construction[/h4]
[h4]Performance Hit on Destruction[/h4]
[h4]Unnecessary Use of Memory[/h4]
[h4]Possible Conflict with Linking Components[/h4]
[h4]Risk of a Double Free[/h4]

Note that these side effects can all be easily avoided just by using nil as the owner of the dynamically created component.


--------------------------------------------------------------------------------

[brown][h3]Conflicting and Confusing Code[/h3][/brown]
One problem with this code is that code readability suffers. The initial creation call where we pass "Self" as the owner conflicts with the Free method call in the Finally block. Passing Self as the owner means that Self will destroy the TTable instance we just created. The last line of code (the call to "Free;") means the dynamically created TTable will not be destroyed by Self, but instead it will be destroyed programmatically. This conflict in readability can increase the cost of maintaining the code.

Contents Side-Effects White Papers Home


--------------------------------------------------------------------------------

[brown][h3]Expensive Code Maintenance [/h3][/brown]
When a maintenance programmer encounters this code, how is the problem fixed? Should the Free method be removed, or should nil be the owner? Is there another component somewhere that has overridden the Notification method intentionally and is waiting for this local TTable to be created? Is this really a problem? Should it be fixed, or left alone? What was the original programmer's intention?

The fact that maintenance programmers need to even ask these questions indicates a serious problem. More problematic is that the answers to these questions can reside outside of the code sample presented here, requiring an intimate source-level understanding of every component in use on the form, as well as a good understanding of the context surrounding the code.

Contents Side-Effects White Papers Home


--------------------------------------------------------------------------------

[brown][h3]Performance Hit on Construction[/h3][/brown]
When our TTable is created, the Create constructor is called for TTable, and for each ancestor class that overrides the Create constructor, all the way to TComponent.Create. Here's the implementation for the TComponent.Create method:

constructor TComponent.Create(AOwner: TComponent);
begin
FComponentStyle := [csInheritable];
if AOwner <> nil then AOwner.InsertComponent(Self);
end;
Notice that if AOwner is nil, the Create constructor is extremely fast. However, if AOwner is not nil, then AOwner's InsertComponent method is called. InsertComponent looks like this:

procedure TComponent.InsertComponent(AComponent: TComponent);
begin
AComponent.ValidateContainer(Self);
ValidateRename(AComponent, '', AComponent.FName);
Insert(AComponent);
AComponent.SetReference(True);
if csDesigning in ComponentState then
AComponent.SetDesigning(True);
Notification(AComponent, opInsert);
end;
A number of methods are called here, but without a doubt the most expensive (in terms of having a negative impact on performance) is the call to Notification. Notification looks like this:

procedure TComponent.Notification(AComponent: TComponent
Operation: TOperation);
var
I: Integer;
begin
if (FFreeNotifies <> nil) and (Operation = opRemove) then
begin
FFreeNotifies.Remove(AComponent);
if FFreeNotifies.Count = 0 then
begin
FFreeNotifies.Free;
FFreeNotifies := nil;
end;
end;
if FComponents <> nil then
for I := 0 to FComponents.Count - 1 do
TComponent(FComponents).Notification(AComponent, Operation);
end;
This time-intensive code here is the last for-loop, which iterates through every single owned component, calling Notification again (it's a iterative call, so every component either owned or indirectly owned by the form (or whatever component was initially passed as the AOwner parameter), will have this method called. Additionally, the Notification method is virtual, so it can be overridden in descendant classes (and often is).

For more information on the uses of the Notification method see the Component Linking topic in the Reuse through Inheritance and Composition online paper.


Figure 2. Owner Impact on Performance

What exactly is the hit on performance? Click here to find out.


--------------------------------------------------------------------------------

[brown][h3]Performance Hit on Destruction[/h3][/brown]
When the TTable instance is destroyed (the call to "Free" in the Finally block), ultimately the TComponent destructor is called (Free checks the instance to see if it's non-nil and then calls Destroy). TComponent.Destroy looks like this:

destructor TComponent.Destroy;
var
I: Integer;
begin
if FFreeNotifies <> nil then
begin
for I := 0 to FFreeNotifies.Count - 1 do
TComponent(FFreeNotifies).Notification(Self, opRemove);
FFreeNotifies.Free;
FFreeNotifies := nil;
end;
Destroying;
DestroyComponents;
if FOwner <> nil then FOwner.RemoveComponent(Self);
inherited Destroy;
end;
Notice near the end of this method the check to see if "FOwner <> nil", followed by the call to the owner's RemoveComponent method. In other words, when this TTable instance was created, an owner was specified in the constructor, then we'll call that owner's RemoveComponent method later when the TTable instance is destroyed. RemoveComponent looks like this:

procedure TComponent.RemoveComponent(AComponent: TComponent);
begin
Notification(AComponent, opRemove);
AComponent.SetReference(False);
Remove(AComponent);
AComponent.SetDesigning(False);
ValidateRename(AComponent, AComponent.FName, '');
end;
Notice at the beginning of this method the call to Notification (passing opRemove). Notification, as we've already seen above, is a iterative call and it's a virtual method. So the additionally unnecessary performance hit gets you twice. Once on creation and once on destruction. This performance hit can be completely avoided by passing nil as the parameter to our TTable instance that is locally created and freed.

When informed of this performance hit, it is sometimes suggested to use Application as the owner instead of Self (in the original code example "Self" was a TForm). This suggestion was based on the fact that statistically, the Application object will tend to own fewer components (e.g., just the auto-create forms), than a form would. While this may be true, passing the Application object as the owner actually has an even more severe impact on performance, because the Notification call iterates through each component owned by the owner. This means that every component on every form owned by the Application object will get it's Notification method called, in addition to every TComponent descendant owned by the Application directly.


--------------------------------------------------------------------------------

[brown][h3]Unnecessary Use of Memory[/h3][/brown]
When you specify an owner in the constructor of a TComponent, the owner's internal component list gains a new reference to your component. If the owner does not own any components, the list does not yet exist and is subsequently created. The amount of memory consumed here could easily be called negligible, but it is unnecessary all the same.


--------------------------------------------------------------------------------

[h3][red]Possible Conflict with Linking Components[/red][/h3]
Notification is used for a number of purposes. One of these uses is auto-hookup. Auto-hookup occurs when a component that needs to link to another component overrides the Notification method, looking for a component of that type. When it's found (assuming the linked property is not already set), it connects automatically. A number of third-party component packages have these. Here's a variation of auto-hookup from the VCL (taken from DBTables.pas):

procedure TSession.Notification(AComponent: TComponent
Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if AutoSessionName and (Operation = opInsert) then
if AComponent is TDBDataSet then
TDBDataSet(AComponent).FSessionName := Self.SessionName
else if AComponent is TDatabase then
TDatabase(AComponent).FSession := Self;
end

When "Self" is the owner, Delphi will pass your dynamically created component to every other component on the form, unnecessarily exposing your component to unexpected third-party auto-hookup code. This code can change your component's properties and assign handlers to it's events.


--------------------------------------------------------------------------------

[brown][h3]Risk of a Double Free[/h3][/brown]
This code also introduces the risk of the Delphi attempting to free the dynamically-created instance twice (this is a bad thing, and can result in AVs and other problems). This will happen if the owner is freed within the Try/Finally block.

This could happen unexpectedly if the code inside the Try block was time-intensive, and a method within the block directly or indirectly called Application.ProcessMessages. If this condition existed, and the user closed the form while execution was in the Try block, then the form would be destroyed in the call to Application.ProcessMessages. When the form is destroyed, it also destroys all owned components (including the TTable). At this point in the execution, the reference created by "TTable.Create" is invalid. Any further references to that TTable object (e.g., calling methods or setting properties of TTable) would most likely result in access violations, as would the final call to Free. If this were to occur, it would be a problem that would be extremely difficult to find, debug, and ultimately trace back to the fact that you should have passed nil in as the owner to the dynamically created and locally-used TTable.


--------------------------------------------------------------------------------

[brown][h2]Summary[/h2][/brown]
If you want to dynamically create a component and explicitly free it sometime later, always pass nil as the owner.
Failure to do so can introduce unnecessary risk, as well as performance and code maintenance problems.
 

Similar threads

A
回复
0
查看
981
Andreas Hausladen
A
A
回复
0
查看
932
Andreas Hausladen
A
A
回复
0
查看
400
Andreas Hausladen
A
A
回复
0
查看
607
Andreas Hausladen
A
A
回复
0
查看
805
Andreas Hausladen
A
后退
顶部