Chapter 6. Expressions and Statements

Expressions and statements are the backbone of most programming languages. We have seen many of Dart’s expressions and statements already. In this chapter we will give a systematic overview of the expressions and statements of Dart.

6.1 Expressions

6.1.1 Literals

We’ve encountered all of Dart’s literals by now, but this is an opportunity to go through them all in an orderly fashion.

6.1.1.1 Integers

God made the integers; all the rest is the work of man.

Leopold Kronecker

In Dart, integers can be of arbitrary size. However, the use of very large integers is likely to be relatively expensive. Beyond a certain boundary, which varies among implementations, integers can no longer directly take advantage of the underlying hardware representation. Consequently, operations on such large integers become significantly more costly.

The implementation is free to use instances of various classes to implement integers. The only requirement is that any such class implements the built in class int. In practice, different classes are likely to be used depending on the size of the integers represented.

When one asks an integer for its runtimeType, the answer is always int, which is also the type the static checker ascribes to integer literals. Therefore, the only way to discover the underlying implementation class of an integer is through reflection, as discussed in the next chapter.

Integer objects are considered identical if they represent the same mathematical value.

A further complication arises in Dart implementations that compile into Javascript. In Javascript, all numbers are represented as IEEE doubles; there are no integers. As a result, it is prohibitively expensive to implement integer semantics on top of Javascript.1 On such an implementation, integers will be approximated when they lie outside a given range (an absolute value of approximately 253).

1. One can only hope that in due course, Javascript will discover the integers.

You can’t define subclasses of int, and you cannot have another class implement int. These restrictions are a compromise that allows Dart implementations to provide high performance at the expense of the principle that one should be able to subclass or implement any class.

6.1.1.2 Doubles

Doubles are objects that implement 64-bit floating point numbers in accordance with the IEEE 754 standard. Doubles are instances of the class double in dart:core.

The rules for identity of doubles are subtly different than the rules for equality. Simply put, two doubles are identical if the underlying bit patterns representing the floating point value is the same. For the most part, this means that two doubles are identical iff they are equal, but there are two exceptions:

1. The doubles -0.0 and 0.0 are equal but not identical.

2. A NaN (Not a Number) is identical to itself, but not equal.

You can’t define subclasses of double and you cannot have another class implement double. The same rule holds num, for the common supertype of double and int.

6.1.1.3 Booleans

There are exactly two Boolean objects: true and false. They are members of the class bool.

Unlike some languages, one cannot coerce an arbitrary expression to a Boolean value. If you try to write

var x = someComputation();
...
if (x) print(x.foo()); // Don't do this !

expecting that the language will turn a non-null value into true, you will be disappointed. Remember, Dart frowns on built-in coercions. If x is true, the print will execute as expected. However, if x is any other object, it will be treated as false in production mode and nothing will be printed. In checked mode it will fail. So the best thing to do is to write proper predicates when performing tests:

if (x != null) print(x.foo());

This makes it clear to the reader what you are actually testing for and does not rely on coercions, which can be very problematic. Of course, if x really is a Boolean value, it is perfectly fine to write

if (x) print(x.foo());

In short, true is the one and only truth in Dart.

You may well ask why, given our avowed aversion to coercions, do we allow other non-Boolean objects to be treated as false? If we were assured that Dart would always run on a Dart VM, we could probably enforce the consistent use of Booleans in all tests. However, given the constraints of compilation to Javascript it turns out to be too costly to insist on using real Boolean values at all times. It is one of the cases where Dart makes a pragmatic compromise.

You can’t define subclasses of bool and you cannot have another class implement bool.

6.1.1.4 Strings

String literals can take several forms in Dart. They can be delimited by either single or double quotes:

'And I quote: "Strings delimited by single quotes can contain double quotes"'
"And I quote: 'Strings delimited by double quotes can contain single quotes'"

Typically, users don’t have to remember which quote marks are used to delimit a string. Either way works, making the language a little bit easier to learn. Also, when quotes appear in strings, dealing with them is also simpler, as the examples above show. Naturally, the delimiters have to match—you cannot start a string with a single quote and close it with a double quote for example.

'An invalid string" // Illegal! will not parse
"Another invalid string' // Just as illegal

Ordinary strings are limited to a single line. If you need a string that covers multiple lines, you can use a multiline string. Multiline strings are delimited with triple quotes; again these may be either triple sets of single quotes or triple sets of double quotes.

"'And I quote:
"Strings delimited by single quotes
can contain double quotes"
"'
"""And I quote:
'Strings delimited by double quotes
can contain single quotes'
"""

There is also another option, which is to split your string into pieces and write the individual pieces next to each other:

' "And I quote: '
'Strings delimited by single quotes'
' can contain double quotes"'

Dart will implicitly concatenate adjacent strings. The resulting string will not span multiple lines however.

Dart supports a variety of escape sequences within a string. These include

• for newline.

• for carriage return.

• f for form feed.

•  for backspace.

• for tab.

• v for vertical tab.

• u{h}, where h is a sequence of hexadecimal digits, for the unicode scalar denoted by h.

• ux1x2x3x4, where x1−−x4 are hexadecimal digits, a shorthand for u{x1x2x3x4}.

• xx1x2, a shorthand for u{x1x2}.

Otherwise k is the same as k.

Dart strings also support string interpolation. String interpolation allows Dart expressions to be embedded within a string literal, as in:

print('The value of 2*2 is ${twice(2)}'),

which prints The value of 2*2 is 4, assuming a reasonable implementation of twice() is in scope. As you can see, a $ sign indicates the appearance of an embedded expression, which is then delimited by curly braces. The embedded expression is evaluated and its toString() method called, and the result is inserted into the string at the point where the expression was given. If the expression consists of a single identifier, the curly braces may be elided:

var roomNumber = 101;
print('Welcome to room $roomNumber'),

which prints Welcome to room 101.

In addition, Dart supports raw strings. Raw strings do not support escape sequences; they contain exactly what appears between the quotes. Raw strings come in both single line and multiline variants. Raw strings are prefixed with an r character.

print(r'A raw string '), // prints: A raw string , with no new line

Much like integers, string literals can be implemented by various classes, but this is undiscoverable except via reflection. Asking a string literal for its runtime type always yields String, which is the type the static checker ascribes to string literals as well. The class String cannot be subclassed or implemented by user code, just as with int, double, num and bool.

Dart represents strings as a sequence of UTF-16 code units. The Dart core library includes the Runes class that can convert a string into a unicode representation.

6.1.1.5 Symbols

Symbols are used to represent the names of declarations in a program. A symbol begins with a pound sign #, and is followed by either one or more dot-separated identifiers, or an operator. Here are some examples:

#MyClass #i #[] #com.evil empire.forTheWin

Here are some illegal symbols:

#. #.. #++ #= #&= #|| #&&#!= #!

The above are all operators that are not user definable. The set of non-user-definable operators includes:2

2. To create symbols that do not have a literal form, one needs to explicitly invoke a constructor on class Symbol These situations typically arise in reflective code. Besides the operators listed above, there is the notable case of unary minus, whose name, for reflective purposes, is “unary-”.

• Assignment, including the basic assignment operator and all the compound ones

• Dots, as used in member selection, and double-dots, used in cascades

• Inequality, which is always defined in terms of the equality operator

• Negation via !

• Postfix operators: increment and decrement

• Logical Boolean operators

Naturally, the static type of a symbol literal is Symbol.

6.1.1.6 Lists

List literals are usually written as a comma-separated series of expressions delimited by square brackets.

All list literals implement the interface of class List. It is convenient to think of the literal [] is a shorthand for new List(), and [‘a’, 2, true] as a shorthand for:

new List() ..
        add('a')..
        add(2)..
        add(true);

Strictly speaking, the above interpretation is not true because the Dart implementation is not required to use the factory constructor of class List at all. However, as a first approximation, we are not far off.

List is a generic class, and so one might reasonably ask whether [1, 2, 3] creates a List<int>? Does it correspond to:

new List() ..
        add(1)..
        add(2)..
        add(3);

or to:

new List<int>() ..
        add(1)..
        add(2)..
        add(3);

The answer (again, just as an approximation for pedagogical purposes) is the former. Dart does not try to infer a more precise type from the elements of the list literal. One reason is that performing such type inference at runtime is quite expensive. Another reason is optional typing.

To obtain the latter version, we can choose to provide a type argument explicitly by writing <int>[1,2,3]. In fact, the syntax [e1, . . . , en] really is a shorthand for < dynamic > [e1, . . . , en]. We can also think of the syntax < T > [e1, . . . , en] as an approximation of:

new List<T>() ..
        add(e1)..
        ...
        add(en);

List literals can be made into compile-time constants as noted in Section 6.1.4 by prefixing them with the reserved word const. The use of const is orthogonal to the use type parameters. Hence const[e1, . . . , en] is precisely a shorthand for const < dynamic > [e1, . . . , en], whereas const < T > [e1, . . . , en] is a close approximation for:

const List<T>() ..
        add(e1)..
        ...
        add(en);

6.1.1.7 Maps

Map literals are written as a comma-separated series of key-value pairs delimited by curly braces. In each pair, the key is written first and separated from the value with a colon, that is, key: value.

All map literals implement the interface of class Map. It is convenient to think of the literal {} as a shorthand for new Map(), and of {‘a’: ‘a’, ‘b’: 2, ‘c’: true} as a shorthand for:

new Map() ..
      ['a'] = 'a'..
      ['b'] = 2..
      ['c'] = true;

As in the case of lists, the above interpretation is only an approximation because the Dart implementation is not required to use the factory constructor of class Map.

Map is a generic class with two type parameters—the first corresponding to the type of the keys, the second to the type of the values. The expression {‘a’:1, ‘b’: 2, ‘c’: 3} creates a Map<dynamic, dynamic> rather than a Map<String, int>.

We can choose to provide type arguments explicitly by writing <String, int>{‘a’:1, ‘b’: 2, ‘c’: 3}. In general, {ke1 : ve1, . . . , ken : ven} is a shorthand for < dynamic, dynamic > {ke1 : ve1, . . . , ken : ven}. We can also think of the syntax < K, V > [{ke1 : ve1, . . . , ken : ven} as an approximation of

new Map<K, V>() ..
      [ke1] = ve1..
      ...
      [ken] = ven;

Map literals can be made into compile-time constants as noted in Section 6.1.4 by prefixing them with the reserved word const. The use of const is orthogonal to the use type parameters. Hence const{ke1 : ve1, . . . , ken : ven} is precisely a shorthand for const < dynamic, dynamic > {ke1 : ve1, . . . , ken : ven}, whereas const < K, V > {ke1 : ve1, . . . , ken : ven} is a close approximation for:

const Map<K, V>() ..
       [ke1] = ve1..
       ...
       [ken] = ven;

6.1.1.8 Functions

Function literals were already discussed in Section 4.4. The rules for function types were given in Section 5.6. We can now give the type rules for function literals. There is no syntax for declaring an explicit return type for a function literal, so function literals always have return type dynamic. The argument types of a function literal are determined in the same way as any other function. If types are declared, they are used, otherwise the type of an argument is taken to be dynamic. Based upon the types of the arguments and whether they are required or optional, positional or named, the obvious function signature is determined. Examples:

(x) => 2*x; // (dynamic) → dynamic
(num a, num b){ return a*b;} // (num, num) → dynamic
(String k, [Function f]) => m.putIfAbsent(k, f);
  // (String, [Function]) → dynamic

6.1.2 Identifiers

Identifiers in Dart are very similar to those in most other languages. An identifier can start with one of the characters A-Z, a-z, or $. Further characters can be any of the preceding or one of the numerals 0-9.

Dart does not allow non-ASCII letters in identifiers. This choice was made after much discussion. We chose to ensure that Dart programs be readable by virtually any professional programmer on earth, admittedly at the expense of convenience for users of non-Roman scripts.

An identifier, as its name suggests, identifies a declaration. It may identify a variable, a function or a type. An identifier might also denote a library, but then it cannot be used in an expression on its own.

When evaluating an identifier, the first step is to find the declaration it identifies. This involves a process of searching for the declaration, starting with the immediately surrounding scope. If no matching declaration (that is, a declaration whose simple name is the identifier we are searching for) is found, the search continues in the enclosing lexical scope, recursively, until the top level of the enclosing library. If we find a declaration, we have some idea what to do with it; but it may be that no declaration is found.

The search for the appropriate declaration is done statically by the compiler; once it knows what declaration is involved, it will generate the appropriate code to evaluate at runtime.

To see how this works, we’ll look at the rather contrived code below:

var global = 0;
var shadowed = 1;

aFunction(x) => x*x + global + shadowed;

class AClass<T> {
  static var aClassVariable = 2;
  var anInstanceVariable = 3;
  static var shadowed = 4; // shadows top-level variable


static aClassMethod() {
   print(shadowed); // 4
   print(aClassVariable); // 2
   print(anInstanceVariable); // error
}


aMethod(aParameter) {
 var aLocal = 5;
 var anUnassignedLocal;
 var shadowed = 6; // shadows static variable
 localFunction(x) => 7;

 {
  var moreLocal = 8;
  var shadowed = 9; // shadows local
  moreLocalFunction(y) {
   return moreLocal;
  }
  print(global); // 0
  print(shadowed); // 9
  print(aFunction);
  print(AnotherClass);
  print(F);
  print(AClass);
  print(T);
  print(aClassVariable); // 2
  print(anInstanceVariable); // 3
  print(aMethod);
  print(anotherMethod);
  print(aParameter);
  print(localFunction);
  print(aLocal); // 5
  print(anUnassignedLocal); // null
  print(moreLocal); // 8
  print(moreLocalFunction);
  }
  print(shadowed); // 6
 }
 anotherMethod() => 10;
}

class AnotherClass{}

typedef int F(int x);

The simplest example is the evaluation of the identifier moreLocal. Our search finds a declaration of the same name in the immediately enclosing scope, which introduces a local variable. The identifier evaluates to the value stored in the local variable at the time of evaluation—in our case, 8. This is so obvious as to be painful, but it is necessary if one is to fully understand the rules. The situation with the identifier aLocal is almost exactly the same; the only difference is that we need to climb up the scope chain one more level to find the relevant declaration. And the case of anUnasssignedLocal is the same again, except that in the latter case the value of the expression will be null because the variable has not been initialized, whereas the former expression evaluates to 5.

Evaluating aParameter is very similar. The name aParameter is declared via a formal parameter declaration one more level up the scope chain, and the value of the formal parameter will of course vary from one invocation to the next.

What if the declaration we find does not denote a variable? There are several possibilities. The declaration might denote a function; the function might be a getter or a normal method (it cannot be a setter, because the name of a setter always ends with = and so is never an identifier).

If an identifier denotes a getter, the result of evaluation is the result of invoking the getter. If it is a static or top-level getter, we invoke it and return the result. If we have an instance getter, we invoke the getter on this.

As an example, consider the identifier shadowed. Inside aMethod it is first evaluated in the innermost scope, and so the innermost declaration is found, and the value of that variable, 9, is the result. At the very end of the method, we evaluate shadowed in the method’s outermost scope, yielding a value of 6. Inside the class method aClassMethod, we reference shadowed again. Here, we begin in the method scope, and go up the scope chain until we reach the classes’ static scope, which defines a getter for the class variable shadowed. Invoking this getter produces the value 4.

In contrast, inside the top-level function aFunction, shadowed evaluates to 1, because we start searching for a declaration inside the scope of aFunction, climbing upwards to the scope of its formal parameters and on to the library-level scope, where we find a suitable top-level getter implicitly declared. The same process works for global in that case; when we evaluate global inside of aMethod, our search will traverse a series of nested scopes until we reach the library scope.

If the declaration denoted by the identifier is for an instance method, then we extract a closure that will invoke the method on the current value of this when called, as described in Section 6.1.7.

Library methods, class methods and local functions are all treated the same; the result of evaluation is the function object (4.7) representing the function/method. For library or class methods, that object will be fixed; for local functions, a new object is created every time we enter the scope where the function is declared.

Another possibility is that id refers to a type declaration—either a class or a typedef. Every class and type alias is reified in Dart as a unique object, and that is the result of the evaluation in that case. However, the identifier could also denote a type variable. In that case, the result will be the value of the type variable at the time of evaluation—just like a normal variable. The type variable’s value is a property of the receiver; any given instance of a generic class is constructed with actual type parameters via an instance creation expression like new List<String>. The actual type argument is itself an object tied to a type (String in this case) and that becomes the value of the corresponding formal type parameter.

Of course things can be more elaborate, as we saw in Chapter 5. For example, consider what happens when the identifier denotes a generic class such as List. No explicit value has been provided for List’s type parameter. The default value for a type parameter is always dynamic so List denotes a type object representing class List<dynamic>.

Now suppose we don’t find a declaration at all. That can happen in perfectly correct code; we might be referring to a member inherited from a superclass. The rule is that if we haven’t found a declaration for id in the lexical scope, we evaluate id as if it was the expression this.id.

We can see this when we attempt to reference the instance variable anInstanceVariable inside of aClassMethod. However, this variable (and hence its getter) is not in scope. The reference is then interpreted as a getter call on this. In instance methods, this rule works well, allowing access to inherited members. Since accessing this inside a class method is illegal, this is a compile-time error in our case.

6.1.2.1 Reserved Words and Built-in Identifiers

Of course, identifiers cannot be reserved words. The following code will never parse correctly:

var if;

Not all of Dart’s keywords are reserved words, however. Dart distinguishes reserved words, which may never be used to name entities in a Dart program, from built-in identifiers. Here are Dart’s reserved words: assert, break, case, catch, class, const, continue, default, do, else, enum, extends, false, final, finally, for, if, in, is, new, null, rethrow, return, super, switch, this, throw, true, try, var, void, while, with. And here are the built-in identifiers: abstract, as, deferred, dynamic, export, factory, get, implements, import, library, operator, part, set, static, typedef.

Interesting situations arise with respect to Dart’s built-in identifiers. Consider

get get => 'get'; // don't do this!

The above is perfectly legal, though very bad style. It defines a getter named get that returns the string ‘get’. Get it? The Dart compiler can easily tell what each occurrence of “get” signifies, but since humans might not, this kind of code is strongly discouraged.

The distinction between reserved words and built-in identifiers allows for easier migration of code from other languages to Dart. In particular, it is often surprisingly easy to convert Javascript code to Dart. Allowing identifiers that would be legal in Javascript to be used in Dart, even when Dart has special uses for them, facilitates such migration.

Built-in identifiers may not be used as type names. Neither classes, typedefs nor type parameters may be given a name that is a built-in identifier. Built-in identifiers (except dynamic) cannot appear in type annotations either. This disallows situations like

get get get => new get(); // compile-time error

Obviously, you, the human reader, can immediate see that the intent is to define a getter named get that returns type get, but the parser would need to look several tokens ahead to make sense of what was going on. These restrictions do not impact migration since no types occur in Javascript programs. Furthermore, since types start with upper-case letters by convention, this rule poses no difficulty for Dart code. The only downside is to limit the employment opportunities for writers of puzzler books.

6.1.3 this

The reserved word this, used as an expression, refers to the receiver of the current method invocation. In addition, this can appear in the context of a constructor, where it denotes the newly created object. One cannot refer to this in a static or library method because no object was the receiver of the invocation.

Note that in a constructor, the keyword this can appear in the context of special formal parameters, and in redirecting constructors it marks calls to other constructors of the same class. However, neither of these uses of this are expressions in their own right.

6.1.4 Constants

A constant expression is an expression that may be evaluated at compile-time. The value of a constant expression can never change. Constants include literal numbers, Booleans, null, constant lists and maps, top-level functions, static methods as well as constant objects and a few specific compound expressions whose subparts are constants.

Constant objects are created by replacing new with const in instance creation expressions as shown in the next section. Constant object creation is only possible under very specific circumstances. The class must have a legal const constructor, and the arguments to the constructor must themselves be constants.

Given

library constants;

const aTopLevelConstant = 99;
thrice(x) => 3*x;

class Foo {
 static threeCubed() => thrice(thrice(3));
}

class Konst {
 final x;
 const Konst(this.x);
}

the following expressions are constants:

"def" 1 2.5 true false null const[7, 8, 9.0] const{}
aTopLevelConstant thrice
Foo.threeCubed 'abc' 3+4
1 - 0 6*7 -1 2 > 3 3 < 3.0 7 >= 3+4
9 <= -0 17 % ( 2*2*2 + 8) '$aTopLevelConstant'
const Konst(42);

In contrast, these expressions are not constants:

thrice(2)
threeCubed == 0
'${ const Point(0, 0)}'
const Konst(thrice(14));

6.1.5 Creating Objects

Objects are created by calling constructors, as described in Section 2.9. Constructors are called using instance creation expressions. The most common form of instance creation is new ConstructorName(args). Some examples:

new List<int>(); // calling a constructor with type arguments
new Map(); // calling a constructor of a generic class without type arguments
new Point.origin(); // calling a named constructor

Using new does not necessarily imply that a new instance is allocated. That depends on the constructor being called. The constructor might be a factory that obtains objects from a cache, directly or indirectly.

The other form of instance creation expression is

const ConstructorName(args)

which is used to create constant objects. In this case, the arguments to the constructors must all be constant objects or a compile-time error occurs.

Apart from new and const, the only way to call a constructor is either from within a constructor or via reflection (7). Within a constructor, super constructors are called either implicitly or via the forms super(args) or super.id(args). In addition, redirecting generative constructors can call other constructors using the syntax this(args) or this.id(args). Redirecting factories can name the constructor they will redirect to. The details of constructors are given in Section 2.9 as noted above.

6.1.6 Assignment

Assignments can be performed on top-level, class, instance and local variables. For top-level, class and instance variables, assignment is a sugar for invoking a setter.

For local variables, assignment acts the same way as in traditional languages, setting the value of the variable on the right-hand side of the assignment to be the result of evaluating the expression on the left-hand side.

One cannot assign to a final variable.

Assignments in Dart include simple assignments of the form variable = expression and compound assignments where the assignment is combined with an operator such as +. An example would be variable += expression, which is just sugar for variable = variable + expression. In addition to +=, we have -=, *=, /=, ~=, %=, <<=, >>= &=, ^=, |=.

As an example, the following program prints the numbers 1, -1, 303 and 10.

library another_lib;

var topLevelVariable = 101;

class SomeClass {
 static var classVariable = 100;
}

library my lib;
import 'anotherLib.dart' as another show topLevelVariable, SomeClass;

var myTopLevelVariable = 0;

class MyClass {
 static var myClassVariable = 0;
}

main(){
 myTopLevelVariable += 1;
 MyClass.myClassVariable -= 1;
 another.topLevelVariable *= 3;
 another.SomeClass.classVariable /= 10;
 print(myTopLevelVariable);
 print(MyClass.myClassVariable);
 print(another.topLevelVariable);
 print(another.SomeClass.classVariable);
}

6.1.7 Extracting Properties

Occasionally it is convenient to use a method as an unevaluated function object. For example, we might want to register a method as a listener in an event loop. Assume that the function onClick() takes a listener function as an argument, eventually calling it back with an event object when a click event has occurred. Assume further that we have defined an instance method listener() to respond to click events.

listener(ev) => print(ev);

We can invoke onClick() as follows:

onClick(listener);

By the usual rules of the language, the above is a shorthand for

onClick(this.listener);

which in turn is (approximately) a shorthand for

onClick((event) => this.listener(event));

In general, the notation e.m denotes the method m of the object that e evaluates to. Strictly speaking, e.m is a closure with exactly the same parameters as m. The effect of calling this closure with a given set of arguments is the same as invoking the method m on the value of e with those arguments. If m is in scope, one can use the identifier m instead of this.m.

Property extraction is actually a bit more than just a shorthand for writing a closure. The closures manufactured have a special implementation of equality as described in Section 4.7.

One cannot extract a getter or setter method in this fashion. With respect to getters, the issue is that if g is a getter, e.g denotes the result of invoking the getter, and so that notation cannot be used to denote the getter method itself. As for setters, their true names end with = and are not proper identifiers, so e.s = is not syntactically valid. In all such cases, one has to write a closure explicitly.

6.1.8 Method Access

Method access has the form e.m(args), where e is an expression that produces an object, m is the name of a member, and args is a possibly empty list of arguments. Examples:

3.toString();
{'a':1, 'b': 2}.containsKey('c'),
'abcd'.indexOf('cd', 1);
myObject.foo(x, y, z);

It’s important to understand the order in which things are evaluated. In member access, the receiver is computed first, then the arguments to the method. The arguments are, as for all function invocations, computed in left-to-right order. Usually this won’t matter, but one can encounter situations where it makes a difference.

String x;
(x = 'xyz').indexOf(x); // evaluates to 0

If we did not compute the target expression (x = ‘xyz’) before computing the arguments, we would pass null as the argument. As a rule, if member access is sensitive to evaluation order, it is a sign of a problem in your code.

What if the method you are accessing does not exist? As you should well know by now, this will result in a call to noSuchMethod(). In fact, noSuchMethod() will get called if you call an existing method with the wrong number of arguments as well.

Another possible problem is that you are referring to a getter rather than a normal method. What actually happens in this case is that the expression e.m(args) is interpreted as (e.m)(args). First we perform a property access e.m, producing the result of calling the getter m. Next we invoke the call method on that result. If the result was a function object (or any other object that supports a suitable call method) then things will work as intended. Otherwise, we will get a noSuchMethod() again.

6.1.9 Using Operators

Operators in Dart are essentially instance methods with special syntax. A few of these are restricted so they may not be redefined in user code, but the majority are treated as ordinary instance methods that can be declared and/or overridden in any class.

The set of operators and their precedence is fixed. The following operators may be defined by user code: <, >, <=, >=, ==, -, +, /, ~/, %, ^, &, <<, >>, []=, [] and ~.

Most of these are binary operators. The exceptions are ~ which is unary, -, which comes in both unary and binary versions, and the indexed assignment operator, which is ternary. We have seen examples of user-defined operators before, starting with class Point. A similar example would be complex numbers:

class Complex {
 final num re, im;
 Complex(this.re, this.im);
 Complex.fromScalar(num s): this(s, 0);
 // nice example of redirecting constructor
 Complex operator + (Complex c) => new Complex(re + c.re, im + c.im);
 Complex operator * (Complex c) =>
 new Complex(re*c.re - im*c.im, re*c.im + im*c.re);
}

Operators are declared just like instance methods. The only difference is that the operator is written instead of an identifier as the method name, and the operator is prefaced with the keyword operator.

We’ve seen the indexing operators used on lists. We can now see that there is no special magic involved—any class can define indexing:

class Matrix {
 final List rows;
 Matrix.fromRows(this.rows); // assert all rows have same length
 Matrix.fromColumns(List cols);
 Matrix operator + (Matrix m) => new Matrix(pointwiseAdd(rows, m.rows));
 operator [](int index) => rows[index];
}

6.1.10 Throw

An expression of the form throw e throws an exception. The exception thrown is the result of evaluating the subexpression e. Unlike many other languages, throw is an expression, not a statement. This allows idioms such as:

unsupportedOperationFoo() => throw 'Foo is not supported, you imbecile!';

If throw were a statement, we could not use the shorthand syntax for functions and would have to write the more verbose:

unsupportedOperationFoo() {
  throw 'Foo is not supported, you imbecile!';
}

In general, expressions compose better than statements and it is advisable to define constructs as expressions rather than statements as often as possible.

As the examples show, any object can be thrown as an exception in Dart. There is no requirement that the object be an instance of a special exception class or a subclass thereof.

6.1.11 Conditionals

Conditional expressions have the form: b ? e1 : e2 where b is a Boolean expression, and e1 and e2 are arbitrary expressions. If b evaluates to true, the overall value of the expression is the value of e1; if b evaluates to false then the value of the conditional is the value of e2.

What if b is not a Boolean value? In checked mode, the code will fail dynamically, as usual. In production, any non-Boolean value is treated as false. If the static type of b is not bool or dynamic, the type checker will warn you.

A conditional expression is another context where the fact that throw is an expression can be handy.

asExpected(x) ? computeNormalResult(x) : throw 'Unexpected input $x';

The type of a conditional is the least upper bound of the types of its two branches.

6.2 Statements

Here we discuss all of Dart’s statements. They are for the most part what one would expect—the workhorses of programming such as if, for, while and so on. Technically, variable and function declarations are also statements, and almost any expression can be a statement as well.

6.2.1 Blocks

A block is a way of grouping several statements together. A block is delimited by curly braces in between which we may list statements. The list may be empty, like so {}. The statements within the block are separated by semicolons. Since blocks themselves are statements, blocks may nest.

The statements in a block are executed in sequence, left to right, top to bottom, in the order they appear in the source code.

Each block introduces its own scope. Declarations introduced within a block are only available inside the block. Within a block, you can refer to a declaration by its name, but only after the declaration. In other words, blocks do not allow mutually recursive declarations.

6.2.2 If

We’ve seen the if statement throughout the book. The reserved word if is followed by a parenthesized Boolean expression, followed by a statement, and optionally followed by an else clause consisting of the reserved word else and another statement. In both cases, the statement can be a simple statement:

if (x > 0) print(x);
if (y < x) print(y); else print(x);

or a block statement, which allows you to execute multiple statements in a single branch:

if (x > 0) { var y = 'in then branch'; print(x);};

Some people favor a style where one always uses block statements:

if (x > 0) {print(x);};
if (y < x) {print(y);} else {print(x);};

because one never needs to add/remove delimiters when adding statements to a branch or loop. Dart takes no position on the matter; it’s up to you.

Dart gently encourages you to use proper Booleans in all tests, including if statements. You are likely to get a type warning if the expression you use in the condition is not a Boolean one; but only if we’re really sure there’s a problem.

For example, the code below:

String primeBeef(x) => isPrime(x) ? 'DEAD BEEF' : null;
if (primeBeef(23)) {print('Prime'),} // warning

will generate a warning because strings are not Booleans. However, in the spirit of getting out of your way, in production mode Dart will not complain if your code is not typed, or if it is typed as Object:

primeBeef(x) => isPrime(x) ? 'DEAD BEEF' : null;
if (primeBeef(23)) {print('Prime'),} // no complaints about truly dynamically typed code

The above will always fail in checked mode, however.

6.2.3 Loops

Dart supports several kinds of loops: for loops in two forms as well as while and do loops.

6.2.3.1 For Loops

Dart supports both the classical for loop syntax and a higher-level for-in syntax.

The latter is less error prone because it eliminates the risk of the off-by-one errors so common in C and other low-level languages. The for-in statement lets you perform a statement for a series of objects provided by an Iterable. An example of an Iterable might be a list:

import 'dart:math' show pow;
main() {
 var v;
 for (v in [1, 2, 4, 8]) print(pow(2, v)); // prints 2, 4, 16, 256
}

Here we iterate over a list and compute two to the power of each element in the list. We use the function pow imported from dart:math to compute the power. Each pass through the loop binds v to another element provided by the list.

It is possible to declare the iteration variable in the for statement itself, with or without a type declaration:

for (int v in [1, 2, 4, 8]) print(pow(2, v)); // prints 2, 4, 16, 256
for (var v in [1, 2, 4, 8]) print(pow(2, v)); // prints 2, 4, 16, 256

Besides lists, there are other classes that implement the Iterable interface. Most important, anyone can implement Iterable making instances of their class usable with the for-in statement.

The classic for loop has four parts: an initializer, a test, a loop variable update and the loop body. The initializer typically declares an iteration variable, but this is not strictly required. One can reuse an existing variable, though this is bad style. In any event, the initializer sets the initial value of the iteration variable. Then the loop evaluates the test; if the result is true, execution continues to the loop body. If the test expression is empty, it is taken to be true so that:

for (; ; ) s;

defines an infinite loop that executes s over and over. You can see that it is not strictly necessary to have an initializer or to define an iteration variable. On each iteration, after the loop body the loop update is executed and the test is repeated.

All of the above is totally standard and will come as no surprise to any trained programmer. However, there is one unusual twist to Dart’s for loops.

Consider the following code, which allocates a list of 100 elements and sets each element to a closure. The closure is supposed to print the value of the iteration variable. After filling the list, we traverse it, list invoking each element in turn.

List<Function> funs = new List(100);
for (var index = 0; index < 100; index++) {
  funs[index] = () => print(index);
}
funs.forEach((f) => f());

In most languages the effect would be to print 99 a hundred times. Each of the closures would capture the same variable index, whose last value, set in the final iteration, was 99. This is perfectly logical, and yet many programmers are surprised by such behavior. In Dart, a fresh variable is allocated at each iteration and so the code above prints 0, 1, ..., 99.

6.2.3.2 While and Do

The most basic and most general of loops are while loops. A while loop evaluates a condition, and if it holds it executes the loop body and then repeats the process recursively; otherwise it terminates.

The do loop is a close cousin of while. It executes the loop body once, and then evaluates a condition. If the condition holds, it repeats itself recursively, otherwise it terminates.

The conditions evaluated by loops should yield a Boolean value. If the value is not Boolean, it is taken to be false.

6.2.4 Try-Catch

To catch exceptions, one uses the try statement. A try statement consists of several parts. The first part is a statement that might throw an exception. This is the statement that immediately follows the try keyword. After that we might have one or more catch clauses, a finally clause, or both. The catch clauses define handlers for specific categories of exceptions. The finally clause defines cleanup code that is intended to be executed no matter what happens—regardless of whether an exception has occurred or not.

try {
  if ( x >= 0) {
     doSomething(x);
     } else { throw 'negative x = $x';}
}
on String catch(e) {
  print(s);
 }
 catch(o) { print('Unknown exception $o'), }

As noted earlier, one can throw any object, and consequently catch clauses can catch any type of object as well. In the above example, we anticipate a string being thrown. It is often convenient to throw a string that describes the problem encountered. However, if the exception object is not a string, the second catch clause will come into play, reporting the error. The second clause does not specify what type of object it expects to catch, and so serves as a catch-all (no pun intended) for any exception that might arise. The latter form of catch clause has the useful property of being compatible with Javascript, easing the task of converting Javascript code into Dart.

The above behavior is quite standard. What makes try-catch different in Dart is how stack traces are handled. Catch clauses optionally accept a second parameter representing the stack trace. Only code that requires access to the trace should specify the second parameter. If the parameter is not specified, the implementation may be able to avoid filling in the stack trace. Filling in a stack trace is an expensive operation, and avoiding it is an important optimization.

try {
  shootFloor();
}
on PodiatryException catch(e, s) {
  print(s);
  print('Shot myself in the foot: $e'),
}

The stack trace object does not support any methods beyond those defined in Object. The most important among these is toString(), which prints out the stack trace.

6.2.5 Rethrow

It is not uncommon to catch an exception, and after examination decide that there is nothing to be done locally, and the exception should be propagated further up the call chain.

The rethrow statement deals with this exact situation by rethrowing an exception that is being processed in a catch clause.

6.2.6 Switch

The switch statement provides a way of choosing a course of action among a number of options based on the value of an expression. Each case handled by a switch corresponds to one possible value of the expression. The possible values must be known in advance—they have to be compile-time constants. We also insist that various cases be of the exact same type.

The best uses of switch are in applications like lexers or interpreters. Let’s look at an absolutely trivial interpreter. The interpreter works on a very simple instruction set:

constant n
plus
minus

The instruction set assumes an implicit operand stack. The instruction constant n pushes the numeric constant n onto the stack; the plus instruction pops the two top elements of the operand stack, sums them and pushes the result back on the stack; minus is similar except that it subtracts rather than adds.

We can represent these instructions in various ways. Here is a very straightforward one:

class Instruction {
  String code;
  Instruction(this.code);
}

class LiteralInstruction extends Instruction {
  num constantValue;
  LiteralInstruction(this.constantValue): super('constant'),
}

Now assuming that we have an iterable sequence of instructions instructions we can execute it using the following code:

List<num> operandStack = new List<num>();
for (Instruction instruction in instructions) {
  switch (instruction.code) {
    case 'plus':
         operandStack.add(operandStack.removeLast() + operandStack.removeLast());
       break;
    case 'minus':
          operandStack.add(operandStack.removeLast() - operandStack.removeLast());
       break;
    case 'constant':
       LiteralInstruction litInst = instruction;
       operandStack.add(litInst.constantValue);
       break;
    }
  }
assert(operandStack.length == 1);
num result = operandStack.last;

We iterate over the instruction stream. On each iteration, the switch statement uses the op-code to direct processing as appropriate. Operands of the instructions are stored on a stack, operandStack. The stack is initially empty. A well-formed instruction stream should leave exactly one element on top of the stack when execution completes. That element is the result of evaluation.

While the above example is trivial and naive, it captures the essence of an interpreter. We can encode the instruction stream more efficiently (typically the instruction stream will be made up of integers, most likely bytes, rather than objects), and we are likely to have many more instructions, but the basic structure won’t change much.

The workings of the switch statement in Dart are close to the traditional behavior in C and its successors, but not identical. In most languages, execution of a case “falls-through” to the next case. This decision, driven by implementation expediency decades ago, has been the source of countless bugs, in particular dangerous security bugs. In Dart, such falling-through is not allowed. If fall-through occurs, it will cause an immediate run time error, preventing hidden bugs. For this reason, each case in our example ends with a break statement, preventing fall-through.

One might naturally define the semantics such that execution simply terminates at the end of each case. This would be cleaner and simpler. However, we also want to make it easy to port code from other languages to Dart. The rules of Dart ensure that correctly written switch statements can be ported to Dart and are very likely to work as intended. Code that relies on fall-through will fail; such code is uncommon and error prone.

One can add a special default case to a switch statement. This case will be executed if none of the other cases match. We’ll see an example below.

The switch statement has a number of requirements. The various cases must all be compile-time constants (2.11, 6.1.4). This allows for efficient compilation. Furthermore, the constants must either:

• All be instances of int, or

• All be instances of String, or

• All be instances of the exact same class and that class must inherit the implementation of == from Object.

If any of these requirements are violated, a compilation error will be flagged.

There is some subtlety to the above rules. Note that the requirement that all cases be of the same class is not imposed on integers. Dart implementations may use different classes to implement integers of widely varying size, yet the following is still allowed:

switch (guest.bankBalance) {
   case 0:
           print('Never darken my doorway again'),
           break;
   case 10000000000:
           print('Welcome; su casa es mi casa'),
           break;
   default: print('Hello'),
   }
}

We only care that all the cases are of type int. Similar reasoning holds for type String, where again an implementation may use several classes depending on the nature of the strings used.

The ban on a user-defined equality method means that the implementation can quickly test whether an object matches a case using object identity (the default implementation of equality). This requirement is ignored for strings and integers; these have their own implementations of equality but it would not do to disallow the use of these types in switch statements.

6.2.7 Assert

Programmers live in a perpetual state of disappointment; their expectations are forever being dashed. Consider the humble factorial function:

factorial(int x) {
  return x == 0 ? 1 : x* factorial(x-1);
}

What could possibly go wrong with such simple code? In fact, sadistic software engineers are fond of asking this question in interviews. It is easy to be convinced of the correctness of the code above. It is also easy to evaluate factorial(-1); and see that the function will recurse until it runs out of stack space.

To guard against such heartbreak, it is good practice to make one’s expectations explicit by generously sprinkling assertions in one’s code. One makes such assertions by means of the assert statement.

Typically, one writes as assertion by enclosing a Boolean expression in an assert:

factorial(int x) {
 assert(x >= 0);
  return x == 0 ? 1 : x* factorial(x-1);
}

The above is very similar to:

factorial(int x) {
 if (x >= 0) return x == 0 ? 1 : x* factorial(x-1);
 else throw new AssertionError();
}

If the condition does not hold, an AssertionError is thrown. The advantage of the assert construct is that in production it is disabled, adding no overhead in space or time. It only has an effect in checked mode.

Of course, this might not be the best way to write factorial ; once we are clear about our assumptions, we probably want

factorial(int x) {
 if (x >= 0) return x == 0 ? 1 : x* factorial(x-1);
 else return 1;
}

or better yet:

factorial(int x) {
 if (x > 1) return x* factorial(x-1);
 else return 1;
}

The point is that one should think carefully about the assumptions one is making and then decide whether to validate them, and what to do if they do not hold.

It is also possible to provide a predicate inside the assert. In that case, the function will be called when the assert statement is executed. If it yields true, the assertion succeeds and all is well. Otherwise the assertion fails. This means that the assertion fails if the function returns false or if it returns any non-Boolean value.

The Dart type checker will complain if the expression within the assert is not one of the following types: bool, ()bool, or a type that is assignable to one of those. The only types assignable to bool are Object and dynamic. In the case of ()bool the possibilities are a bit more varied. Here are some assertions that won’t cause warnings:

var x, y;
... f() => false;
g() => x > y;
assert(f); // Fine, though will always fail in checked mode
assert(g); // Fine
assert(() => 'error!!'), // closures always have static return type dynamic
assert(([a, b]) => a > b ); // optional arguments are compatible

and here are some that will give rise to warnings:

String f() => 'true';
assert(f); // String return type incompatible with bool
assert((a, b) => a > b ); // incompatible arity

As noted in Chapter 5, checked mode treats type annotations as assertions. Every checked mode assignment of the form T v = e is treated like the following production mode code:

T v = () { var v= e; assert(null == v′ || vis T ); return v;}();

Similar logic applies to parameter passing and to returning values from a function.

6.2.8 Return

The return statement transfers control from a function to its caller. There are two variants of return—one with an associated expression and one without:

return 42;
return;

The latter is essentially a shorthand for return null.

Executing a return statement will transfer control to the caller of the function immediately enclosing the return, at the point immediately following the call. The result of the current function is the value of the expression following the return (or null, if there is no expression). However, if the return is enclosed in a try-finally statement, control will transfer to the finally clause.

The Dart type checker will admonish us if the static type of the expression associated with the return cannot be assigned to the return type of the enclosing function. Furthermore, in checked mode, if the type of the value being returned is not a subtype of the declared return type, Dart will stop in its tracks (as always, null is ok).

The type checker will also warn if we write a return without an expression unless the surrounding function is declared to return type dynamic or void. So while the following is fine:

void postNotice(String n) {
   print(n);
   return;
}

as is:

postNotice(String n) {
   print(n);
   return;
}

this is not:

String postNotice(String n) {
   print(n);
   return;
}

In the last variant, we may have intended to return n to the sender, perhaps as part of a fluent API, which is why we declared the function to return String. Or we really never meant to return a string at all. In either case, something is not right, and Dart will issue a warning.

Return statements are treated specially in (generative) constructors. A return followed by an expression is disallowed in a generative constructor:

class Point {
  var x, y;
  Point(a, b) { return 0; } // ILLEGAL Dart:: compilation error!
}

Obviously, a constructor like Point() is supposed to return a new instance of Point and nothing else. That instance will be manufactured by the runtime and returned implicitly, so returning any other value makes no sense whatsoever.

In a generative constructor, we do allow a return without an accompanying expression, but we interpret it to mean return this rather than return null. In theory, we could also tolerate an explicit return this but no purpose is served by such a dispensation.

6.2.9 Yield and Yield-Each

Yield statements are used inside generator functions (4.8) to add new results to the collection being generated. A yield statement has the form yield e;.

A yield always causes its expression to be evaluated. Typically, the resulting value will be appended to the collection associated with the enclosing generator. If the generator is synchronous (4.8.2) then the associated collection is an iterable (4.8.1); if it is asynchronous (8.6.2), the associated collection is a stream (8.3).

Beyond this point the behavior of yield differs depending on whether the enclosing generator is synchronous or not. In the synchronous case, yield suspends the enclosing generator, and the call to moveNext() that caused the generator to execute will return to its caller with a value of true.

In the asynchronous case, execution of the generator continues.

Yield statements are only allowed inside generators.

The word yield is not a reserved word in Dart, nor even a built-in identifier. However, it is treated as a keyword inside a generator.

Sometimes, it is desirable to define a generator recursively.

naturalsDownFrom(n) sync* {
 if ( n > 0) {
   yield n;
   for (var i in naturalsDownFrom(n-1)) {
     yield i;
   }
 }
}

The code above is functionally correct, but a nasty performance bug lurks within: it runs in quadratic time. To see why, consider the following trace of naturalsDownFrom(3):

naturalsDownFrom(3)

 if ( 3 > 0) // true
   yield 3;
   for (var i in naturalsDownFrom(2))
   →
      if ( 2 > 0)
         yield 2;
         for (var i in naturalsDownFrom(1))
          →
          if ( 1 > 0)
            yield 1;
            for (var i in naturalsDownFrom(0))
            →
              if ( 0 > 0) // false
            ← // returns the empty sequence 1
        ← // the sequence 1
        yield 1;
   ← // the sequence 2, 1
   yield 2;
   yield 1;
← // the sequence 3, 2, 1

Notice that yield i; is executed n − 1 times for the nth element in the sequence: once at each level of recursion; the first element, 3, is only yielded by the yield n; statement; the second element, 2, is yielded once by yield n; and once by yield i;. The third element is 1, which is yielded once by yield n; and twice by yield i;.

Altogether we have n(n − 1) executions of yield i;, which is O(n2).

Dart provides the yield-each statement to address this problem. The yield-each statement has the form:

yield* e;

The expression e must evaluate to a collection, or a runtime error will be raised; yield* appends all the elements of e to the collection associated with the enclosing generator.

We can rewrite our code using yield-each as follows:

naturalsDownFrom(n) sync* {
  if ( n > 0) {
    yield n;
    yield* naturalsDownFrom(n-1));

  }
}

The latter version runs in linear time.

6.2.10 Labels

Dart allows labels in code. The intent of labels is to facilitate automatic code generation by tools. We mention them here for the sake of completeness. However, you should never use labels in code you write. They are a low-level mechanism that should never appear in code that is written, or intended to be read, by humans.

6.2.11 Break and Continue

The statements break and continue support what might be called semi-structured control flow. They are a legacy of earlier languages in the C tradition. Typically they are useful when one is deep inside a control structure and needs to escape from the entire flow.

As an example, suppose we are processing messages in a loop. We keep processing messages until we get the message #stop. One way to handle this is to use an infinite loop and use break to escape from it.

var msg;
while (true) {
  msg = getMessage();
  if (msg == #stop) break; else processMsg(msg);
}

Another approach to the same problem is to test for the #stop message at the top of the loop:

var msg = getMessage();
while (msg != #stop) {
 processMsg(msg);
 msg = getMessage();
}

but this is perhaps less natural. However, one might do better to make the above a separate routine and use return to escape instead:

processMessages() {
 var msg;
 while (true) {
   msg = getMessage();
   if (msg == #stop) return; else processMsg(msg);
 }
}

Small methods are good programming style. A method has a name that helps documents the code’s purpose, and moreover short methods are easier to read and comprehend than long ones with deeply nested control structures.

As a rule a break statement will escape from the nearest enclosing loop (be it a while, do or for) or switch statement. Any intervening finally clauses will be executed. If one writes a break statement that is not enclosed in a loop or switch, a compile-time error occurs.

One can also associate a break statement with a label. In that case, there must be an enclosing statement with the same label, and control will escape to that statement. Again, any intervening finally clauses will be executed. As noted in Section 6.2.10 above, labels (and break or continue statements that target them) should not be used in code written or read by humans. These constructs are there strictly to facilitate compilation into Dart.

The continue statement is very similar to break but is restricted to loops. Its effect is not to escape the loop, but to immediately continue to the next iteration.

6.3 Summary

Dart provides a fairly conventional spectrum of expressions and statements. The primary goal is to remain familiar to mainstream programmers. A few details vary, such as:

• The handling of Boolean items in conditionals and loops, motivated by Dart’s aversion to runtime coercions.

• The rules for switch.

• The treatment of for loop variables, in order to prevent common errors.

• The handling of stack traces in catch clauses, driven by a desire to support more efficient exception handling.

• Allowing user-defined objects as constants.

• Operators, which are mostly treated as instance methods, as in, say, Smalltalk or Scala. Unlike those languages, the set of operators is restricted to a fixed set.

• Support for asynchrony.

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

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