back to index

Complex datastructures: Classes

A class is a structure which is composed of the scalars int, float and objects, i.e. instances of C++ resp. user defined script classes. The elements of a class are called members, fields or properties. Classes are used to extend applications by new datatypes.

Similar to basic datatypes like e.g. int or float, classes first need to be instanciated before their members and methods can actually be accessed. Class instances are called Objects.

Class functions ("static methods") and constants can be used without a class instance (object). The class is just used as a namespace in this case.

Members

A member is a variable defined in the namespace of a class. Each time a class is instanciated, memory for its members is allocated, too. Members are visible in class methods; they can also be accessed from outside script code using the "objname.member" syntax. Member declarations may also contain initializers (e.g. int i=42; or int ia2[16]; )

Example:

        class MyClass {
                int        i;
                float      f;
                String     s;
                int        ia[];    // same as IntArray ia; . initial size=0.
                int        ia2[16]; // initial size=16
                FloatArray fa;      // same as float fa[]; . initial size=0.
                float      fa2[32]; // initial size=32
                HashTable  ht;      // initial size=57
        }
        MyClass obj;

        trace "obj.f="+obj.f;


Static members

Static members work like global variables except they are placed in the respective class' namespace. Example:

class C { static int i=42; }
print C.i;

Static members are initialized exactly once during script startup (unlike non-static members which are initialized each time a class is instanciated).

Script vs. C++ classes

While script classes are declared by user defined scripts, C++ classes are either built into the script-engine (core API classes) or imported through the YAC plugin interface (by means of the use statement.

The following C++ classes are part of the core TKS API: Boolean, Buffer, Byte, ClassArray, Configuration, Double, Envelope, Event, File, Float, FloatArray, HashTable, IntArray, Integer, Long, ListNode, ObjectArray, PointerArray, Pool, Script, Short, Stack, Stream, String, StringArray, StringIterator, Time, TKS, TreeNode, UnsignedByte, UnsignedLong, UnsignedShort, Value, Variable.

It is not allowed to derive a script class from a native class.

Please see the API documentation for a comprehensive description of all core classes.

Initializers

Constants

The define keyword is used to define constants in the namespace of a class. Example:

Functions

A function that is declared within a class can be accessed without an instance of the class. Thus, the class serves as a namespace for its contained functions. Example:

class MyClass { 
   public function sayHello() { 
      print "hello, world."; 
   } 
} 
MyClass.sayHello(); // call function (static method) of class MyClass

the example above is equal to the following code:

class MyClass {
   public static method sayHello() {
      print "hello, world.";
   }
}
MyClass.sayHello(); // call static method (function) of class MyClass

Methods

Methods are used to bundle frequently used statements which operate on objects. A method is similar to a function except that it uses the keyword "method" and that it can also see the properties of a class (i.e. the "class variables").

Example:

        class MyClass {
            method myMethod() { print "myMethod called."; }
        }
        MyClass c;
        c.myMethod();


The method may be implemented directly within the class definition (like in the above example) or a forward declaration can be used so the method body can be implemented later on (recommended for larger statement blocks):

Example:

class MyClass {
        int i;
        float f;

        method myMethod(int _i, float _f); // forward declaration
    }

MyClass::myMethod { // implementation
    i=_i;
    f=_f;
    print "myMethod called. i="+i+" f="+f;
}
MyClass c;
c.myMethod(42,PI);

Methods in super classes (i.e. base classes) can be called by prepending the base class name:

Example:

        class BaseClass {
            sayHello() { stdout "hi!\n"; }
        }
        class MyClass extends BaseClass {
            method myMethod() { stdout "myMethod says" + BaseClass::sayHello(); }
        }

        MyClass c;
        c.myMethod();


Constructors and Destructors

In order to initialize class elements upon object instanciation, a class may provide suitable constructors and destructors.

A constructor is a method which carries the same name as the class; this also applies to the destructor with the difference that its name is preceeded by the ~ char.

Currently, no parameters may be passed to constructors and destructors; appriopriate init() resp. exit() methods should be used instead. A possible workaround is also to define a global function called myclass (in case your class is called MyClass) which returns a new instance of MyClass and initializes it). The drawback is ofcourse that you cannot use multiple constructors (depending on type and number of arguments). This will be subject to change in future releases.

Please see the class_construct.tks example which demonstrates how to work-around the missing constructor feature.

Constructors are called when an object is created. If the class was derived from a base class, the constructor of the base class is called first.

Please notice that the members/fields of an object are first initialized using a list of effective initializer statements that is created from the tree of anchestor (base/super) class dependencies. The constructors are called when the field initialization has completed.

When an object is deleted, its destructors are called in the reversed order, i.e. the base class destructor is called last.

Example:

        class Telephone { 
            float fVolume; // a member (property)

            Telephone() { // constructor
                trace "constructing Telephone."; 
                fVolume=1.0; 
            } 

            ~Telephone(); // forward declaration of the destructor

            ring() { stdout "Telephone::ring\n"; } // a regular method

            // ---- define a user interface to set the volume ----
           setVolume(float _f) { fVolume=_f; } 
        }

        Telephone::~Telephone { // implement the destructor
            trace "deleting Telephone."; 
        } 

        // ---- instanciate a Telephone object, implicite constructor call ----
        Telephone t; 

        t.fVolume=0.74; // set fVolume property/member to 0.74

        // ---- set a property using the designated user interface method ----
        t.setVolume(0.74); 

        t.ring(); // call a method
        t<=null; // explicitely delete the object, implicite destructor call


Inheritance and late binding

For purposes of refinement, specialization or extension, a class may be derived from one or more base classes. A derived class inherits all members and methods of its base class(es). TKS supports multiple inheritance, i.e. each class may have up to 64 base classes.

Methods of the base class(es) may be overwritten with new implementations as long as their parameter signature (i.e. number and type of parameters) is preserved.

When an overwritten method is called, the late binding mechanism first looks up the actual method to be called. The selected method depends on the type of the object, not the type of the object variable since objects that have one or more base classes may be bound (downcasted) to any base class pointer variable. This process is also known as virtual call in C++.

Example:

        class BaseClass { 
                exec() { trace "BaseClass::exec"; } 
        }

        class ExtClass extends BaseClass { 
                exec() { trace "ExtClass::exec"; } 
        }

        BaseClass bc <= new ExtClass; // <i>downcast to base class</i>

        // ---- late binding mechanism selects ExtClass::exec method ----
        bc.exec();


Polymorphy

Example:

        class C1 { 
            int i; 
        }

        class C2 { 
            float f; 
        }

        class C3 : C1, C2 { 
            C3() { 
                i=42; f=10; 
                trace "C3::C3()"; 
            } 
        }

        C3 c;


Example:

        class CClass {   // "interface" definition..
                outputHTML() { return "*ill*"; }
        }

        class CLink : CClass {
                String target;
                String label;
                getLabel() { return label; }
                outputHTML() {
                        return "<a href=\""+target+"\">"+getLabel()+"</a>";
                }
        }

        class CImage : CClass {
                String img_source;
                String img_alt;
                outputHTML() { 
                        return "<img alt=\""+img_alt+
                       "\" src=\""+img_source+"\">"; 
                }
        }

        class CImageLink : CImage, CLink {
                getLabel() { return CImage::outputHTML(); }
                outputHTML() { return CLink::outputHTML(); }
        }

        CImageLink il;
        il.target="http://tkscript.de";
        il.img_source="images/test.png";
        il.img_alt="test";
        trace il.outputHTML();


Runtime class type checks

The type of an Object can be tested at runtime by either using the instanceof expression or the yacInstanceOf() and yacMetaClassInstanceOf() Object class methods.
Example:

Boolean b=File instanceof Stream; print b;
File f; Boolean b=f.yacInstanceOf(Stream); print b;
Also see class_instanceof.tks

Information hiding

The process of limiting the visibility of class members and methods is called information hiding. The idea is to split members and methods into groups (aspects) which have individual access restrictions. The public aspect defines the set of methods and members that is meant to be refered to in applications which simply "use" a class; the private, protected and module protection modes are used to specify (and limit) the extensibility of a class.

The visibility of members (fields), methods and functions can explicitely be specified by the following qualifier keywords:

If no visibility modifier keyword is used during a class member declaration, public is assumed.

Example: See class_protection.tks

Serialization

Classes (like almost any TKS API class) can be (de)serialized from/to a filestream. In order to avoid infinite recursions only "read-write" object references will be serialized, except for Strings.

Class members which ought to be serialized have to be "tagged" with the tag keyword.

Please notice that in the following example the tcobject() expressions is used to create read/write copies of the int/float array and hashtable initializers.

Example:


class MyClass {
      tag int i;
      tag float f;
      tag String s;
      tag IntArray ia;
      tag FloatArray fa;
      tag HashTable ht;
      
      init() {
         i=42;
         f=2PI;
         s="hello, world.";
         ia<=tcobject([1,2,3,4]);
         fa<=tcobject([1.1,2.2,3.3,4.4]);
         ht<=tcobject(#[a=1, b=2, c=3]);
      }
}

MyClass mc; mc.init();
Buffer b; b.size=1024; // allocate buffer stream
b << mc;  // serialize class into stream

mc<=new MyClass; // delete old object and create new MyClass object without init()
b.offset=0;  // rewind stream
mc << b;  // deserialize class from stream

print "mc.i="+mc.i;
print "mc.f="+mc.f;
print "mc.s="+mc.s;
print "mc.ia[2]="+mc.ia[2];
print "mc.fa[2]="+mc.fa[2];
print "mc.ht[\"c\"]="+mc.ht["c"];

Also see the class.serialize.tks example.


back to index

TkScript and the TkScript documentation are (c) Copyright 2001-2005 by Bastian Spiegel.