< Day Day Up > |
As we learned in Chapter 2, classes use properties to store information. But some information relates to a class as a whole (e.g., the default size for all SpaceShip objects) and some information relates to individual objects (e.g., the current speed or position of each SpaceShip instance). Consequently, properties come in different varieties:
In our coverage of properties, we'll concentrate first on instance properties because they are by far the most common type of property. We'll also largely ignore dynamic properties because they are considered bad form in ActionScript 2.0 despite being somewhat common in ActionScript 1.0. In ActionScript and ECMAScript 4, instance properties are sometimes called instance variables , and class properties are sometimes called class variables (following Java's terminology). Instance variables and class variables should not be confused with the normal ActionScript variables that can be created without reference to an object or a class. To keep things clear, we'll avoid the terms instance variable and class variable, but you should expect to see them in other technical documentation and even in ActionScript compiler error messages. Furthermore, to avoid awkward wordiness, we'll often use the shorter term , property , to mean instance property . Unless otherwise noted, you should assume that all properties discussed in this book are instance properties, not class properties. On the other hand, we'll nearly always refer to class properties by the full name , "class property," unless the context makes the type of property abundantly clear. Finally, don't confuse instance properties ”which are declared once for the class, but for which each instance maintains it own value ”with the dynamic properties discussed under "The dynamic attribute." Dynamic properties are properties defined solely for a single instance (not for all instances). Instance properties typically store information that describes an object's state. Instance properties:
The key thing to remember is that an instance property is declared once for the entire class, but each instance of the class maintains its own value. For example, every movie clip has its own _rotation instance property that indicates the amount, in degrees, that the clip is rotated . The _rotation instance property is declared once in the MovieClip class, but each movie clip (each an instance of the MovieClip class) defines its own value for _rotation . If we have two MovieClip instances, circle_mc and square_mc , we can set the instance property _rotation to 45 on circle_mc and to 120 on square_mc , causing each instance to be rotated individually on screen. To define an instance property, we use the var statement within the body of a class definition. The general syntax is: var propertyName : datatype = value ; where propertyName is the property's identifier (case sensitive in ActionScript 2.0), datatype is the built-in or custom datatype of the property, and value is the optional default value for the property ( value must be a compile-time constant expression , as discussed later).
If datatype is omitted, no type checking is performed on the property (allowing it to store any value). For example: var propertyName = value ; // No type checking for this property. If the default value is omitted, then the default is undefined : var propertyName : datatype ; // Property set to undefined .
Let's see what an ActionScript 2.0 instance property definition looks like in real life by adding height and width properties to our Box class: class Box { var width:Number; var height:Number; } Notice that lines 2 and 3 of the preceding code sample end with " :Number ", which, as we learned in Chapter 3, is the type declaration of each property definition. It indicates that our width and height properties must store numeric data (i.e., data of the Number datatype). As a result, any attempt to assign a nonnumeric value to the width or height properties will cause a compile-time error. For example, the following code attempts to assign the String value "tall" to the numeric property height : var b:Box = new Box( ); b.height = "tall"; which results in the following error: Type mismatch in assignment statement: Found String where Number is required. In ActionScript 2.0, all properties must be declared explicitly using var , whether or not you also specify their datatype or initial value. That is, you must use at least the following minimal declaration before making reference to the height property in later code: var height; Making reference to a property that doesn't exist causes an error (unless the class that defines the property is declared dynamic ). For example, the following code attempts to assign a value to the nonexistent description property: var b:Box = new Box( ); b.description = "A square object"; which results in the following error: There is no property with the name 'description'. To fix the problem, you could add the line shown in bold to the Box class definition: class Box { var width:Number; var height:Number; var description:String; }
Even if our Box class defines a property by the name description , it's often considered bad form to access properties directly from outside a class, as does the code: var b:Box = new Box( ); b.description = "A square object"; When a property is accessible to code outside a class, code that uses the property becomes dependent on the class definition, limiting its flexibility. If, for example, we were to rename the description property to desc , we'd also have to change all code outside the class that referred to description . If enough external code relies on a class's property name, changing the name can become unfeasible. Furthermore, when a property is accessible outside a class, class-external code might unwittingly assign illogical or illegal values to the property. For example, code external to the Box class might assign a negative or fractional value to the height property, which may not be considered legal by the Box class. In order to prevent external access to a class's properties, we use the private attribute. What luck, we're discussing the private property attribute next ! 4.3.1 Property AttributesWe saw earlier that class definitions could be modified by the dynamic and instrinsic attributes. Property definitions can also be modified with three possible attributes ” public , private , and static ” which control how and where the property can be accessed. These attributes must be listed before the keyword var in a property definition. For example, to add the public attribute to the height property of our Box class, we'd use: class Box { public var height:Number; } When a property has more than one attribute, the attributes can appear in any order. However, the public or private attribute is conventionally placed before the static attribute, as in: public static var someProperty : someDataType = someValue ; Furthermore, the public and private attributes cannot both be used to modify the same property definition; they are mutually exclusive. 4.3.1.1 Restricting property access with the private and public attributesThe private and public attributes are known as access control modifiers because they determine whether a property is accessible outside of the class that defines it. To make a property inaccessible to code outside of a class, we use the private attribute. For example, to make our Box class's height and width properties inaccessible to code outside of the Box class, we use: class Box { private var width:Number; private var height:Number; } Now suppose we place the following code somewhere outside of the Box class: var b:Box = new Box( ); b.height = 10; When that code is compiled, the following error appears in the Output panel: The member is private and cannot be accessed. The somewhat unusual exception to this rule is that code in a subclass can access private properties defined by its superclass. In most other OOP languages (e.g., Java and C++), private members are accessible within a single class only, not its subclasses. There is no way to impose a similar restriction in ActionScript 2.0.
By defining a class's properties as private , we keep the class's information safely encapsulated, preventing other code from relying too heavily on the internal structure of the class or accidentally assigning invalid values to properties. When a private property represents some aspect of an object that should be externally modifiable (e.g., the height of a box), we must provide an accessor method as an external means of making the modification. We'll learn more about accessor methods when we cover methods, later in this chapter. If a property definition does not include the private attribute, the property is considered public, meaning that it is accessible to code anywhere (not just to code within the class that defines the property). That is, all properties are public unless otherwise specified. If, however, we wish to show that a property is public by intention , not just by default, we can use the optional public attribute, as follows : class Box { public var width:Number; public var height:Number; }
In general, it's good form to specify either the public or private attribute explicitly for every property. When a property is defined as public , we can rest assured that the programmer made a conscious decision to expose the property (i.e., make it publicly accessible). A designation of public says "use this property freely without worrying that it may become private in the future." By the same token, no property should be made public unless it is specifically required to be so by the class's architecture. If you are unsure whether to make a property public or private, make it private. Down the road, you can easily make the property public if required. By contrast, if you start with a public property, you'll have a tough time changing it to private later if external code already relies on it.
Note that ActionScript does not support the protected access control modifier found in Java and C++. The rules governing ActionScript's access control modifiers are relatively simple (albeit nonstandard): private properties are accessible to code in a class and its subclasses; public properties are accessible from anywhere. The rules for Java and C++ access control modifiers are somewhat more complex. For comparison, Table 4-1 lists the meanings of public , protected , and private in ActionScript, Java, and C++. In all cases, the public and private attributes are mutually exclusive. (Note that Table 4-1 focuses strictly on access modifiers in various languages and, therefore, does not include other property attributes such as static. ) Table 4-1. Access control modifiers in ActionScript, Java, and C++
4.3.1.2 Defining class properties with the static attributeThe static attribute determines whether a property is associated with instances of the class or with the class itself. By default, property definitions create instance properties. To indicate that a property should be treated as a class property instead of an instance property, we precede it with the keyword static , which is traditionally placed after the keyword public or private and before the keyword var . Because the static attribute is used to define a class property, class properties are sometimes called static properties . Our Box class could define a class property, numSides , that indicates the number of sides all Box instances have, as follows: class Box { public static var numSides:Number = 4; } Class properties are accessed through a class directly, independent of any object instance. They are used to store information that relates logically to an entire class, as opposed to information that varies from instance to instance.
Within the class that defines a class property (and any of its subclasses), a class property can be accessed either by name directly, as in numSides , or through the class, as in Box.numSides . For example, this code, if written within the class definition: trace("A box has " + numSides + " sides."); is synonymous with this code: trace("A box has " + Box.numSides + " sides."); Outside of its defining class, a class property can be accessed only through the class name, as in Box.numSides (and only if the property is not declared private ). For example, because numSides is defined as public , we can use the following code outside the Box class definition: // Displays: A box has 4 sides. trace("A box has " + Box.numSides + " sides."); By contrast, the following code, which does not qualify the property name numSides with the class name Box , is valid only inside the Box class (or any of its subclasses): trace("A box has " + numSides + " sides."); You cannot access a class property through a reference to an instance of a class. For example, if we attempt to access numSides through b (a Box instance), as follows: var b:Box = new Box( ) b.numSides; the following error appears in the Output panel: Static members can only be accessed directly through classes. A class property stores only a single value. If that value changes due to, say, a method call on a particular instance, the change is universal. In Example 4-1, we add two methods to our Box class, one to set the value of Box.numSides and one to retrieve it. No matter which instance invokes the methods, they operate on a single numSides property value. (We haven't studied methods yet, so you may want to return to this code later once you're familiar with method definitions.) Example 4-1. Accessing a static property through a method// The Box class, with methods to set and retrieve the // value of the class property, numSides . class Box { // The class property. private static var numSides:Number = 4; // The method that sets numSides . public function setNumSides (newNumSides:Number):Void { numSides = newNumSides; } // The method that retrieves numSides . public function getNumSides ( ):Number { return numSides; } } // *** Code placed somewhere outside the Box class.*** // Make two unique Box instances. var box1:Box = new Box( ); var box2:Box = new Box( ); // Check the value of numSides for each instance. trace(box1.getNumSides( )); // Displays: 4 trace(box2.getNumSides( )); // Displays: 4 // Set numSides through instance box1 . box1.setNumSides(5); // Retrieve numSides through instance box2 . trace(box2.getNumSides( )); // Displays: 5 // Both box1 and box2 accessed the single // value for numSides . You may be saying, "Wait a minute! I thought class properties cannot be accessed via instances but only directly through the class. Why does the code access numSides through the box1 and box2 instances?" You are correct that class properties cannot be accessed directly via an instance. The following is invalid: box2.numSides However, Example 4-1 does not access the class property numSides directly through an instance. Instead, it accesses the class property through a method, getNumSides( ) , which is accessible to instances of the class. And if you're wondering, "Is there such a thing as a static method?" Yes, there is. We'll discuss static methods later.
Class properties are useful for sharing data across objects of a class or even between classes. For example, in Chapter 5 we'll study an ImageViewer class that creates a rectangular region for displaying an image on screen. The ImageViewer class needs to know which depths it should use when creating the movie clips that make up the rectangular region. The depths are the same for every ImageViewer instance, so they are best implemented as class properties. Here's an excerpt of the ImageViewer class that defines the depth- related properties: class ImageViewer { // Depths for visual assets private static var imageDepth:Number = 0; private static var maskDepth:Number = 1; private static var borderDepth:Number = 2; // Remainder of class definition not shown... } A class property might also provide default values to use when constructing instances of a class. Here we add defaultHeight and defaultWidth as class properties to the Box class. If values for h and w aren't supplied to the Box constructor, it uses the defaults (we'll cover constructor functions in detail later): class Box { private var width:Number; private var height:Number; private static var defaultWidth:Number = 30; private static var defaultHeight:Number = 20; public function Box (w:Number, h:Number) { if (w == undefined) { w = defaultWidth; } if (h == undefined) { h = defaultHeight; } width = w; height = h; } } We might similarly add maxHeight and maxWidth class properties to use to check for a valid Box size when constructing Box instances. Class properties can also be handy for storing global counters for a class, such as a property to track the number of instances that have been created. Or, a class property might have nothing to do whatsoever with its instances ”some classes are simply collections of related class properties and are never actually instantiated . The built-in Math class is a good example of this; it consolidates numerical constants such as PI and numerical functions such as max( ) and min( ) . As another example, consider a typical application in which we might have an AppStartupSettings class that maintains a set of application-wide startup parameters as class properties: class AppStartupSettings { public static var CONFIG_LOCATION:String = "config.xml"; public static var SOUND_ENABLED:Boolean = true; public static var SHOW_TIP:Boolean = true; } In some cases, we might want to guarantee that the preceding settings never change (i.e., make the property values constant ). The ECMAScript 4 standard provides a means of making that guarantee, via the final attribute, but ActionScript 2.0 does not support final . Following a common convention in other languages, we use ALL CAPS when naming static properties that should not be modified by a program (i.e., should be treated as constants by other programmers).
4.3.1.3 Global variables versus class propertiesIn addition to class properties, ActionScript supports global variables , which are created like this: _global. someVariable = someValue ; Notice that no datatype is specified when a global variable is created. ActionScript 2.0 does not support static typing for global variables. Global variables are accessible from anywhere in a program and were commonly used in ActionScript 1.0. While global variables are legal in ActionScript 2.0, using them is considered unsafe OOP practice. In an OOP application, information needed across classes should be stored in class properties, both for the sake of organization and to prevent variable name collisions (i.e., cases in which separate classes or .swf files create global variables of the same name). Java, for comparison, does not support global variables at all and expects all cross-class data to be stored in static variables (the equivalent of ActionScript's class properties). In general, you should try to place your class properties in the classes that use them. If more than one class needs access to the same information, consider creating a separate class, such as the earlier AppStartupSettings class, that provides a central point of access to the information. 4.3.1.4 Subclasses and class propertiesIf you're new to the concept of overriding properties and defining subclasses with the extends keyword, you should skip this section for now and return to it once you've read Chapter 6. When a class property in a superclass is not overridden by a subclass, the property maintains a single value that is accessible via both the subclass and the superclass. For example, suppose an Employee class defines a class property, defaultSalary . Further suppose a Manager class extends the Employee class but does not override the defaultSalary property. Here's the code, greatly simplified to show only the defaultSalary property definition: // Employee class class Employee { public static var defaultSalary:Number = 34000; } // Manager class class Manager extends Employee { } In this situation, the references to the properties Employee.defaultSalary and Manager.defaultSalary are synonymous: trace(Employee.defaultSalary); // Displays: 34000 trace(Manager.defaultSalary); // Displays: 34000 Furthermore, changes made to defaultSalary via Employee.defaultSalary are reflected in Manager.defaultSalary and vice versa: trace(Employee.defaultSalary); // Displays: 34000 Manager.defaultSalary = 40000; trace(Employee.defaultSalary); // Displays: 40000 Employee.defaultSalary = 50000; trace(Manager.defaultSalary); // Displays: 50000 By contrast, when a property in a superclass is overridden by a subclass, the property in the subclass maintains a unique value, distinct from the property's value in the superclass. Let's rewrite our Manager class so that it overrides the Employee class property defaultSalary (changes shown in bold): // Employee class class Employee { public static var defaultSalary:Number = 34000; } // Manager class class Manager extends Employee { // Override defaultSalary public static var defaultSalary:Number = 60000; } Now the properties Employee.defaultSalary and Manager.defaultSalary are different: // Check defaultSalary 's value for each class trace(Employee.defaultSalary); // Displays: 34000 trace(Manager.defaultSalary); // Displays: 60000 And changes made to Employee.defaultSalary are separate from Manager.defaultSalary and vice versa: // Change Manager 's defaultSalary value Manager.defaultSalary = 70000; // Check defaultSalary 's value for each class trace(Employee.defaultSalary); // Displays: 34000 trace(Manager.defaultSalary); // Displays: 70000 // Change Employee 's defaultSalary value Employee.defaultSalary = 20000; // Check defaultSalary 's value for each class trace(Employee.defaultSalary); // Displays: 20000 trace(Manager.defaultSalary); // Displays: 70000 For more information on overriding properties, see Chapter 6.
For example, in the following code, the class property Employee.defaultSalary is inherited by Manager : // Code in Employee.as class Employee { public static var defaultSalary:Number = 34000; } // Code in Manager.as class Manager extends Employee { } If, outside of the Employee and Manager classes, we attempt to access Manager.defaultSalary before referencing the Employee class, then the property will be undefined : // Fails when this code appears outside of Employee.as and Manager.as trace(Manager.defaultSalary); // Displays: undefined To fix the problem, we simply refer to the Employee class before accessing the property: // Refer to Employee Employee; // Now it works trace(Manager.defaultSalary); // Displays: 34000 Note, however, that the inherited class property bug does not cause problems for code inside the subclass. For example, the following code in Manager works without any special reference to Employee : class Manager extends Employee { function paySalary ( ):Void { // This reference is valid because Employee is referenced // in the Manager class definition. var salary:Number = Manager.defaultSalary; // Remainder of method not shown... } } 4.3.2 Compile-Time Constant ExpressionsEarlier, we learned that an instance property definition can assign a default value to a property, provided that the value is a so-called compile-time constant expression . A compile-time constant expression is an expression whose value can be fully determined at compile time (i.e., it does not rely on any values set at runtime). Specifically, a compile-time constant expression may include only:
Examples of valid compile-time constant expressions are: 4 + 5 "Hello" + " world" null [4, 5] new Array(4, 5) {a: 5, b: 10} new Object( ) The following code assigns the value of DEFAULT_HEIGHT to height , which is legal because DEFAULT_HEIGHT contains a compile-time constant expression: class Box { private var DEFAULT_HEIGHT:Number = 10; private var height:Number = DEFAULT_HEIGHT; } Compile-time constant expressions cannot contain function calls, method calls, object construction, or any other value that cannot be entirely determined at compile time. For example, the following code attempts to use parseInt( ) in the calculation of an instance property value: private var area = parseInt("100.5"); which results in the following error: A class's instance variables may only be initialized to compile-time constant expressions. This code attempts illegally to assign an object as the initial value of the property target : private var target:MovieClip = _root; // ERROR! // References to objects are // not compile-time constants. Fortunately, it's easy to work around the compile-time constant requirement placed on instance properties. Simply move the initialization code to the class's constructor function. For example, to initialize an area property for our Box class, we would use the following code: class Box { // Property definition private var width:Number; private var height:Number; private var area:Number; // Constructor public function Box (w:Number, h:Number) { width = w; height = h; // Initialize area . This is perfectly legal within a constructor. area = width * height; } } 4.3.3 Enumerating Properties with for-in loopsEven if you are familiar with core ActionScript 1.0 syntax such as for-in loops (and this book assumes you are), you may not be aware of their subtle interaction with object-oriented aspects of the language. Specifically, although the for-in statement lets us list (a.k.a. enumerate ) properties of an object, not all properties are enumerable. The following example displays the enumerable properties of obj in the Output panel: for (var prop:String in obj) { trace("property name: " + prop); trace("property value: " + obj[prop]); } By default, instance properties (i.e., properties not declared as static ) are not enumerable with a for-in loop. For example, suppose our Box class defines two public properties, width and height , and initializes them both to the value 15: class Box { public var width:Number = 15; public var height:Number = 15; } When we create a new Box object, its width and height properties are set to 15: var b:Box = new Box( ); trace(b.width); // Displays: 15 trace(b.height); // Displays: 15 But when we enumerate the properties of the Box object, width and height are not listed: // This loop generates no output for (var prop:String in b) { trace("property name: " + prop); trace("property value: " + b[prop]); }
By contrast, class properties (i.e., properties declared as static ) are enumerable with a for-in loop. For example, suppose our Box class defines two class properties, maxWidth and maxHeight , and initializes them both to 250: class Box { public static var maxWidth:Number = 250; public static var maxHeight:Number = 250; } We can use a for-in loop to list the Box class's class properties: for (var prop:String in Box) { trace("property name: " + prop); trace("property value: " + Box[prop]); } // Displays: property name: maxHeight property value: 250 property name: maxWidth property value: 250 Perhaps surprisingly, class properties are enumerable even when they are declared private . Dynamic properties (new properties added to an individual instance of a class declared as dynamic ) are also enumerable, as shown in this example: // The dynamic class definition dynamic class Box { public var width:Number = 15; public var height:Number = 15; } // Create an instance var b:Box = new Box( ); // Add a new dynamic property to the instance b.newProp = "hello world"; // Enumerate b 's properties for (var prop in b) { trace("property name: " + prop); trace("property value: " + b[prop]); } // Displays: property name: newProp property value: hello world Notice in the preceding example that the dynamic property, newProp , is enumerated, but the instance properties width and height are not.
|
< Day Day Up > |