Since the time we program with object orientation, we can play at being all-powerful, deciding the lifetime of our objects, and limiting "what they can, and what they can't, do". Now, in some cases these objects seem to take on a life of their own, and don't respect our wishes. As the old saying goes, "the computer does what I tell it to do, not always what I want it to do".
But, why does this happen? Normally this happens when we haven't done a good design, or haven't correctly established the scope of our objects; this will be the subject of this article.
Scope
Objects are normally created and assigned to a variable.
Visual FoxPro has three different scopes that can be assigned to a variable, namely Public, Private, and Local.
Public is the definition we should never used, since a public variable can be accessed from any place of our application, even from a parent object or an object totally unrelated to the object assigned to a public variable. As a result, in some cases we would have access to an object, in others we won't; or an object mysteriously changes its value (anybody, in any part of the application, can change the value, and this will affect other parts of the program). Conclusion: Our applications should not have any public variable.
Private is the scope we should reserve for the cases where we need a variable to be accessed by some object that is related to the object where the variable was created. The classical example is when in a program we create a variable that will be printed in a report. Conclusion: Use sparingly.
Local is the scope we should assign to all our variables, since this makes it available only within the method or procedure where it was created. Conclusion: The fact that we define a variable as local will guarantee that, in most cases, when a variable ends its scope (the method or procedure that creates it finishes ejecution), our object will die.
LOCAL loFirst, loSecond loFirst = CREATEOBJECT("MyFirstClass") loSecond = CREATEOBJECT("MySecondClass") RETURN *--------------------------------------------- DEFINE CLASS MyFirstClass as Custom PROCEDURE DESTROY WAIT WINDOW This.Name ENDPROC ENDDEFINE *--------------------------------------------- DEFINE CLASS MySecondClass as MyFirstClass ENDDEFINE
In this simple example, we see that, even though we didn't explicitly kill the objects loFirst and loSecond, both die when the scope where they were created finishes.
Note: It is recommended, as a good practice, to explicitly kill the objects. This would be as in the following example (that is not much different from the previous one, but explicitly kills the object instances); from the two possibilities of eliminating an object, I prefer to assign NULL rather than RELEASE, since an object assigned a NULL in an evaluation will return "X" in a type evaluation, whereas an object freed with RELEASE will return "U" as if it had never existed.
LOCAL loFirst, loSecond loFirst = CREATEOBJECT("MyFirstClass") loSecond = CREATEOBJECT("MySecondClass") loFirst = NULL loSecond = NULL RETURN *--------------------------------------------- DEFINE CLASS MyFirstClass as Custom PROCEDURE DESTROY WAIT WINDOW This.Name ENDPROC ENDDEFINE *--------------------------------------------- DEFINE CLASS MySecondClass as MyFirstClass ENDDEFINE
A few paragraphs before, I said "in most cases", and here we must have the design idea very clear, and not create a monster that decides for itself how long it should live, even if we decide to kill an object, either by assigning a NULL to the object or executing the RELEASE instruction. Here we can see some examples of a bad design that results in our objects not being killed when we decide to kill it.
In this first example, we can see one of the cases where VFP can't destroy objects, even when they are out of scope.
* To understand this example, execute this program twice from the Command Window, * and then type Clear All in the Command Window. * * Before the "Clear All", the objects will remain in memory. * After the "Clear All", the Destroy method will execute twice for every object. LOCAL loFirst AS Object, loSecond AS Object *-- Create an instance of MyFirstClass and assign it to loFirst * at this moment, the name table index (for MyFirstClass) * has a value of 1 loFirst = CREATEOBJECT("MyFirstClass") MESSAGEBOX("First object created") *-- Create an instance of MySecondClass and assign it to loSecond * at this moment, the name table index (for MySecondClass) * has a value of 1. loSecond = CREATEOBJECT("MySecondClass") MESSAGEBOX("Second object created") *-- Now, the object loFirst is assigned a property in loSecond * and the Name Table Index (for MyFirstClass) becomes 2 loSecond.Pointer = loFirst MESSAGEBOX("Second object has a pointer to the first object") *-- Now, the object loSecond is assigned a property in loFirst * and the Name Table Index (for MySecondClass) becomes 2 * with a circular reference loFirst.Pointer = loSecond MESSAGEBOX("First object has a pointer to the second object, and a circular reference is created") MESSAGEBOX("Even when the variables get out of scope, DESTROY is not executed " +; + CHR(13) + CHR(13) + "REMEMBER TO EXECUTE CLEAR ALL AFTER THE SECOND RUN. ") RETURN *--------------------------------------------- DEFINE CLASS MyFirstClass as Custom Pointer = NULL PROCEDURE DESTROY MESSAGEBOX("Destroying " + This.Name) ENDPROC ENDDEFINE *--------------------------------------------- DEFINE CLASS MySecondClass as MyFirstClass ENDDEFINE
In this second example, we see a bad design, with a pointer to an object that should have been destroyed, due to the pointer it still exists.
LOCAL loForm AS FORM, loTextbox AS TEXTBOX * -- In this example, we are doing two different things: * 1 – Creating an instance of a form (MyForm) * 2 – The form creates a Textbox * Name Table Index (for the form) is 1 * Name Table Index (for the textbox) is 1 loForm = CREATEOBJECT("MyForm") * -- Another pointer is assigned to the Textbox * Name Table Index (for the textbox) is 2 loTextbox = loForm.txtTest READ EVENTS *-- Now, Name Table Index (for the form) will be 0 * The method Destroy is executed, and the Name Table Index (for the textbox) goes back to 1 * But, since the textbox still has a reference to the form, it can't be destroyed. loForm = .NULL. =MESSAGEBOX('Variable loform has been assigned a NULL' + CHR(13) + CHR(13) + ; ' - loForm = .Null.' + CHR(13) + CHR(13) + ; 'But it can't be freed from memory, since a circular reference to the textbox exists' + CHR(13) + ; 'The value of VARTYPE(loForm) is "' + VARTYPE(loForm) + '"' + CHR(13) + ; 'and the value of loTextbox.value is "' + ALLTRIM(loTextbox.VALUE) + '"' + CHR(13) + CHR(13) + ; 'and ... the form is visible.') *-- Finally, the Garbage Collector can eliminate the references loTextbox = NULL =MESSAGEBOX('Finally, the form is dead' + CHR(13) + CHR(13) + ; 'The reference to the textbox was assigned a NULL and the form was released.' + CHR(13) + CHR(13) + ; ' - loTextbox = NULL') RETURN DEFINE CLASS MyForm AS FORM ADD OBJECT txtTest AS TEXTBOX ADD OBJECT lblTest AS LABEL PROCEDURE INIT() WITH THIS .txtTest.LEFT = 10 .txtTest.TOP = 40 .txtTest.VALUE = "Write something:" .txtTest.WIDTH = 280 .lblTest.LEFT = 10 .lblTest.TOP = 80 .lblTest.WIDTH = 280 .lblTest.CAPTION = "When you finish, press the X on the form to close it" .caption = "Demo: multiple references to an object " + TTOC(DATETIME(),1) .AUTOCENTER = .T. .VISIBLE = .T. ENDWITH ENDPROC PROCEDURE DESTROY MESSAGEBOX("Destroying the form... ") CLEAR EVENTS ENDPROC ENDDEFINE
Conclusion
After doing some research on the scope of objects, assigning a NULL, using RELEASE, and thinking about the design, we can state:
With these recommendations, we can have a feeling of being all-powerful over our applications.