A Quick Introduction to Virgil - Classes

This is part of a basic introduction to programming in Virgil and gives an overview of the language, its syntax, and its structures. This page focuses on declaring and using the object-oriented features of Virgil with classes.

Overview

Virgil provides classes to support building data structures and programs in an object-oriented way. A class serves as both a type for objects as well as a collection of related functionality. Classes can be instantiated an arbitrary number of types through the use of the new operator. Each instance represents a separate heap entity with its own storage and identity called an object. All objects are passed by reference.

Virgil supports single inheritance between classes only. Each declared subclass becomes a subtype of the parent class and inherits the non-private methods and fields of the parent class.

Fields

class A {
    field size: int;
    field name: char[] = "MyName";
    method test() { size = 0; }
}
component B {
    method main(o: A) {
        o.size = 1;
    }
}

Fields are locations in an object that can store values. Because Virgil is a statically typed language, each field declaration must be given an explicit type, using a similar syntax to declaring local variables. A field declaration within a class is identical to a field declaration with a component; it begins with the keyword field, followed by the name of the field (which must be a valid identifier), then a colon :, then the declared type, and then an optional expression to initialize the field. The difference between a field declared in a component and a field declared in a class is that each object of the class has an associated field with the declared name and type, whereas for a component, the field exists only once and is global. Thus, accessing the field of an object requires a reference to the object. Accessing the field is done by using the member access operator . on an object expression, followed by the name of the field.

In the example on the right, the class A declares a size field which is accessed from within the test() method in A and the main() method in the component B. It also declares a name field, which is initialized to the value "MyName".

Methods

class A {
    method run() { ... }
    method test() { run(); }
}
component B {
    method main(o: A) { o.run(); }
}

Methods can be declared inside a class in the same way that methods are declared inside of a component, using identical syntax. The declaration begins with the method keyword, followed by the name of the method (which must be a valid identifier), followed by the argument types in parentheses ( ... ), followed by an optional return type (as explained in the methods page). Like fields, methods in a class are always associated with a particular object. Thus, we can invoke a method on an object through a reference using the member access operator ., similar to how fields are accessed.

In the example on the right, the class A declares a run() method which is called from within the test() method in A. In the component B, the main() method accepts an object of type A and invokes the run() method.

The "this" Parameter

class A {
    field f: int;
    method increment() {
        // equivalent statements:
        this.f = 0;
	f = 0;
    }
}

Every method declared inside of a class accepts an implicit, hidden parameter called this. The this parameter will always refer to the object in which the current method is executing, thus allowing the code inside the method to refer to the enclosing object instance by simply referring to this. The method may, for example, pass the this reference to other methods of other objects, access fields or methods through the reference, etc. In fact, any time a method in a class refers to a field or method of the class directly, by its name, it is implicitly using the this parameter.

Private Members

class A {
    private method internal() { ... }
}
component B {
    method test(o: A) {
        o.internal(); // ERROR
    }
}

Methods and fields within a class can be declared to be private, which makes them accessible only within the declaring class. This provides a way to hide internal implementation details and protect the implementation of a class from outside disturbance. When a member is declared private, any attempt to access the member from outside of the declaring class will result in a compile-time error. It is typically a good practice to declare most fields private to prevent their modification from outside.

Inheritance

class A {
    field name: char[];
    method test() { ... }
}
class B extends A {
    method mine(o: A) {
        local n = name; // inherited
	test();         // inherited
    }
}

Virgil supports single inheritance between classes. When declaring a class, an optional parent class can be specified with the extends keyword. When a class extends another class, it inherits the non-private members (fields and methods) of the parent class. It can then access these members through their names. The new class can add new fields and methods, thereby extending the functionality of the class. Instances of the new class will not only have storage for the fields that were declared in the new class, but also storage for the fields inherited from its parent class.

The new class also becomes a subtype of the parent class. This means that we can use references that have a declared type of the new class (the subtype) anywhere a reference of the parent class is expected. This is known as subsumption, and its intuitively safe because the new class has all of the required functionality of the parent class (and more).

Method Overriding

class A {
    method test(a: int) { ... }
}
class B extends A {
    method test(a: int) { ... }
}

In addition to supporting inheritance of methods, Virgil also supports overriding of methods, where a new class can supply a different implementation for a method that it would otherwise inherit from its parent class. This allows the class to specialize its behavior and alter its behavior from that of the base class. When the method is invoked on an object reference, a virtual dispatch selects the method implementation corresponding to the object's dynamic type.

Virgil requires that the parameter types and return type of an overridden method match exactly between the parent class and the class declaring the overriding implementation.

Objects are References

// assuming class A { field f: int; }
. . .
local x: A = new A();
local y: A = x;
y.f = 100;
local z: int = x.f; // z == 100

Virgil objects are always passed by reference. This means that assigning a variable to an object expression, or passing an object across a method call does not copy the contents of the object, but instead just passes a reference to the object. Thus, updates to the object's fields after a reference has been passed are visible to the new holder of the reference.

Exceptions

// assuming class A { 
//     field f: int; method m(): int; }
local x: A = null;
local y: int = x.f;   // NullCheckException
local z: int = x.m(); // NullCheckException

Access of an object's fields or methods can generate the NullCheckException. A reference that is null indicates that the reference does not refer to any valid object. A field access or method access will generate a NullCheckException if the object reference is null.

Go back to the tutorial.