- Object Variables
- Class Definition Syntax
- Subclassing and Inheritance
- Fields
- Methods
- Constructors
- Finalizers
- Class Properties
- Super
- Statically Bound Method Calls
- Generic Copy
- Modify Notice
- Aliases
- Object Lifetime
Class definitions introduce new reference types called classes and define their members, which may be fields, constructors, finalizers, and methods. The instances of class types are called objects.
Object Variables
Classes are reference types, and as such, all object variables (fields, globals, locals, arguments) only refer to an object, rather than being that object. In other words, the actual value of an object variable is a pointer--the memory address where an object actually resides (if non-null).
Thus, an assignment from one object variable to another results in the latter referring to the same object:
public class ObjGuy { public int num; public constructor() {} } { // Declaration of object variable `guyOne` + construction of a new // instance of the ObjGuy class and assignment of the latter's pointer // to the former ObjGuy guyOne(); // ...same for 'guyTwo' ObjGuy guyTwo(); // Declaration of object variable `dupGuy` + assignment of the value of // `guyOne` to it ObjGuy dupGuy = guyOne; // Usage of an object variable in a string context evaluations to a call // to the toS() method on the object the variable points to, so here we // are asking that object for its own address in order to show the // ACTUAL value of the variable. // readableId() provides a simplified (but not necessarily 1:1) reduction // of the pointer for easier (human-readable) indentification of // individual instances when debugging. pln(#guyOne.pointer; readableId(guyOne)); pln(#guyTwo.pointer; readableId(guyTwo)); pln(#dupGuy.pointer; readableId(dupGuy)); pln(#guyOne.num; #guyTwo.num; #dupGuy.num); // Set field on the object that both `guyOne` and `dupGuy` point to dupGuy.num = 22; pln(#guyOne.num; #guyTwo.num; #dupGuy.num); }
Output: guyOne.pointer=0x00007FF418A294E8, 10 guyTwo.pointer=0x00007FF418A29508, 42 dupGuy.pointer=0x00007FF418A294E8, 10 guyOne.num=0, guyTwo.num=0, dupGuy.num=0 guyOne.num=22, guyTwo.num=0, dupGuy.num=22
Assigning an object variable to an independent instance based on another can be done via copying.
The fact that the actual value of object variables is a pointer also means that by default, comparing two object variables results in the comparison of the pointers regardless of whether they point to objects that could be considered equivalent.
Class Definition Syntax
class-definition: export class id extends-clauseopt class-prop-listopt class-body extends-clause: extends id class-prop-list: : class-props class-props: class-props, class-property class-property class-body: { membersopt } members: member members member member: field-definition ; method-definition constructor-definition
Subclassing and Inheritance
The inheritance hierarchy for non-rigid classes forms a tree with the Object
class at the root, in other words, all classes except rigids have a common ancestor, and multiple inheritances are not supported. The identifier after the optional extends
keyword names the immediate ancestor of the class, called its superclass. The class inherits from Object
if extends-clause is omitted.
Rigid classes are typically classes that have been defined in C++. See the Class Properties section for more about rigid classes.
Fields
A field is a member that represents an instance variable. It must have a type, name, and export declaration, and may contain a set of properties specifying additional attributes.
field-definition: export type id property-listopt ; property-list : field-properties field-properties: field-properties, field-property field-property field-properties: stream = null public readable package readable ignore modify notice method domain id domain expr copy-property copy-property: copy = 0 copy = null copy = reference copy = shallow copy = function id
The export modifier controls the visibility of the field to non-members. Unless the export modifier is public
, the properties public readable
and package readable
can be used to extend read access to everyone or to other members of the same package, respectively, without affecting write access.
- The
stream=null
property tells the object streaming facility to ignore the field, thus saving space.
When Should a Field Be Streamed?
All fields in a snapper class, by default, are streamed.
Only parameters that aren't constant and are absolutely necessary for defining the snapper (system) should be streamed. Fields that do not fall under the aforementioned category should be marked stream=null
.
Here's what _graph
in GraphSnapper
(cm/core/graphSnapper.cm) looks like:
/** * 2D graph. */ private Graph _graph : copy=reference, stream=null, ignore modify notice, public readable;
_graph
shouldn't be streamed as it can be derived from the parameters that define the snapper
Why Can't I Just Stream Everything?
This is because users might have saved instances of the snapper class in their drawings, and they should be able to load these snappers in even after a new version of the extension is released.
Snapper
, for example, has been modified hundreds of times since it was first created. loadFailed()
contains a chunk of code that reads values from old fields; fields that aren't used anymore. This is to ensure that the drawing loaded in by the user appears exactly as it did when it was saved.
Methods
A method is a function that is a member of a class. Methods that are not final can be overridden in subclasses. The receiver of the call is then selected at runtime by inspecting the object's type. More precisely, the call goes through a V-Table.
The least specific implementation of a method must be marked with extend
to signal that it extends the object protocol with a new method.
extend public int foo() { return 1; }
If the return type of a method is a class, overriding methods may change the return type into a subclass of the parent's return type. For other types, overriders must return the exact same type as their parent method.
Final
methods can only have one implementation and are bound at compile time. Calls to final
methods are generally faster than calls to dynamically bound functions, but cannot be overridden by subclasses.
final public int foo() { return 1; }
It also shares some behaviors like functions, which is documented here:
Functions
Constructors
constructor-definition: export constructor ( formal-argument-listopt ) { method-body }
Constructors are special member functions that are used to initialize new instances of a class. The newly allocated object is passed as a hidden parameter named "this" to the constructor function, which is then responsible for setting the fields as appropriate. Constructors, just like final
methods, are not virtual since the exact type of the object is known at compile time.
The constructors of the superclass will be inherited by subclasses in two cases:
- when the subclass does not define any constructors of its own
- when the
inherit constructors
class property is used
Field assignments supplied in the definition of a field are injected at the start of constructor()
, so assignments in constructor()
will take precedence over them. This also means calling super()
from a constructor()
in a derived class may result in more fields being assigned than can be seen in the body of the constructor
.
Finalizers
private finalizer() { statements }
A finalizer is a special method that is called at the end of an object's lifetime, right before its memory is reclaimed. The exact time this happens is indeterminate and depends on when the garbage collector discovers that the object is no longer referenced.
Class Properties
class-property: abstract non incremental rigid inherit constructors uncopyable unstreamable modify notice box type replacing load align (expr) cast id = type
- An abstract class is a class that cannot be instantiated and only serves as a base class that other classes can derive from.
- The rigid property is intended for use with classes defined in other languages, such as C++. Rigid classes do not inherit from
Object
and cannot contain non-final methods. They cannot be changed incrementally. - non incremental classes cannot be changed at run time. The program must be restarted after changes have been made to a non-incremental class. Like rigid classes, the primary use for non-incremental classes is with classes imported from other languages, but unlike them, non incremental classes may contain virtual methods, and always inherit
Object
. - modify notice is explained in a section below
- inherit constructors means the constructors of the superclass should be inherited. See the constructor section.
- The box type property makes the class a box class for type.
Super
Inside every method that's an overrider, the implementation that was overridden is available via super()
, which can be called just like any other method. Likewise, calling super()
inside constructor()
will call the matching constructor in the parent class.
Statically Bound Method Calls
There are times when you want to call a specific implementation of a method that is not final. Normally the implementation of a method is selected at run time, but a specific implementation can instead be bound at compile time by prefixing the call with the name of the class containing the method in question (this is similar to the :: operator in C++).
Please note that the this
parameter must be passed explicitly when a method is called this way.
private class TestClass1 { public constructor() { } extend public int foo() { return 1; } } private class TestClass2 extends TestClass1 { public constructor() { } public int foo() { return 2; } } private class TestClass3 extends TestClass2 { public constructor() { } public int foo() { return TestClass2.foo(this); } }
The call TestClass2.foo(this)
inside TestClass3
calls foo()
in TestClass2
. It has the same effect as calling super
since TestClass2
is the superclass of TestClass3
. However, with this technique you can call the implementation of foo()
found in TestClass1
too, thus going two levels up the inheritance hierarchy. You can also make static calls to methods in classes from completely different class hierarchies.
If there is a conflict between the name of a method defined in a class and the name of a method defined in Type, the former has precedence. To specify the latter, use the fully qualified package name.
Int num(24); cm.lang.Int.toS(num);
Generic Copy
CM comes with a built-in facility for copying objects. The Object
class contains the method
extend public Object copy(CopyEnv, bool shallow=false)
which by default returns a deep copy of the object; that is, the object is copied recursively. If the shallow
parameter is true, a shallow copy will be used instead. In this case, fields pointing to other objects will simply be copied to the new object, without copying the child objects themselves. The copy and the original may thus share structure.
There are also several field properties that can be used to control how individual fields are copied:
- The copy=null and copy=0 properties indicate that the field should not be copied; The corresponding field in the copy will be cleared to null or 0 instead.
- copy=reference means this field should always be copied by reference, and never recursively.
- The copy=shallow property only makes sense for fields of reference types, and instructs copy() to create a new object, the fields of which will equal the fields of the original. If the field contains an object, which in turn has two fields pointing to objects A and B, then a new object will be created for the copy, but A and B will be copied by reference into the copy.
- copy=function id means the function bound to id should be called to produce copies of this field.
Modify Notice
If the modify notice class property is used, modifyNotice()
will be called when a field value is about to change, unless the modifyNoticed
field is true or the field used the ignore modify notice property.
extend public void modifyNotice()
The modify notice class property applies to subclasses, too. Assignments in constructors do not cause modify notices to be sent.
Here the Test
class has been written to output the message "state changed" every time one of its fields is assigned. Since it only has two fields, and one of them is marked "ignore modify notice", this is equal to saying it should print a message when the value of the "state" field changes. Of course, subclasses may introduce additional fields which will also generate modifyNotice calls when changed, since the modify notice property is inherited.
/** * Test class. */ public class Test : modify notice { /** * State. */ public int state; /** * Modify noticed field. */ public bool modifyNoticed : copy=null, stream=null, ignore modify notice; /** * Constructor. */ public constructor() { } /** * Modify notice method. */ extend public void modifyNotice() { pln("state changed"); modifyNoticed = true; } } { Test t(); t.state = 1; }
Output:
state changed
Aliases
An alias is a way to refer to a class with another name. It can be used for reusing an existing class in a different context or to simply make a long class name smaller and easier to work with.
public alias Vector = Point; private alias ExternalGW = ExternalSourceScrollableGridWindow;
Object Lifetime
Objects are always allocated by a garbage collector and never on the stack. They are always passed by reference. The garbage collector decides when the memory of an object should be reclaimed. It can be reclaimed as soon as no live reference points to the object, but this is not detected immediately. Exactly when the memory is reclaimed and the object is destroyed depends on the behavior of the running program and is not predictable in general. Before the memory is reclaimed the finalizer of the object will be invoked if any.
Comments
0 comments
Please sign in to leave a comment.