Chapter 2. Objects, Interfaces, Classes and Mixins

It bears repeating: everything in Dart is an object. This includes even the simplest data such as numbers or the Boolean values true and false.

An object consists of a (possibly empty) set of fields, providing state, and a set of methods, providing behavior. The state of an object may be mutable or immutable. An object’s set of methods is never empty because all Dart objects have some behavior. Objects get their behavior from their class. Every object has a class; we say that the object is an instance of the class. Because every object has a class that determines its behavior, Dart is a class-based language.

Consider the Point class we encountered in the previous chapter.

class Point {
 var x, y;
 Point(this.x, this.y);
 scale(factor) => new Point(x * factor, y * factor);
 operator +(p) => new Point(x + p.x, y + p.y);
 static distance(p1, p2) {
  var dx = p1.x - p2.x;
  var dy = p1.y - p2.y;
  return sqrt(dx * dx + dy * dy);
 }
}

Instances of Point each have two fields, x and y that constitute their state. Points also have several methods that provide useful behavior. These methods include scale and +, but there are in fact others. These additional methods are not defined by the class Point itself, but are inherited from its superclass. Every class has a superclass, except for the class Object which is built in to every Dart implementation. Classes may list their superclass explicitly, but do not have to. If a class does not list a superclass, the superclass is Object. This is the case with Point. We could have explicitly specified Object as the superclass of Point as follows; the two definitions are entirely equivalent.

class Point extends Object { ... rest of definition unchanged }

The methods that define an object’s behavior are known as its instance methods. Note that the method distance() is not part of the behavior of instances of Point. It is a static method, not an instance method.

2.1 Accessors

Accessors are special methods that provide convenient access to values. To understand accessors, let us revisit Point once more, and consider how to change points so that they use a polar representation. We can easily replace the fields x and y with new fields rho and theta. However, we may have any number of clients that still require access to the Cartesian coordinates. So we might choose to compute these based upon the stored polar coordinates. The resulting class follows:

class Point {
 var rho, theta;
 Point(this.rho, this.theta);
 x() => rho * cos(theta);
 y() => rho * sin(theta);
 scale(factor) => new Point(rho * factor, theta);
 operator +(p) => new Point(x() + p.x(), y() + p.y());
 static distance(p1, p2) {
  var dx = p1.x() - p2.x();
  var dy = p1.y() - p2.y();
  return sqrt(dx * dx + dy * dy);
 }
}

The code is actually broken, because inside + we are calling the constructor with Cartesian coordinates whereas it expects polar coordinates. Set that issue aside; we’ll deal with it later.1

1. Another issue we will ignore is the question of numerical precision; all these conversions may not yield perfect results.

The result is unsatisfactory for other reasons. We’ve replaced the fields x and y with methods to compute the corresponding values. All our clients are going to have to modify the references to x and y so that they are method invocations. For instance, if a client had a fragment such as

print(myPoint.x);

it needs to be changed to

print(myPoint.x());

The only difference is the empty argument list following x; it’s a small change, but it is a change nevertheless. Modern tools can help, by automatically refactoring, but if one modifies a widely used API, one does not know who all the clients are, and one cannot afford to burden them all with making such changes, even using convenient tools.

Dart provides a better solution in the form of getter methods, usually referred to as getters. A getter is a special method that takes no parameters, and can be called without an explicit argument list. A getter method is introduced by prefixing the method name with the word get. One must not give a parameter list—not even an empty one.

class Point {
 var rho, theta;
 Point(this.rho, this.theta);
 get x => rho * cos(theta);
 get y => rho * sin(theta);
 scale(factor) => new Point(rho * factor, theta);
 operator +(p) => new Point(x + p.x, y + p.y);
 static distance(p1, p2) {
  var dx = p1.x - p2.x;
  var dy = p1.y - p2.y;
  return sqrt(dx * dx + dy * dy);
 }
}

Now our hypothetical clients need not be changed. The syntax for a getter invocation is indistinguishable from a variable access.

The alert reader should now ask how does the Dart compiler know the difference between the two? The answer is that it doesn’t. All instance variable accesses in Dart are actually getter calls. An instance variable always has a getter associated with it, courtesy of the Dart compiler.

So far we’ve only solved part of the problem. What about clients that assign to the fields, such as

myPoint.y = myPoint.y * 2.

The new version has no field that can be assigned. It isn’t clear what change the client can make to keep their code running. To address this issue we use setter methods (setters for short). A setter is prefixed with the word set and takes a single parameter. A setter is invoked using the same syntax as a conventional variable assignment. If an instance variable is mutable, a setter is automatically defined for it, and all instance variable assignments are in fact setter calls.

class Point {
 var rho, theta;
 Point(this.rho, this.theta);
 get x => rho * cos(theta);
 set x(newX) {
   rho = sqrt(newX * newX + y * y);
   theta = acos(newX/rho);
 }
 set y(newY) {
   rho = sqrt(x * x + newY * newY);
   theta = asin(newY/rho);
 }
 get y => rho * sin(theta);
 scale(factor) => new Point(rho * factor, theta);
 operator +(p) => new Point(x + p.x, y + p.y);
 static distance(p1, p2) {
   var dx = p1.x() - p2.x();
   var dy = p1.y() - p2.y();
   return sqrt(dx * dx + dy * dy);
 }
}

At this point, we’ve solved two parts of the problem, but there remains a third part. Our clients created points by invoking the constructor of class Point, as in

new Point(3,4);

Our new class expects the arguments to the constructor to represent the length and orientation of the vector, not its Cartesian coordinates. And as we noted earlier, we rely on this API inside the class as well, and so there is no way that this will function correctly. As usual, the solution is to preserve the existing API while still changing the representation as desired. We therefore choose to use the polar representation, but to maintain the interface, we retain the existing constructor which takes Cartesian arguments.

class Point {
 var rho, theta;
 Point(a, b){
   rho = sqrt(a * a + b * b);
   theta = atan(a/b);
 }
 ... rest unchanged ...
}

We have now achieved our goal of changing the representation of points without any impact on their clients. We did this without any pre-planning. We did not have to decide in advance to expose the coordinates of points via accessor methods or special property declarations; Dart did that for us, without any syntactic inconvenience. We could have fixed the constructor in any mainstream language. However, without accessors, a completely smooth transition would not have been possible.

2.2 Instance Variables

When a class declares an instance variable, it is ensuring that each of its instances will have its own unique copy of the variable. The instance variables of an object occupy memory. This memory is allocated when an object is created. It’s important that this memory be set to some reasonable value before the program can access it. In low-level languages such as C, this is not necessarily the case, and the contents of freshly allocated storage may be undefined—typically whatever value was stored in them before. This causes problems with respect to reliability and security.

Dart initializes every newly allocated variable (not just instance variables, but also local variables, class variables and top-level variables) to null. In Dart, null is an object, much like any other. One must not confuse null with other objects such as 0 or false. The object null is simply the unique instance of the class Null defined in the Dart core library. This is different from the situation in languages such as C, C++, C#, Java or Javascript, but is an inevitable consequence of the axiom that everything is an object.

Declaring an instance or static variable automatically introduces a getter. If the variable is mutable, a setter is also automatically defined. In fact, fields are never accessed directly in Dart. All references to fields are invocations of accessor methods. Only the accessors of an object may directly access its state. All code that accesses an object’s state has to go through these accessor methods. This means that the representation of a class can be always be changed without any change to client source code. Even recompilation is not required—ever! This property is known as representation independence.

We saw the advantage of representation independence in the previous section, where we showed how to change the representation of points without modifying any code that used points.

2.3 Class Variables

In addition to instance variables, classes may define class variables. A class has only one copy of a class variable, regardless of how many instances it has. The variable exists even if there are no instances at all.

A class variable declaration is prefaced with the word static. We could add a class variable to a class to track how many instances have been created.

class Box {
 static var numberOfInstances = 0;
 Box(){numberOfInstances = numberOfInstances + 1;}
}

Here, every time the constructor of class Box() runs, it increments the count of all boxes ever made.

Like instance variables, class variables are never referenced directly. All accesses to them are mediated via accessors.

A class variable can be referred to by its name anywhere within its declaring class. Outside the class, it can be accessed only by prefixing it with the name of the class that declared it:

Box.numberOfInstances == 0 ? print('No boxes yet') : print('We have boxes!'),

Class variables are often referred to as static variables, but the term “static variable” is also used to encompass both class variables and top-level variables. To avoid any confusion, we’ll stick with the term “class variable.” We will often use the term “fields” when we speak of instance and class variables collectively.

Class variables are initialized lazily; the initializer of a class variable is executed the first time its getter is invoked—that is, the first time one attempts to read the variable. If a class variable has no initializer, it is initialized to null, just like any other variable.

Lazy initialization of class variables helps avoid a classic problem: sluggish application startup due to an excess of up-front initialization. However, laziness can cause surprising behavior. Suppose a class variable is assigned before it has been read, as in the example below.

class Cat {}
class DeadCat extends Cat {}
class LiveCat extends Cat {
 LiveCat() {print("I'm alive!");}
}
var schrodingers = new LiveCat();

main() {
 schrodingers = new DeadCat();
}

Here, the initializer of schrodingers will never be executed, and the call to print() will never be executed. While this may seem pretty obvious above, in more complex situations that may not be the case. For example, during debugging one might check for the variable’s value, which would provoke initialization. Programmers should always pay close attention to the implications of lazy initialization.

2.4 Finals

Variables in Dart may be prefixed with the word final, indicating that they may not be changed after they are initialized. A final field has a getter but no setter. Final class variables must be initialized at the point they are declared.

Final instance variables must be initialized before any instance methods are run. There are several ways of accomplishing this. The first is to initialize the variable at its point of declaration, for example,

final origin = new Point(0,0);

This isn’t always convenient. It may be that the variable will be set differently in different constructors. For example it may depend on the arguments to the constructor. If one wants the value of a final instance variable to be set to the value of a constructor argument, one can use the common constructor shorthand. As an example, consider this variant of Point where points are immutable:

class Point {
 final x, y;
 Point(this.x, this.y);
 // the rest of Point ...
}

However, sometimes this is not enough. The value may depend on a constructor argument, but be different—that is, it may involve some computation based on the constructor argument. We’ll look into how to achieve this in just a little while, in section 2.9

Attempting to assign a final instance variable will usually lead to a NoSuchMethodError, since an assignment is just sugar for invoking a setter, and the setter won’t be defined. It is possible that a setter was declared separately however, in which case it will get invoked. This won’t have any effect on the value of the instance variable however—there is simply no way to change a final variable after it is initialized.

Most instance variables are set when the instance is allocated and never change afterwards. You might find this surprising, but this has been verified by systematic studies[3]. It is therefore best to declare most instance variables final.

There is a strong argument that final should be the default, but that runs against established habit, and so Dart makes the traditional choice here.

2.5 Identity and Equality

All objects support the equality operator ==. This operator is defined in class Object, so all classes inherit it, and it is thus included in the behavior of all instances. Consider

var aPoint = new Point(3, 4);
var anotherPoint = new Point(3,4);
aPoint == anotherPoint; // evaluates to false

The result of the last line is somewhat disturbing. After all, both aPoint and anotherPoint have the same x and y values, so why are they not equal?

The method == in Object tests whether the argument is identical to the receiver. As noted earlier, each object has a unique identity. An object is identical to itself, and only to itself. Two objects can be instances of the same class and have the exact same state, yet still not be identical, as the above example shows.

The above explanation is rather mechanistic. We say that the two objects are not equal because equality is defined as identity, but this begs the question of why equality is defined this way.

Unfortunately, there is no obviously correct way to decide if two arbitrary objects are equal. One could compare all the fields of the object for equality, but this is still too conservative. An object may have fields that are used to store auxiliary data that is not essential to the semantics of equality. For example, suppose we have a timestamp that tells us when some cached value was last computed. The cache is considered stale after some time. Two objects might differ on the timestamp (or on the cached value) and yet still be equal.

And so, it is the responsibility of the programmer who defines a class to determine what notion of equality makes sense for instances of the class. The way to do this is to override ==.2 In the case of Point we can define equality as

2. It would be nice to use = for equality, but the C tradition and mathematics contradict each other.

operator == (p) => x == p.x && y == p.y;

Notice that this code remains valid (modulo issues of numeric precision) even if we switch to polar coordinates (though it may not be as efficient). In fact, it holds even if p is a Cartesian point and the receiver is polar, or any other combination.

There are situations where one may wish to check if two expressions denote identical objects, but they are relatively rare. Typically, one tests objects for equality.

One can compare the identities of two objects using the built in function identical(), defined in dart:core.

We find that identical(origin, origin) evaluates to true, as do identical(aPoint, aPoint) and identical(anotherPoint, anotherPoint). On the other hand identical(aPoint, another-Point) evaluates to false.

We now know enough to define the equality method of class Object ourselves:

bool operator ==(other) => identical(this, other);

All Dart objects also support the getter method hashCode. Equality and hashCode are interrelated. If two objects are equal, their hash codes should be equal. Implementors must take care to preserve this property. In practice, this means that if you choose to override either one of these methods, you should override the other as well.

Implementing user-defined equality requires some care. We expect our equality predicates to be reflexive (a == a), transitive ((a == b) && (b == c) implies a == c) and commutative (a == b implies b == a). Reflexivity is something you can ensure in your own implementation of ==. The other properties are difficult to preserve in an extensible system. One can always introduce

class BadApple {
 operator == (x) => true;
}

and undermine the mathematical properties of equality throughout the system.

2.6 Class and Superclass

Each class declares a set of instance members that include instance variables and several kinds of instance methods. In addition, every class (except Object) inherits instance members from its superclass. Since every class except Object has exactly one superclass, and Object has no superclass, the Dart class hierarchy forms a tree with Object as its root. This arrangement is known as single inheritance, and is illustrated below.

Image

In the diagram, we see the class hierarchy for numbers, Booleans and Null. Of course, this is just a small fragment of the entire Dart class hierarchy.

If a subclass declares an instance method with the same name as a method of its superclass, the subclass method is said to override the superclass method.

Overrides are not always legal. You can’t override a getter with an ordinary method or vice versa for example. These situations will lead to a compilation error.

class S {
  var v;
  final f = 0;
  get g => 42;
  set s(x) => v = 2;
  m(a, b) => 91;
}

class C extends S {
  v() => 1; // ILLEGAL: method v() overrides implicit getter method v
  f() => 2; // ILLEGAL: method f() overrides implicit getter method f
  g() => 100; // ILLEGAL: method g() overrides getter method g
}

Attempting to override a setter with a method or getter, or to override a method or getter with a setter is technically impossible because the names of setters do not overlap with those of methods and getters. However, Dart will warn you if you try:

class D extends S {
  s(y) => 200; // WARNING: D has method s and setter s=
}

There are cases where a nonsensical override will result in a warning rather than a compilation error. When an overriding method requires more parameters than the method it overrides a warning is issued but the program will still compile.

class E extends S {
  m(x, y, z) => 101; // WARNING: overriding method has incompatible arity
}

Why does Dart only warn about this situation rather than reject the code out of hand? Because Dart tries to avoid dictating a workflow to the programmer as much as possible. Inconsistencies of various kinds can arise during development and should ultimately be corrected. However, forcing the programmer to deal with these situations immediately, before any other progress can be made is often counter-productive. Therefore Dart makes sure that the developer is alerted to such problems via warnings, but does not abort compilation unless it is absolutely essential.

2.7 Abstract Methods and Classes

Often it is useful to simply declare a method without providing its implementation. Such a method is called an abstract method. Any kind of instance method can be abstract, regardless of whether it is a getter, setter, operator or ordinary method.

Declaring an abstract method tells anyone reading the code (human or computer) that such a method is expected to be available when code is actually executed. This improves programmer comprehension as well as error handling and tooling.

A class that has an abstract method is itself an abstract class. Abstract classes are declared by prefacing their declaration with the word abstract.

Below we see an abstract class Pair that contains no implementation information whatsoever. It acts purely as an interface, as we’ll describe below.

abstract class Pair {
 get first;
 get second;
}

Pair has two abstract getter methods, first and second. Pair is explicitly declared as an abstract class. If we omitted the abstract modifier, the Dart analyzer would issue a warning that the class has abstract methods. It is of course perfectly correct to have abstract methods in an abstract class, but it is clearly a problem to retain such methods in a concrete class. The abstract modifier allows us to declare our intent and the warnings the analyzer issues will vary accordingly.

An abstract class is not intended to be instantiated; after all, it is missing parts of its implementation. Instantiating it will result in a runtime error; specifically, an AbstractClassInstantiationError will be raised. The Dart analyzer would issue a warning as well.

new Pair();
// Static warning: attempting to instantiate an abstract class
// Throws an AbstractClassInstantiationError

As far as the runtime is concerned, an abstract method simply doesn’t exist. After all, there is no implementation to run. Invoking such a method is treated exactly the same as if no method declaration were present.

A class that only contains abstract methods is useful in defining interfaces, as described in the next section.

2.8 Interfaces

Every class implicitly defines an interface that describes what methods are available on its instances. Dart does not have formal interface declarations like those found in many other languages. These are unnecessary, as one can always define an abstract class to describe a desired interface.

abstract class CartesianPoint {
 get x;
 get y;
}

abstract class PolarPoint {
 get rho;
 get theta;
}

Despite the absence of interface declarations, classes may assert that their instances implement specific interfaces

class Point implements CartesianPoint, PolarPoint {
// Usual Point implementation goes here
}

Point is not a subclass of CartesianPoint; it does not inherit any members from CartesianPoint (or PolarPoint for that matter). The purpose of the implements clause is to establish an intended relationship between interfaces, not to share implementations.

It is possible to test whether an object conforms to an interface at runtime:

5 is int; // true
'x' is String; // true
[] is Point; // false
aPoint.toString() is String; // true
new Point(0,0) is String; // false
aPoint is CartesianPoint; // true

Note that an is test does not check whether the object is actually an instance of a given class or one of its subclasses. Rather, the is test checks whether an object’s class explicitly implements an interface (directly or indirectly). In other words, we are not concerned about what the implementation of an object is, but only with the interface it is intended to support. This is a crucial difference compared to most other languages that support such constructs. If a class wishes to emulate the interface of another class, it is not constrained to share implementation. Such emulation should be indistinguishable (modulo reflection(7)) to clients. We’ll have more to say about is in Chapter 5 when we examine types in detail.

Interfaces inherit from each other very much like classes do. The implicit interface of a class inherits from the implicit interface of its superclass as well as from any interfaces the class implements. As with classes, instance methods of an interface override those from superinterfaces. Again, some overrides may be illegal because the number of arguments should be compatible between the overriding method and the overridden one, or because one is trying to override a getter or setter with an ordinary method or vice versa.

Additionally, because an interface has multiple direct superinterfaces, conflicts can arise between the different superinterfaces. Assume a method occurs in more than one superinterface, and the different superinterfaces have versions of the same method that disagree on the method arity. In this situation, no version of the conflicting method is inherited — likewise if one superinterface defines a getter and another an ordinary method. As we’ll see in Chapter 5, these situations will give rise to various warnings as well.

2.9 Life of an Object

Computation in Dart revolves around objects. Because Dart is a purely object-oriented language, even the most trivial Dart program involves the creation of objects. For example, the “Hello World” program shown in the very first paragraph of the book involves the creation of a string object.

Some objects, such as the string ‘Hello World’, the Boolean value true or the number 91 are literals. These come in to being simply by virtue of being mentioned in a running program. Most objects, however, are created explicitly using an instance creation expression such as new Point(0,1). Such an expression calls a constructor—in this case, Point(). Every class has at least one constructor. The name of a constructor always begins with the name of the class whose instance we want to construct. Constructors may be declared explicitly by the programmer, or they may be generated implicitly. Implicit constructors are generated when no explicit constructor is declared; they take no arguments and have no body. For example

class Box {
 var contents;
 }

is equivalent to

class Box {
 var contents;
 Box();
 }

which itself is equivalent to

class Box {
 var contents;
 Box(){}
}

We have already seen a few variations of constructors in our examples using Point. We’ll assume the simplest one:

Point(a, b){x = a; y = b;};

The first step in evaluating an instance creation expression is evaluating the arguments to the constructor. In new Point(0,1), the arguments are the literal integers 0 and 1. As with any function invocation, the parameters are set to the corresponding arguments. So we have a set to 0 and b set to 1. Now we can allocate a fresh instance of class Point, which will have storage for two fields, x and y. Initially, these fields will be set to null by the system. This ensures that user code will never encounter uninitialized memory.

We can now execute the body of the constructor, which involves two assignments, to x and y respectively. As you know by now, these assignments are really setter method invocations. It is the implicitly defined setters that actually set the values of the fields x and y to 0 and 1 as expected. At this point, the constructor returns a reference to the newly allocated instance, which is the end result of the new expression.

Now, let us consider a class representing three-dimensional points. This class has coordinates x, y and z and can naturally be declared as a subclass of Point.

class Point3D extends Point {
 var z;
 Point3D(a, b, c): super(a,b) {z = c;}
}

One can create an instance of Point3D by writing new Point3D(1,2,3). The evaluation process is similar but a little more involved. We still begin by evaluating the arguments and allocating the object—this time with three fields—the two inherited fields x, y from Point, and the z field declared in Point3D. All three fields will again be set to null. However, before executing the body of the Point3D constructor, we will have to execute the body of the Point constructor, otherwise the fields x and y will not be properly initialized.

In general, the Dart compiler cannot determine what arguments should be passed to the super constructor. It may seem obvious in this case, but it is not always so simple. In all but the simplest cases (where there are no arguments) we require an explicit super-constructor call to guide us. In our example, that is super(a,b). In a superconstructor call, super is a stand-in for the name of the superclass, so that Point3D(x1, y1, z1) calls Point(x1, y1) before executing its own body.

The description above is a bit simplistic, as we will see shortly. Object creation is one of the more involved aspects of object-oriented programming languages, especially in the face of inheritance. To refine our understanding further, we shall look once more at the question of changing the representation of points. This time, assume that points are immutable, but are represented using polar coordinates.

Since the points are immutable, rho and theta are final. We cannot initialize the instance variables at their declaration, since we need access to the constructor arguments. We cannot use initializing formals like this.rho, since the values of rho and theta are each a function of both x and y. And we cannot assign the fields inside the constructor body, because no setter is defined for them. Dart solves this problem with initializer lists, which are designed to initialize instance variables before ordinary code is run.

class Point {
 final rho, theta;
 Point(a, b) : rho = sqrt(a * a + b * b), theta = atan(a/b);
 get x => rho * cos(theta);
 get y => rho * sin(theta);
 scale(factor) => new Point(rho * factor, theta);
 operator +(p) => new Point(x + p.x, y + p.y);
 static distance(p1, p2) {
  var dx = p1.x - p2.x;
  var dy = p1.y - p2.y;
  return sqrt(dx * dx + dy * dy);
 }
}

The initializer list here starts with the colon following Point(a, b) and continues until the semicolon at the end of the line: : rho = sqrt(a * a + b * b), theta = atan(a/b). It consists of two initializers, one for rho and one for theta. Initializers are separated by commas and are executed from left to right. Besides initializers for instance variables, an initializer list may contain a single superconstructor call. We saw this in Point3D(), where we had the initializer list : super(a,b). Note that if no superconstructor call appears in the initializer list explicitly, an implicit super constructor call of the form super() is appended to the list.

We have seen several ways to initialize instance variables. Let’s review them again. One can initialize an instance variable at its point of declaration

class Point {
 var x = 0, y = 0;
}

or via initializing formals in a constructor

class Point {
 var x, y;
 Point(this.x, this.y);
}

or in an initializer list

class Point {
 var x, y;
 Point(a, b) : x = a, y = b;
}

or in a constructor body

class Point {
 var x, y;
 Point(a, b) { x = a; y = b;}
}

For an ordinary instance variable, one can choose any one of these options, or several of them, or none at all. The last option is not available for final instance variables because it uses setter methods, which finals do not have. Final instance variables must be initialized only once, and so one has to choose one of the first three options above.

When an object is instantiated, the various initialization constructs are executed in the order listed above.

We can now describe the process of instance creation in its full glory. Assume that Point3D is defined as above, and that Point has the following constructor:

Point(a, b) : x = a, y = b;

The instance creation process is illustrated below, with execution following the direction of the arrow:

Image

Creating a new instance of Point3D via new Point3D(7, 8, 9) begins by computing the actual arguments, which in this case are 7, 8 and 9. Then the constructor Point3D() is called. The next step is allocating a fresh instance of Point3D. All instance variables are set to null. We then proceed to execute the initializer list of Point3D. This causes the super initializer to be executed, which causes the initializer list of Point to be executed. This will set the instance variables x and y, and then execute the implicit super initializer added at the end of Point’s initializer list. This will call the initializer list of Object which does nothing (it doesn’t even have a super initializer at the end). All these steps are shown on the left side of the figure above.

Having completed traversing all the initializer lists in the superclass chain, the next step is to execute the constructor body. In the figure, this corresponds to the u-turn of the arrow.

A constructor body always begins by implicitly running the superconstructor body. The arguments passed to the superconstructor are the same ones used in the superconstructor call given in the initializer list. They are not recomputed. So in our case, we’ll start running the body of Point3D(), which starts running the body of Point(), which starts running the body of Object(), which does nothing. Since no body was specified for Point(), we return to the body of Point3D() where we initialize z, after which we return the newly allocated object. The process corresponds to the right side of our diagram.

2.9.1 Redirecting Constructors

Sometimes it is useful to define a constructor in terms of another. Consider the case of converting from Cartesian points to polar points again. Our previous effort only allowed points to be created from Cartesian coordinates. Given the fact that our actual representation was polar, this is a bit odd. Of course, we could just change the constructor to take polar coordinates, but we know that is problematic because we have existing users who depend on the Cartesian interface. We really should retain the old constructor, but also add a new one that accepts polar coordinates. We’ll define a new named constructor Point.polar() for this purpose, and revise the pre-existing constructor so that it converts incoming Cartesian coordinates into polar ones, and forwards the call to Point.polar().

class Point {
 var rho, theta;
 Point.polar(this.rho, this.theta);
 Point(a, b) : this.polar( sqrt(a * a + b * b), atan(a/b));
 get x => rho * cos(theta);
 set x(newX) {
   rho = sqrt(newX * newX + y * y);
   theta = acos(newX/rho);
 }
 set y(newY) {
   rho = sqrt(x * x + newY * newY);
   theta = asin(newY/rho);
 }
 get y => rho * sin(theta);
 scale(factor) => new Point.polar(rho * factor, theta);
 operator +(p) => new Point(x + p.x, y + p.y);
 static distance(p1, p2) {
   var dx = p1.x - p2.x;
   var dy = p1.y - p2.y;
   return sqrt(dx * dx + dy * dy);
   }
}

Point() is now a redirecting constructor. The purpose of a redirecting constructor is, as you might guess, to redirect execution to another constructor, in this case Point.polar(). In a redirecting constructor, the parameter list is followed by a colon and a call of the form this.id(. . .) which specifies which constructor to redirect to.

At this point, we have successfully converted the representation of points while preserving the original API of the class. We’ve used a named constructor Point.polar() to manufacture points using the new representation, allowing us to keep the old constructor in place. We converted the old constructor into a redirecting constructor so that it produces our revised points. We also used getters and setters to preserve the original API of point instances.

2.9.2 Factories

Now suppose we want to avoid excess allocation of points. Rather than generating a fresh point on every request, we’d like to maintain a cache of points. Whenever someone tries to allocate a point, we’d like to check our cache and see if an equivalent point is already there, and return that one.

Traditionally, constructors make that difficult. In most languages, a constructor always allocates a fresh instance as we’ve described. If you want to use a cache, you need to think of this in advance, and ensure that your points are allocated via a method call, often referred to as a factory method.

Tragically, programmers are denied the gift of perfect foresight, and on occasion may fail to predict the need for a cache in advance. In Dart however, any constructor may be replaced with a factory in a manner that is completely transparent to the client. We do this by means of factory constructors.

Factory constructors are prefaced by the word factory. They look like ordinary constructors, except that they may not have initializer lists or initializing formal parameters. Instead, they must have a body that returns an object. The factory can return the object from a cache, or it can allocate a fresh instance as it chooses. It can even allocate instances of a different class (or look them up in a cache or any other data structure). As long as the resulting object complies with the interface of the class, all will be well.

In this way, Dart addresses some of the classic weaknesses of traditional constructors.

2.10 noSuchMethod()

Computation in Dart revolves around calling methods on objects. If one calls a method that doesn’t exist, the default behavior is to raise a NoSuchMethodError. However, this is not always the case.

When one calls a method that doesn’t exist on an instance, the Dart runtime calls the method noSuchMethod() on that same object. Because the implementation of noSuch-Method() in Object throws NoSuchMethodError, we usually see the familiar behavior.

The beauty of this scheme is that noSuchMethod() can be overridden. For example, if you are implementing a proxy for another object, you can define the proxy’s noSuchMethod() to forward all invocations to the target of the proxy.

class Proxy {
 final forwardee;
 Proxy(this.forwardee);
 noSuchMethod(inv) { return runMethod(forwardee, inv);}
}

The argument to noSuchMethod() is an instance of Invocation, a special type defined in the core library and used to describe method invocations. An Invocation reflects the original call, describing what was the name of the method we were trying to invoke, what were the arguments, and a few other details.

In order to actually forward each call to forwardee, our implementation of noSuch-Method() makes use of an auxiliary function runMethod() that takes a receiver object and an invocation, and invokes the named method on the receiver with the arguments supplied. We’ll show how to implement runMethod() later, when we discuss reflection in Chapter 7.

A robust implementation of a proxy is a little bit more involved than the code above. One subtlety is that Proxy will not forward methods defined in Object, since these are inherited and will not cause noSuchMethod() to be called. The interface of Object is small by design and can be intercepted manually.

Even with these complications, a full implementation of Proxy is straightforward. Later in the book, once we’ve learned a few more tricks, we’ll show a full version of Proxy. The ability to code a general-purpose proxy, independent of the type of the target, is a perfect example of the flexibility an optionally typed language like Dart can provide. Such flexibility is missing in mandatory typed languages.

We shall see several interesting additional uses of noSuchMethod() throughout this book.

2.11 Constant Objects and Fields

Some objects are constants that can be computed at compile-time. Many of these are obvious: literal numbers like 3.14159, literal strings like ‘Hello World’ and so forth. We will give a detailed account of constant expressions in section 6.1.4.

Dart also supports user-defined constant objects, and these will be our focus here. Let’s see if we can make constant points

class Point {
 final x, y;
 const Point(this.x, this.y);
 ... the usual
}
const origin = const Point(0,0);

Under the right conditions we can produce a constant Point object representing the origin. The origin variable is declared to be a constant. We can only assign a constant object to it. Constant objects get created using const instead of new. Like a new expression, a const expression invokes a constructor, but that constructor must be a constant constructor, and the arguments to the constructor must themselves be constant. In fact, Dart requires that the arguments also be restricted to numbers, Booleans or strings. Fortunately, numeric literals like 0 are always constants.

We’ve declared Point’s constructor to be constant. That imposes some pretty severe restrictions on the class and on the constructor itself. We need a class whose state is immutable. Fortunately, immutable points are quite natural.

In addition, a constant constructor cannot have a body. It can have an initializer list, provided that it only computes constants (assuming that the parameters are known constants). In our case, no computation is required.

Unfortunately, we could not define a constructor like Point.polar() as a constant constructor, since it makes use of functions like sqrt() whose results are not considered constant. It’s okay for Point to have such a constructor—we just can’t invoke it using const. So any points we’d create with this constructor would not be constants.

We don’t have to create constant points all the time. In fact, we can still call a constant constructor using new. If we do that, there are no restrictions on the arguments we pass, but the results will not be constants.

Constants can be computed in advance, once, and need not be recomputed. Constants are canonicalized—only one constant of a given value is ever created in a Dart program.

2.12 Class Methods

Class methods are methods that are not tied to individual instances. We’ve already encountered some of these when discussing class variables. The accessors induced by class variables are class methods. We may speak of class getters or class setters. Besides the automatically induced class accessors, programmers can define such accessors explicitly. Indeed, one can define ordinary methods on a per-class basis as well.

We saw such an example in Chapter 1. In class Point we had

static distance(p1, p2) {
   var dx = p1.x() - p2.x();
   var dy = p1.y() - p2.y();
   return sqrt(dx * dx + dy * dy);
}

Just like class variable accessors, the names of user-defined class methods are available within the class that declares them. Outside the class, the methods can only be accessed if they are prefixed with the name of their enclosing class, for example, Point.distance(aPoint, anotherPoint).

It is a compilation error to use this inside a class method. Since a class method is not specific to any instance, this is not defined within it.

If you try and call a class method that doesn’t exist, you’ll get a runtime error:

Point.distant(aPoint, anotherPoint); // NoSuchMethodError!

This behavior highlights an interesting design issue. Since this is a static call, the Dart compiler can detect, at compile-time, that there is no class method distant() in Point. Why not give a compilation error on the spot? After all, we know for a fact that this code must fail at runtime.

However, Dart is designed to keep out of the programmer’s way as much as possible. During development, incomplete fragments of code may be written and tested in a spirit of experimentation. A programmer may prefer to test one path through the code, knowing full well that another path is not yet functional.

In systems that insist on complete definitions developers work around such requirements by defining stubs. These stubs will lead to incorrect results or failure at runtime, and their only purpose is to allow development to move forward by silencing an overzealous compiler.

Of course, the compiler’s zeal is not entirely unmotivated. The undefined method might just as easily be a typographical error:

Pont.distance(aPoint, anotherPoint); // NoSuchMethodError!

The difference between the undefined class Pont and the intended class Point can be very hard for a human to detect and the programmer may spend a long, frustrating time trying to understand the cause of the failure. Worse, the failure may not be on a tested code path, resulting in a crash in production.

Fortunately, Dart will give a static warning in these cases, but still compile and run the code. This provides the flexibility of a dynamic system with much of the assurance provided by a conventional type system.

It’s important to remember that class members are never inherited

class ExtendedPoint extends Point {
  var origin = new Point(0,0);
  get distanceFromOrigin => distance(origin, this);
  // sorry, NoSuchMethodError!
  }

The getter method distanceFromOrigin cannot make use of the name distance. We could have defined distanceFromOrigin in Point itself (assuming we also defined origin) but distance is not in scope in any subclass of Point. To make this work, we’d have to write

get distanceFromOrigin => Point.distance(origin, this); // ok

Since class methods are never inherited, it makes no sense to declare an abstract class method. You won’t get far if you try, as it is not even syntactically valid.

2.13 Instances, Their Classes and Metaclasses

Every object is an instance of a class. Since everything is an object, classes are objects too, and since classes are objects, they are themselves instances of a class. The class of a class is often referred to as a metaclass. The Dart language mandates that classes have type Type, but does not specify what class they belong to.

In a typical implementation, the classes might be instances of a private class Type. What would the class of Type be? Usually it would be Type—that is, Type would be an instance of itself, resolving what might otherwise be an infinite regress. The situation is illustrated in the following diagram, showing an object aC which is an instance of a class C which is an instance of Type, which is an instance of itself.

Image

The only way to reliably discover what the class of an object is is via reflection, which we will discuss extensively in Chapter 7. Objects do support a getter runtimeType, which by default does return the class of the object. However, subclasses are free to override runtimeType.

It is important that one can override runtimeType. Recall our Proxy example above. The proxy object must be indistinguishable from the real objects. If runtimeType was hardwired, we could find out that these proxies were instances of Proxy.

More generally, the principle that the only thing that matters about an object is its behavior can all too easily be violated. It requires great care to ensure that nothing in the language causes irrelevant implementation details of this nature to leak.

On the other hand, some code might have a legitimate need to find out the actual class of an object. An IDE, or debugger, or a profiler is expected to show true and accurate information. The mechanism for this is reflection.

Finally, a rather disturbing anomaly. It is too easy to confuse calling class methods with invoking instance methods on type literals

Type.runtimeType; // Ouch! NoSuchMethodError
(Type).runtimeType; // works

The first line is a call to a non-existent class method; the class Type does not have a class method runtimeType. The second invokes the method runtimeType defined for all objects on the type literal Type. We could resolve this issue by viewing class methods as instance methods on the corresponding Type object but this not the case at the moment.

2.14 Object and Its Methods

The interface of class Object is exceedingly small. We have in fact encountered almost all of its methods. We encountered the operator method == along with the getter hashCode in section 2.5. Section 2.10 revolved around noSuchMethod(). We saw runtimeType in the immediately preceding section. And perhaps the most commonly used method of all is toString(). This method returns a string representing the object. The default version will typically print out something like ‘An Instance of C’ where C is the name of the class of the object. It’s often useful override it to something more meaningful.

Altogether, only five methods constitute the interface shared by all objects. An outline of class Object would look like this

class Object {
 bool operator == (another) { ... }
 int get hashCode { ... }
 String toString(){ ... }
 noSuchMethod(Invocation im) { ... }
 Type get runtimeType{ ... }
}

You’ll notice that, for the first time, types have made their appearance in our code. Dart code may be decorated with type annotations, and the Dart core libraries almost always are. As you might expect, equality yields a Boolean result, the hash code is an integer, toString() returns a string and the runtime type is, well, a Type. As discussed above, noSuchMethod() takes an Invocation. The syntax should be familiar to most programmers.

We’ve elided the bodies of the methods. In fact, if you look at the source code you’ll find something close, yet different:

class Object {
 bool operator == (other)=> identical(this, other);
 external int get hashCode;
 external String toString();
 external noSuchMethod(Invocation im);
 external Type get runtimeType;
}

Except for == whose implementation we figured out earlier, all the other methods are supplied by the Dart implementation rather than implemented directly in Dart code. They are marked as external indicating that their implementation is being provided elsewhere. The external mechanism is useful for declaring code whose implementation comes from the outside. There are various ways such code can be provided—via a foreign function interface, as primitives of the underlying implementation (as is the case here) or possibly even dynamically generated implementations. The first of these scenarios is by far the most likely one for most programmers.

2.15 Mixins

Sometimes single inheritance can be rather constraining. A classic real-life example comes when implementing GUI frameworks. You find that you need a compound widget that groups together several smaller widgets. It is very natural and convenient to view this compound widget as a collection. Collections have some useful shared functionality, so you’d like to define CompoundWidget to inherit from Collection. There’s only one catch; you also want CompoundWidget to inherit from Widget, since it is a widget and there is a lot of useful functionality to be had there as well.

One approach has been to allow a class to have multiple superclasses. Experience has shown this to be more trouble than it is worth. Instead, Dart uses mixin-based inheritance.

To understand mixin-based inheritance, we shall examine a simplified collection class

abstract class Collection {
  forEach(f); // perform the function f on each element of the collection
  where(f) ...
  map(f) ...
}

What we really want from Collection is its body—the stuff between the curly braces. The body contains the functionality the class declaration itself contributes. It is the difference between a Collection and its superclass. We call this difference a mixin.

Image

We’d like to take this functionality and “mix it in” to CompoundWidget as illustrated below.

Image

We could achieve our goal by copying the contents of Collection into a new class, WidgetCollection

abstract class WidgetCollection extends Widget {
  forEach(f); // perform the function f on each element of the collection
  where(f) ...
  map(f) ....
}

but every time Collection was changed, we’d need to update WidgetCollection. In general, such copy/paste coding can lead to all sorts of problems—maintenance, type checking, legal issues etc. Happily, we can use mixins to address this issue.

Each Dart class has a mixin. The mixin, as noted, is the class-specific functionality defined in the class body. So we can graphically depict each class C with superclass S as

Image

One way to look at a mixin is as a function that takes a superclass S and creates a new subclass of S with a particular body:

mixinCollection(S) { // ILLEGAL: just an illustration
 return class Collection extends S {
  forEach(f); // perform the function f on each element of the collection
  where(f) ...
  map(f) ....
}}

Each application of such a function to a given class yields a new subclass. We therefore speak of mixin application as the operation of deriving a class using a mixin and a superclass. We write S with M to denote the application of mixin M to superclass S. Obviously, S must name a class, but how do we name a mixin? By naming a class; the class implicitly defines a mixin via its body, and that is the mixin we’ll apply to S.

We now know how to define CompoundWidget without replicating code.

class CompoundWidget extends Widget with Collection {
 ... compound widget stuff
}

The superclass of CompoundWidget is Widget with Collection, which is the mixin of class Collection applied to the superclass Widget, yielding a new, anonymous, class. The desired hierarchy now looks like

Image

At the time of this writing, a class used as a mixin must not have an explicitly declared constructor. Violating this restriction will cause a compilation error. This restriction may be relaxed in the future.

2.15.1 Example: The Expression Problem

In this section we’ll consider a classic design challenge known as the expression problem and see how mixins facilitate an elegant solution. The expression problem is so-named because the concrete example used is the implementation of a language of expressions. However, the issues the expression problem brings up are characteristic of a much broader set of programming examples.

Consider the following language of expressions:
ExpressionExpression + Expression|ExpressionExpression|Number An AST for this language might have the form

class Expression {}

class Addition extends Expression {
 var operand1, operand2;
}

class Subtraction extends Expression {
 var operand1, operand2;
}

class Number extends Expression { int val;}

Now suppose you want to define an evaluator for this language. You can define it in classic OO style by adding eval methods to each leaf class above:

get eval => operand1.eval + operand2.eval;  // in Addition
get eval => operand1.eval - operand2.eval; // in Subtraction
get eval => val; // in Number

This is problematic however. When you want to convert such expressions to strings, you need to add another method to the original hierarchy. There is an unbounded number of similar functions and your classes will rapidly become unwieldy. Besides, not everyone who wants to define new functionality has access to the original source code.

An alternative is to define the evaluator as a function outside of the AST classes. You would have to test the incoming expression to see what type it was, then take the appropriate action. This is tedious and inefficient, and so one typically uses the visitor pattern instead. Regardless, this organization has a dual problem: although adding functions is easy, adding new types is hard.

The dilemma is illustrated in the following table:

Image

Types correspond to rows in the table, and functions correspond to columns. The object-oriented style makes it easy to add rows, but adding columns is invasive. The functional style does the opposite—adding columns is easy, but adding rows is invasive.

What we really need is a way to add individual entries to the table independently. The problem can be nicely addressed with mixins. The following Dart code shows how. We start out with three initial datatypes, but we define them as abstract classes because these won’t be the datatypes we actually instantiate. They serve to define the structure of the types and their constructors.

library abstract expressions;


abstract class AbstractExpression{};
abstract class AbstractAddition {
 var operand1, operand2;
 AbstractAddition(this.operand1, this.operand2);
}


abstract class AbstractSubtraction {
 var operand1, operand2;
 AbstractSubtraction(this.operand1, this.operand2);
}


abstract class AbstractNumber {
 var val;
 AbstractNumber(this.val);
}

Now let’s define our first function, the evaluator. We’ll do this via a set of mixin classes.

library evaluator;


abstract class ExpressionWithEval {
 get eval;
}


abstract class AdditionWithEval {
 get operand1;
 get operand2;
 get eval => operand1.eval + operand2.eval;
}


abstract class SubtractionWithEval {
 get operand1;
 get operand2;
 get eval => operand1.eval - operand2.eval;
}


abstract class NumberWithEval {
 get val;
 get eval => val;
}

The evaluator above is completely independent of the type hierarchy; notice we have not imported so much as a single dependency.

The actual types our clients should use are defined separately

library expressions;


import 'abstractExpressions.dart';
import 'evaluator.dart';


abstract class Expression = AbstractExpression with ExpressionWithEval;


class Addition = AbstractAddition with AdditionWithEval implements Expression;


class Subtraction = AbstractSubtraction with SubtractionWithEval
                                        implements Expression;


class Number = AbstractNumber with NumberWithEval implements Expression;

Each concrete AST type is defined as a mixin application, extending the appropriate abstract datatype with the corresponding evaluator mixin.

We can add a main() function to expressions that builds a simple expression tree. This is possible because the various AST node classes have synthetic constructors implicitly defined for them based on the constructors of their superclasses.

main(){
 var e = new Addition(new Addition(new Number(4), new Number(2)),
                        new Subtraction(new Number(10), new Number(7))
             );
}

What is the point of the separation of abstract and concrete types? The role of expressions is to define our overall system by connecting its various components via mixin application. The role of abstractExpressions is to define the form of our AST nodes. Keeping these separate will allow us to extend the system by modifying expressions without touching our datatype representation.

The general pattern is that each concrete class is based on extending an abstract class that defines its data representation, with a series of mixins representing all the functions on that datatype. The approach works because we define a separate mixin for each combination of function and type. For example, above, each eval method is defined in its own mixin class.

If we want to add a type, we can do so independently. We will add an AST node for multiplication:

library multiplication;


abstract class AbstractMultiplication {
 var operand1, operand2;
 AbstractMultiplication(this.operand1, this.operand2);
}

Again the addition is completely independent of the original class hierarchy and of any functions. At this stage, we also have to define how multiplication is evaluated. We can define that as a separate library

library multiplication evaluator;


abstract class MultiplicationWithEval {
 get operand1;
 get operand2;
 get eval => operand1.eval * operand2.eval;
}

which again, is completely independent. We do need to create the corresponding concrete class in expressions.

class Multiplication = AbstractMultiplication with MultiplicationWithEval
                                 implements Expression;

As you can see, it follows the same pattern as all the other types. We will need to add the following imports to expressions for the above to be valid.

import 'multiplication.dart';
import 'multiplicationEvaluator.dart';

The rest of the expressions library remains unchanged.

It would be nice to print the tree we build in main(). We can modify main() to do this; we’ll also change our code to make use of the new type.

main(){
 var e = new Multiplication(new Addition(new Number(4), new Number(2)),
                           new Subtraction(new Number(10), new Number(7))
             );
 print('$e = ${e.eval}'),
}

The print-out is less informative than we would like, since e is printed out using the default implementation of toString() inherited from Object. To address this, we can add a specialized implementation of toString() to our class hierarchy.

library string converter;


abstract class ExpressionWithStringConversion {
  toString();
}


abstract class AdditionWithStringConversion {
   get operand1;
   get operand2;
  toString() => '($operand1 + $operand2)';
}


abstract class SubtractionWithStringConversion {
  get operand1;
  get operand2;
 toString() => '($operand1 - $operand2)';
}


abstract class NumberWithStringConversion {
  get val;
 toString() => '$val';
}


abstract class MultiplicationWithStringConversion {
  get operand1;
  get operand2;
 toString() => '($operand1 * $operand2)';
}

Again, we follow the formula of defining a mixin per method/type combination. This time, we know that the hierarchy involves multiplication and have defined the corresponding case in the same library as the other cases. Likewise, we’ll want to refine expressions to integrate the new functionality.

library expressions;


import 'abstractExpressions.dart';
import 'evaluator.dart';
import 'multiplication.dart';
import 'multiplicationEvaluator.dart';
import 'stringConverter.dart';

abstract class Expression = AbstractExpression with
              ExpressionWithEval, ExpressionWithStringConversion;

class Addition = AbstractAddition with
      AdditionWithEval, AdditionWithStringConversion implements Expression;

class Subtraction = AbstractSubtraction with
      SubtractionWithEval, SubtractionWithStringConversion implements Expression;

class Number = AbstractNumber with
            NumberWithEval, NumberWithStringConversion implements Expression;

class Multiplication =
   AbstractMultiplication with MultiplicationWithEval,
                           MultiplicationWithStringConversion implements Expression;

Our main function remains unchanged, but it now prints a nice description of the tree:

((4 + 2) * (10 - 7)) = 18

One can continue the process of extension as desired. You can add as many types as you want, and as many functions as you want. You just have to define the final forms of the types you are going to use as mixin applications as shown above. For each type, the corresponding case for every function is defined by a separate mixin class. Adding a new function does require modifying those mixin applications, but this is really more like tweaking your make files to include the additional functions and/or types. If new types and new functions are defined independently, we can always define a mixin that adds the logic for applying the new function to the new type separately and mix it in as well.

In terms of the table, each mixin represents an individual entry, and each mixin application composes a row of the table.

We will revisit this design again when we discuss types in Chapter 5.

2.16 Related Work

As noted in the introduction, Dart’s object model is heavily influenced by that of Smalltalk[1]. Smalltalk was the first pure object-oriented language, and many of the ideas here are directly tied to it. The notion of a metaclass hierarchy that circles back on itself originates there though Dart’s metaclass hierarchy is rather simple compared to Smalltalk’s. Unlike Smalltalk, instance construction is handled via constructors rather than methods, following the tradition established by C++. However, we address the major flaw of classic constructors, their inability to produce objects that are not freshly allocated by their class.

The use of noSuchMethod() is closely modeled after Smalltalk’s doesNotUnderstand:.

The notion of representation independence can be traced back to the language Self. However, Dart does not uphold the principle of uniform reference, due to the influence of Javascript and the C style syntax.

Mixins originated as an idiom in certain Lisp dialects. The linguistic model of mixins used here was introduced by William Cook and the author in 1990[4], and first implemented in Strongtalk[5],[6], an innovative Smalltalk system with a particularly strong influence on Dart. Closely related constructs exist in languages such as Newspeak, Scala (where they are called traits) and Ruby.

The name “expression problem” is due to Philip Wadler. The expression problem has been discussed extensively in the literature ([7], [8], [9]). Proposed solutions vary depending on the language and requirements involved.

2.17 Summary

Dart is a pure object-oriented, class-based language, which means that all runtime values are objects and every object is an instance of some class.

Objects have state and behavior. The state is only available via special accessor methods—getters and setters. This ensures that all computation on Dart objects is done via a procedural interface.

Classes are reified at runtime and so must be objects themselves. Hence every class is an instance of a metaclass of type Type. Every class has at least one constructor; constructors are used to create objects. Some objects are constant, meaning they can be pre-computed at compile-time.

Every Dart class has a unique superclass, except for the root of the class hierarchy, Object. All Dart objects inherit common behavior from Object.

Dart supports mixin-based inheritance: every class induces a mixin capturing its unique contribution to the class hierarchy. Mixins allow the code of a class to be reused in a modular fashion independent of its place in the class hierarchy.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.217.199.122