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.