Chapter 2. A Tour of the Dart Language
This chapter shows you how to use each major Dart feature, from
variables and operators to classes and libraries, with the assumption that
you already know how to program in another language.
Consult the Dart
Language Specification whenever you want more details about a
language feature.
The following code uses many of Dart’s most basic features:
// Define a function.
printNumber(num aNumber) {
print('The number is $aNumber.'); // Print to the console.
}
// This is where the app starts executing.
main() {
var number = 42; // Declare and initialize a variable.
printNumber(number); // Call a function.
}Here’s what this program uses that applies to all (or almost all)
Dart apps:
// This is a
comment.Use // to indicate that the rest of the line is a comment.
Alternatively, use /* ... */. For details, see the section called “Comments”.
numA type. Some of the other built-in types are String, int, and
bool.
42A number literal. Literals are a kind
of compile-time constant.
print()A handy way to display output.
'...' (or "...")A string literal.
$variableName (or
${expression})String interpolation: including a variable or expression’s
string equivalent inside of a string literal. For more information,
see the section called “Strings”.
main()The special, required, top-level function
where app execution starts.
varA way to declare a variable without specifying its
type.
Note
Our code follows the conventions in the Dart Style
Guide. For example, we use two-space indentation.
As you learn about the Dart language, keep these facts and concepts
in mind:
Everything you can place in a variable is an
object, and every object is an instance of a
class. Even numbers, functions, and null are objects. All objects inherit from
the Object
class.
Specifying static types (such as num in the preceding example) clarifies your
intent and enables static checking by tools, but it’s optional. (You
might notice when you’re debugging your code that objects with no
specified type get a special type: dynamic.)
Dart parses all your code before running it. You can provide
tips to Dart—for example, by using types or compile-time constants—to
catch errors or help your code run faster.
Dart supports top-level functions (such as
main()), as well as functions tied to a class or
object (static and instance
methods, respectively). You can also create functions
within functions (nested or local
functions).
Similarly, Dart supports top-level
variables, as well as variables tied to a class
or object (static and instance variables). Instance variables are
sometimes known as fields or
properties.
Unlike Java, Dart doesn’t have the keywords public, protected, and private. If an identifier starts with an
underscore (_), it’s private to its library. For details, see the section called “Libraries and Visibility”.
Identifiers can start with a letter or _,
followed by any combination of those characters plus digits.
Sometimes it matters whether something is an
expression or a statement,
so we’ll be precise about those two words.
Dart tools can report two kinds of errors: warnings and errors.
Warnings are just hints that your code might not work, but they don’t
prevent your program from executing. Errors can be either compile-time
or run-time. A compile-time error prevents the code from executing at
all; a run-time error results in an exception being raised while the code
executes.
Dart has two runtime modes: production
and checked. Production is faster, but checked is helpful at
development.
Table 2.1, “Dart keywords” lists the words that the Dart
language reserves.
Table 2.1. Dart keywords
| abstract | continue | factory | import | return | try |
| as | default | false | in | set | typedef |
| assert | do | final | is | static | var |
| break | dynamic | finally | library | super | void |
| case | else | for | new | switch | while |
| catch | export | get | null | this | with |
| class | external | if | operator | throw | |
| const | extends | implements | part | true | |
We recommend that you develop and debug in checked mode, and
deploy to production mode.
Production mode is the default runtime mode
of a Dart program, optimized for speed. Production mode ignores assert statements and static types.
Checked mode is a developer-friendly mode
that helps you catch some type errors during runtime. For example, if
you assign a non-number to a variable declared as a num, then checked mode throws an
exception.
Here’s an example of creating a variable and assigning a value to
it:
var name = 'Bob';
Variables are references. The variable called name contains a reference to a String object
with a value of “Bob”.
Uninitialized variables have an initial value of null. Even variables with numeric types are
initially null, because numbers are objects.
int lineCount;
assert(lineCount == null);
// Variables (even if they will be numbers) are initially null.
Note
The assert() call is ignored in production
mode. In checked mode, assert(condition)
throws an exception unless condition is
true. For details, see the section called “Assert”.
You have the option of adding static types to your variable
declarations:
String name = 'Bob';
Adding types is a way to clearly express your intent. Tools such
as compilers and editors can use these types to help you, by providing
early warnings for bugs and code completion.
If you never intend to change a variable, use final or const, either instead of var or in addition to a type. A final variable
can be set only once; a const variable is a compile-time
constant.
A local, top-level, or class variable that’s declared as final is initialized the first time it’s
used:
final name = 'Bob'; // Or: final String name = 'Bob';
// name = 'Alice'; // Uncommenting this results in an error
Note
Lazy initialization of final variables helps apps start up
faster.
Use const for variables that
you want to be compile-time constants. Where you declare the variable,
set the value to a compile-time constant such as a literal, a const
variable, or the result of an arithmetic operation on constant
numbers:
const bar = 1000000; // Unit of pressure (in dynes/cm2)
const atm = 1.01325 * bar; // Standard atmosphere
The Dart language has special support for the following
types:
You can initialize an object of any of these special types using a
literal. For example, 'this is a
string' is a string literal, and true is a boolean literal.
Because every variable in Dart refers to an object—an instance of a
class—you can usually use
constructors to initialize variables. Some of the
built-in types have their own constructors. For example, you can use the
Map() constructor to create a map, using code such as
new Map().
Dart numbers come in two flavors:
-
int Integers of arbitrary size
-
double 64-bit (double-precision) floating-point numbers, as
specified by the IEEE 754 standard
Both int and double are subtypes of num. The num type includes basic
operators such as +, -, /, and *, as well as bitwise operators such as
>>. The num type is also where you’ll find
abs(), ceil(), and
floor(), among other methods. If num and its subtypes
don’t have what you’re looking for, the Math library might.
(In JavaScript produced from Dart code, big integers currently behave
differently than they do when the same Dart code runs in the
Dart VM.)
Integers are numbers without a decimal point. Here are some
examples of defining integer literals:
var x = 1;
var hex = 0xDEADBEEF;
var bigInt = 346534658346524376592384765923749587398457294759347029438709349347;
If a number includes a decimal, it is a double. Here are some
examples of defining double literals:
var y = 1.1;
var exponents = 1.42e5;
Here’s how you turn a string into a number, or vice versa:
// String -> int
var one = int.parse('1');
assert(one == 1);
// String -> double
var onePointOne = double.parse('1.1');
assert(onePointOne == 1.1);
// int -> String
String oneAsString = 1.toString();
assert(oneAsString == '1');
// double -> String
String piAsString = 3.14159.toStringAsFixed(2);
assert(piAsString == '3.14');The int type specifies the traditional bitwise shift (<<,
>>), AND (&), and OR (|) operators. For example:
assert((3 << 1) == 6); // 0011 << 1 == 0110
assert((3 >> 1) == 1); // 0011 >> 1 == 0001
assert((3 | 4) == 7); // 0011 | 0100 == 0111
A Dart string is a sequence of UTF-16 code units. You can use
either single or double quotes to create a string:
var s1 = 'Single quotes work well for string literals.';
var s2 = "Double quotes work just as well.";
var s3 = 'It\'s easy to escape the string delimiter.';
var s4 = "It's even easier to just use the other string delimiter.";
You can put the value of an expression inside a string by using
${expression}.
If the expression is an identifier, you can skip the {}. To get the
string corresponding to an object, Dart calls the object’s
toString() method.
var s = 'string interpolation';
assert('Dart has $s, which is very handy.' ==
'Dart has string interpolation, which is very handy.');
assert('That deserves all caps. ${s.toUpperCase()} is very handy!' ==
'That deserves all caps. STRING INTERPOLATION is very handy!');Note
The == operator tests whether
two objects are equivalent. Two strings are equivalent if they have
the same characters.
You can concatenate strings using adjacent string literals:
var s = 'String ' 'concatenation'
" works even over line breaks.";
assert(s == 'String concatenation works even over line breaks.');Another way to create a multi-line string: use a triple quote with
either single or double quotation marks:
var s1 = '''
You can create
multi-line strings like this one.
''';
var s2 = """This is also a
multi-line string.""";
You can create a “raw” string by prefixing it with
r:
var s = r"In a raw string, even \n isn't special.";
You can use Unicode escapes inside of strings:
print('Unicode escapes work: \u2665'); // Unicode escapes work: [heart]For more information on using strings, see the section called “Strings and Regular Expressions”.
To represent boolean values, Dart has a type named bool. Only two objects have type bool: the
boolean literals, true and false.
When Dart expects a boolean value, only the value true is treated as true. All other values are
treated as false. Unlike in JavaScript, values such as 1, "aString", and someObject are all treated as false.
For example, consider the following code, which is valid both as
JavaScript and as Dart code:
var name = 'Bob';
if (name) {
print('You have a name!'); // Prints in JavaScript, not in Dart.
}If you run this code as JavaScript, it prints “You have a
name!” because name is a
non-null object. However, in Dart running in production
mode, the above doesn’t print at all because name is converted to false (because name
!= true). In Dart running in checked
mode, the above code throws an exception because the name variable is not a bool.
Here’s another example of code that behaves differently in
JavaScript and Dart:
if (1) {
print('JavaScript prints this line because it thinks 1 is true.');
} else {
print('Dart in production mode prints this line.');
// However, in checked mode, if (1) throws an exception.
}Note
The previous two samples work only in production mode, not
checked mode. In checked mode, an exception is thrown if a non-boolean
is used when a boolean value is expected.
Dart’s treatment of booleans is designed to avoid the strange
behaviors that can arise when many values can be treated as true. What
this means for you is that, instead of using code like if (nonbooleanValue),
you should instead explicitly check for values. For example:
// Check for an empty string.
var fullName = '';
assert(fullName.isEmpty);
// Check for zero.
var hitPoints = 0;
assert(hitPoints <= 0);
// Check for null.
var unicorn;
assert(unicorn == null);
// Check for NaN.
var iMeantToDoThis = 0/0;
assert(iMeantToDoThis.isNaN);
Perhaps the most common collection in nearly every programming
language is the array, or ordered group of objects.
In Dart, arrays are List objects,
so we usually just call them lists.
Dart list literals look like JavaScript array literals. Here’s a
simple Dart list:
var list = [1,2,3];
Lists use zero-based indexing, where 0 is the index of the first
element and list.length - 1 is the
index of the last element. You can get a list’s length and refer to list
elements just as you would in JavaScript:
var list = [1,2,3];
assert(list.length == 3);
assert(list[1] == 2);
The List type has many handy methods for manipulating lists.
For more information
about lists, see the section called “Generics” and the section called “Collections”.
In general, a map is an object that associates keys and values.
Dart support for maps is provided by map literals and the Map
type.
Here’s a simple Dart map:
var gifts = { // A map literal
// Keys Values
'first' : 'partridge',
'second' : 'turtledoves',
'fifth' : 'golden rings'
};In map literals, each key must be a string.
If you use a Map constructor, any object can be a key.
var map = new Map(); // Use a map constructor.
map[1] = 'partridge'; // Key is 1; value is 'partridge'.
map[2] = 'turtledoves'; // Key is 2; value is 'turtledoves'.
map[5] = 'golden rings'; // Key is 5; value is 'golden rings'.
A map value can be any object, including
null.
You add a new key-value pair to an existing map just as you would
in JavaScript:
var gifts = { 'first': 'partridge' };
gifts['fourth'] = 'calling birds'; // Add a key-value pairYou retrieve a value from a map the same way you would in
JavaScript:
var gifts = { 'first': 'partridge' };
assert(gifts['first'] == 'partridge');If you look for a key that isn’t in a map, you get a null in
return:
var gifts = { 'first': 'partridge' };
assert(gifts['fifth'] == null);Use .length to get the number
of key-value pairs in the map:
var gifts = { 'first': 'partridge' };
gifts['fourth'] = 'calling birds';
assert(gifts.length == 2);For more information about maps, see the section called “Generics”
and the section called “Maps”.
Here’s an example of implementing a function:
void printNumber(num number) {
print('The number is $number.');
}Although the style guide recommends specifying the parameter and
return types, you don’t have to:
printNumber(number) { // Omitting types is OK.
print('The number is $number.');
}For functions that contain just one expression, you can use a
shorthand syntax:
printNumber(number) => print('The number is $number.');The =>
expr; syntax is a shorthand for
{ return
expr;}. In the
printNumber() function above, the expression is the
call to the top-level print() function.
You can use types with =>, although the convention is not to do
so:
printNumber(num number) => print('The number is $number.'); // Types are OK.Here’s an example of calling a function:
printNumber(123);
A function can have two types of parameters: required and optional.
The required parameters are listed first, followed by any optional
parameters.
Optional parameters can be either positional or named, but not
both.
Both kinds of optional parameter can have default values. The
default values must be compile-time constants such as literals. If no
default value is provided, the value is null.
If you need to know whether the caller passed in a value for an
optional parameter, use the syntax ?param:
if (?device) { // Returns true if the caller specified the parameter.
// ...The user set the value. Do something with it...
}Optional named parameters
When calling a function, you can specify named parameters using
paramName:
value. For example:
enableFlags(bold: true, hidden: false);
When defining a function, use {param1,
param2, …} to specify named
parameters:
/// Sets the [bold] and [hidden] flags to the values you specify.
enableFlags({bool bold, bool hidden}) {
// ...
}Use a colon (:) to specify
default values:
/**
* Sets the [bold] and [hidden] flags to the values you specify,
* defaulting to false.
*/
enableFlags({bool bold: false, bool hidden: false}) {
// ...
}
enableFlags(bold: true); // bold will be true; hidden will be false.Optional positional parameters
Wrapping a set of function parameters in [] marks them as optional positional parameters:
String say(String from, String msg, [String device]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
return result;
}Here’s an example of calling this function without the optional
parameter:
assert(say('Bob', 'Howdy') == 'Bob says Howdy');And here’s an example of calling this function with the third
parameter:
assert(say('Bob', 'Howdy', 'smoke signal') ==
'Bob says Howdy with a smoke signal');Use = to specify default
values:
String say(String from, String msg,
[String device='carrier pigeon', String mood]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
if (mood != null) {
result = '$result (in a $mood mood)';
}
return result;
}
assert(say('Bob', 'Howdy') == 'Bob says Howdy with a carrier pigeon');Functions as First-Class Objects
You can pass a function as a parameter to another function. For
example:
printElement(element) {
print(element);
}
var list = [1,2,3];
list.forEach(printElement); // Pass printElement as a parameter.You can also assign a function to a variable, such as:
var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';
assert(loudify('hello') == '!!! HELLO !!!');Dart is a lexically scoped language, which means that the scope of
variables is determined statically, simply by the layout of the code.
You can “follow the curly braces outwards” to see if a variable is in
scope.
Here is an example of nested functions with variables at each
scope level:
var topLevel = true;
main() {
var insideMain = true;
myFunction() {
var insideFunction = true;
nestedFunction() {
var insideNestedFunction = true;
assert(topLevel);
assert(insideMain);
assert(insideFunction);
assert(insideNestedFunction);
}
}
}Notice how nestedFunction() can use variables
from every level, all the way up to the top level.
A closure is a function object that has
access to variables in its lexical scope, even when the function is used
outside of its original scope.
Functions can close over variables defined in surrounding scopes.
In the following example, adder() captures the
variable addBy. Wherever the returned
function goes, it remembers addBy.
/// Returns a function that adds [addBy] to a number.
Function makeAdder(num addBy) {
adder(num i) {
return addBy + i;
}
return adder;
}
main() {
var add2 = makeAdder(2); // Create a function that adds 2.
var add4 = makeAdder(4); // Create a function that adds 4.
assert(add2(3) == 5);
assert(add4(3) == 7);
}Each time you create a closure, that closure is a new object. This
can cause problems when you want to test whether two functions are
equivalent. For example, consider this code:
var s = 'some string'; // Create a String object.
var splitClosure1 = s.split; // Get a reference to its split() method.
var splitClosure2 = s.split; // Get another reference to its split() method.
// Because each reference to the method creates a separate closure,
// s.split != s.split, and splitClosure1 != splitClosure2.
assert(s.split != s.split);
assert(splitClosure1 != splitClosure2);
The key to comparing instance methods is to save a reference to
the closure:
splitClosure2 = splitClosure1;
assert(splitClosure1 == splitClosure2);
Top-level and static methods are different—you can compare them by
name:
foo() {}
class SomeClass {
static void bar() {}
}
main() {
assert(foo == foo);
assert(SomeClass.bar == SomeClass.bar);
}All functions return a value. If no return value is specified, the
statement return null; is implicitly
appended to the function body.
Dart defines the operators shown in Table 2.2, “Operators and their precedence”. You can override many of these operators, as
described in the section called “Overridable Operators”.
Table 2.2. Operators and their precedence
| Description | Operator |
|---|
| unary postfix and argument definition test | expr++
expr-- ()
[] .
?identifier |
| unary prefix | -expr
!expr
~expr
++expr
--expr |
| multiplicative | * / %
~/ |
| additive | + - |
| shift | << >> |
| relational and type test | >= >
<= < as is
is! |
| equality | == !=
|
| bitwise AND | & |
| bitwise XOR | ^ |
| bitwise OR | | |
| logical AND | && |
| logical OR | || |
| conditional | expr1 ?
expr2 :
expr3 |
| cascade | .. |
| assignment | = *= /=
~/= %= += -=
<<= >>= &=
^= |= |
When you use operators, you create
expressions. Here are some examples of operator
expressions:
a++
a + b
a = b
a == b
a? b: c
a is T
In Table 2.2, “Operators and their precedence”, each operator has higher
precedence than the operators in the rows below it. For example, the
multiplicative operator % has higher
precedence than (and thus executes before) the equality operator ==, which has higher precedence than the logical
AND operator &&. That
precedence means that the following two lines of code execute the same
way:
if ((n % i == 0) && (d % i == 0)) // Parens improve readability.
if (n % i == 0 && d % i == 0) // Harder to read, but equivalent.
Warning
For operators that work on two operands, the leftmost operand
determines which version of the operator is used. For example, if you
have a Vector object and a Point object, aVector + aPoint uses the Vector version of
+.
Dart supports the usual arithmetic operators, as shown in Table 2.3, “Arithmetic operators”.
Table 2.3. Arithmetic operators
| Operator | Meaning |
|---|
| + | Add |
| – | Subtract |
-expr | Unary minus, also known as negation (reverse the sign of
the expression) |
| * | Multiply |
| / | Divide |
| ~/ | Divide, returning an integer result |
| % | Get the remainder of an integer division (modulo) |
Example:
assert(2 + 3 == 5);
assert(2 - 3 == -1);
assert(2 * 3 == 6);
assert(5 / 2 == 2.5); // Result is a double
assert(5 ~/ 2 == 2); // Result is an integer
assert(5 % 2 == 1); // Remainder
print('5/2 = ${5~/2} remainder ${5%2}'); // 5/2 = 2 remainder 1Dart also supports both prefix and postfix increment and decrement
operators.
Table 2.4. Increment and decrement operators
| Operator | Meaning |
|---|
++var | var =
var + 1 (expression value is
var + 1) |
var++ | var =
var + 1 (expression value is
var) |
--var | var =
var – 1 (expression value is
var – 1) |
var-- | var =
var – 1 (expression value is
var) |
Example:
var a, b;
a = 0;
b = ++a; // Increment a before b gets its value.
assert(a == b); // 1 == 1
a = 0;
b = a++; // Increment a AFTER b gets its value.
assert(a != b); // 1 != 0
a = 0;
b = --a; // Decrement a before b gets its value.
assert(a == b); // -1 == -1
a = 0;
b = a--; // Decrement a AFTER b gets its value.
assert(a != b) ; // -1 != 0
Equality and Relational Operators
Table 2.5. Equality and relational operators
| Operator | Meaning |
|---|
| == | Equal; see discussion below |
| != | Not equal |
| > | Greater than |
| < | Less than |
| >= | Greater than or equal to |
| <= | Less than or equal to |
To test whether two objects x and
y represent the same thing, use the == operator.
(In the rare case
where you need to know whether two objects are the exact same object,
use the identical()
function instead.)
Here’s how the == operator works:
If x or
y is null, return true if both are null,
and false if only one is null.
Return the result of the method invocation x.==(y).
(That’s right, operators such as == are methods that are invoked on their
first operand. You can even override many operators, including
==, as you’ll see in the section called “Overridable Operators”.)
Here’s an example of using each of the equality and relational
operators:
assert(2 == 2);
assert(2 != 3);
assert(3 > 2);
assert(2 < 3);
assert(3 >= 3);
assert(2 <= 3);
The as, is, and is!
operators are handy for checking types at runtime.
Table 2.6. Type test operators
| Operator | Meaning |
|---|
| as | Typecast |
| is | True if the object has the specified type |
| is! | False if the object has the specified type |
The result of obj is T is true
if obj implements the interface
specified by T. For example, obj is Object is always true.
Use the as operator to cast an
object to a particular type. In general, you should use it as a
shorthand for an is test on an object
following by an expression using that object. For example, consider the
following code:
if (person is Person) { // Type check
person.firstName = 'Bob';
}You can make the code shorter using the as operator:
(person as Person).firstName = 'Bob';
Note
The code isn’t equivalent. If person is null
or not a Person, the first example (with is) does
nothing; the second (with as) throws an exception.
As you’ve already seen, you assign values using the = operator. You can also use compound
assignment operators such as +=,
which combine an operation with an assignment.
Table 2.7. Assignment operators
| = | –= | /= | %= | >>= | ^= |
| += | *= | ~/= | <<= | &= | |= |
Here’s how compound assignment operators work:
The following example uses both assignment and compound assignment
operators:
var a = 2; // Assign using =
a *= 3; // Assign and multiply: a = a * 3
assert(a == 6);
You can invert or combine boolean expressions using the logical
operators.
Table 2.8. Logical operators
| Operator | Meaning |
|---|
!expr | inverts the following expression (changes false to true,
and vice versa) |
| || | logical OR |
| && | logical AND |
Here’s an example of using the logical operators:
if (!done && (col == 0 || col == 3)) {
// ...Do something...
}Bitwise and Shift Operators
You can manipulate the individual bits of numbers in Dart.
Usually, you’d use these bitwise and shift operators with integers, as
shown in Table 2.9, “Bitwise and shift operators”.
Table 2.9. Bitwise and shift operators
| Operator | Meaning |
|---|
| & | AND |
| | | OR |
| ^ | XOR |
~expr | Unary bitwise complement (0s become 1s; 1s become
0s) |
| << | Shift left |
| >> | Shift right |
Here’s an example of using bitwise and shift operators:
final value = 0x22;
final bitmask = 0x0f;
assert((value & bitmask) == 0x02); // AND
assert((value & ~bitmask) == 0x20); // AND NOT
assert((value | bitmask) == 0x2f); // OR
assert((value ^ bitmask) == 0x2d); // XOR
assert((value << 4) == 0x220); // Shift left
assert((value >> 4) == 0x02); // Shift right
A few operators remain, most of which you’ve already seen in other
examples.
Table 2.10. Other operators
| Operator | Name | Meaning |
|---|
| () | Function application | Represents a function call |
| [] | List access | Refers to the value at the specified index in the
list |
expr1 ? expr2 : expr3 | Conditional | If expr1 is true, executes
expr2; otherwise, executes
expr3 |
| . | Member access | Refers to a property of an expression; example: foo.bar selects property bar from expression foo |
| .. | Cascade | Allows you to perform multiple
operations on the members of a single object; described in the section called “Classes” |
?identifier | Argument definition test | Tests whether the caller specified an optional parameter;
described in the section called “Optional Parameters” |
You can control the flow of your Dart code using any of the
following:
if and else
for loops
while and do-while
loops
break and continue
switch and case
assert
You can also affect the control flow using try-catch and throw, as explained in the section called “Exceptions”.
Dart supports if statements
with optional else statements, as the
next sample shows. Also see conditional expressions (?:), which are
covered in the section called “Other Operators”.
if (isRaining()) {
you.bringRainCoat();
} else if (isSnowing()) {
you.wearJacket();
} else {
car.putTopDown();
}Remember, unlike JavaScript, Dart treats all values other than
true as false. See the section called “Booleans” for
more information.
You can iterate with the standard for loop. For example:
var message = new StringBuffer("Dart is fun");
for (var i = 0; i < 5; i++) {
message.write('!');
}Closures inside of Dart’s for
loops capture the value of the index, avoiding a common pitfall found in
JavaScript. For example, consider:
var callbacks = [];
for (var i = 0; i < 2; i++) {
callbacks.add(() => print(i));
}
callbacks.forEach((c) => c());The output is 0 and then
1, as expected. In contrast, the
example would print 2 and then
2 in JavaScript.
If the object that you are iterating over is an Iterable, you can
use the forEach()
method. Using forEach() is a good option if you don’t
need to know the current iteration counter:
candidates.forEach((candidate) => candidate.interview());
Iterable classes such as List and Set also support the for-in form of iteration, which is described
in the section called “Iteration”:
var collection = [0, 1, 2];
for (var x in collection) {
print(x);
}A while loop evaluates the
condition before the loop:
while(!isDone()) {
doSomething();
}A do-while loop evaluates the condition
after the loop:
do {
printLine();
} while (!atEndOfPage());Use break to stop
looping:
while (true) {
if (shutDownRequested()) break;
processIncomingRequests();
}Use continue to skip to the
next loop iteration:
for (int i = 0; i < candidates.length; i++) {
var candidate = candidates[i];
if (candidate.yearsExperience < 5) {
continue;
}
candidate.interview();
}You might write that example differently if you’re using a Iterable
such as a list or set:
candidates.where((c) => c.yearsExperience >= 5)
.forEach((c) => c.interview());Switch statements in Dart compare integer, string, or compile-time
constants using ==. The compared
objects must all be instances of the same class (and not of any of its
subtypes), and the class must not override ==.
Each non-empty case clause ends
with a break statement, as a rule.
Other valid ways to end a non-empty case clause are a continue, throw, or return statement.
Use a default clause to execute
code when no case clause
matches:
var command = 'OPEN';
switch (command) {
case 'CLOSED':
executeClosed();
break;
case 'PENDING':
executePending();
break;
case 'APPROVED':
executeApproved();
break;
case 'DENIED':
executeDenied();
break;
case 'OPEN':
executeOpen();
break;
default:
executeUnknown();
}The following example omits the break statement in the case clause, thus generating an error:
var command = 'OPEN';
switch (command) {
case 'OPEN':
executeOpen();
// ERROR: Missing break causes an exception to be thrown!!
case 'CLOSED':
executeClosed();
break;
}However, Dart does support empty case clauses, allowing a form of
fall-through:
var command = 'CLOSED';
switch (command) {
case 'CLOSED': // Empty case falls through.
case 'NOW_CLOSED':
// Runs for both CLOSED and NOW_CLOSED.
executeNowClosed();
break;
}If you really want fall-through, you can use a continue statement and a label:
var command = 'CLOSED';
switch (command) {
case 'CLOSED':
executeClosed();
continue nowClosed; // Continues executing at the nowClosed label.
nowClosed:
case 'NOW_CLOSED':
// Runs for both CLOSED and NOW_CLOSED.
executeNowClosed();
break;
}A case clause can have local
variables, which are visible only inside the scope of that
clause.
Use an assert statement to
disrupt normal execution if a boolean condition is false. You can find
examples of assert statements throughout this tour. Here are some
more:
assert(text != null); // Make sure the variable has a non-null value.
assert(number < 100); // Make sure the value is less than 100.
assert(urlString.startsWith('https')); // Make sure this is an HTTPS URL.Note
Assert statements work only in checked mode. They have no effect
in production mode.
Inside the parentheses after assert, you can put any expression that
resolves to a boolean value or to a function. If the expression’s value
or function’s return value is true, the assertion succeeds and execution
continues. Otherwise, the assertion fails and an exception (an AssertionError)
is thrown.
Your Dart code can throw and catch exceptions. Exceptions are errors
indicating that something unexpected happened. If the exception isn’t
caught, the isolate that raised the exception is suspended, and typically
the isolate and its program are terminated.
In contrast to Java, all of Dart’s exceptions are unchecked
exceptions. Methods do not declare which exceptions they might throw, and
you are not required to catch any exceptions.
Dart provides Exception
and Error
types, as well as numerous predefined subtypes. You can, of course, define
your own exceptions. However, Dart programs can throw any non-null
object—not just Exception and Error objects—as an exception.
Here’s an example of throwing, or raising, an
exception:
throw new ExpectException('Value must be greater than zero');You can also throw arbitrary objects:
throw 'Out of llamas!';
Because throwing an exception is an expression, you can throw
exceptions in => statements, as well as anywhere else that allows
expressions:
distanceTo(Point other) => throw new UnimplementedError();
Catching, or capturing, an exception stops the exception from
propagating. Catching an exception gives you a chance to handle
it:
try {
breedMoreLlamas();
} on OutOfLlamasException {
buyMoreLlamas();
}To handle code that can throw more than one type of exception, you
can specify multiple catch clauses. The first catch clause that matches
the thrown object’s type handles the exception. If the catch clause does
not specify a type, that clause can handle any type of thrown
object:
try {
breedMoreLlamas();
} on OutOfLlamasException { // A specific exception
buyMoreLlamas();
} on Exception catch(e) { // Anything else that is an exception
print('Unknown exception: $e');
} catch(e) { // No specified type, handles all
print('Something really unknown: $e');
}As the preceding code shows, you can use either on or catch
or both. Use on when you need to
specify the exception type. Use catch
when your exception handler needs the exception object.
To ensure that some code runs whether or not an exception is
thrown, use a finally clause. If no
catch clause matches the exception, the exception is
propagated after the finally clause runs:
try {
breedMoreLlamas();
} finally {
cleanLlamaStalls(); // Always clean up, even if an exception is thrown.
}The finally clause runs after any matching
catch clauses:
try {
breedMoreLlamas();
} catch(e) {
print('Error: $e'); // Handle the exception first.
} finally {
cleanLlamaStalls(); // Then clean up.
}Learn more by reading the section called “Exceptions”.
Dart is an object-oriented language with classes and single
inheritance. Every object is an instance of a class, and all classes
descend from Object.
To create an object, you can use the new keyword with a
constructor for a class. Constructor names can be
either ClassName or
ClassName.identifier. For example:
var jsonData = json.parse('{"x":1, "y":2}');
var p1 = new Point(2,2); // Create a Point using Point().
var p2 = new Point.fromJson(jsonData); // Create a Point using Point.fromJson().Objects have members consisting of functions
and data (methods and instance
variables, respectively). When you call a method, you
invoke it on an object: the method has access to that
object’s functions and data.
Use a dot (.) to refer to an instance variable or method:
var p = new Point(2,2);
p.y = 3; // Set the value of the instance variable y.
assert(p.y == 3); // Get the value of y.
num distance = p.distanceTo(new Point(4,4)); // Invoke distanceTo() on p.
Use the cascade operator (..) when you want to perform a series of
operations on the members of a single object:
query('#button')
..text = 'Click to Confirm' // Get an object. Use its
..classes.add('important') // instance variables
..onClick.listen((e) => window.alert('Confirmed!')); // and methods.Some classes provide constant constructors. To create a compile-time
constant using a constant constructor, use const instead of new:
var p = const ImmutablePoint(2,2);
Constructing two identical compile-time constants results in a
single, canonical instance:
var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);
assert(identical(a,b)); // They are the same instance!
The following sections discuss how to implement classes.
Here’s how you declare instance variables:
class Point {
num x; // Declare an instance variable (x), initially null.
num y; // Declare y, initially null.
num z = 0; // Declare z, initially 0.
}All uninitialized instance variables have the value null.
All instance variables generate an implicit
getter method. Non-final, non-const instance
variables also generate an implicit setter
method. For details, see the section called “Getters and setters”.
class Point {
num x;
num y;
}
main() {
var point = new Point();
point.x = 4; // Use the setter method for x.
assert(point.x == 4); // Use the getter method for x.
assert(point.y == null); // Values default to null.
}If you initialize an instance variable where it is declared
(instead of in a constructor or method), the value is set when the
instance is created, which is before the constructor and its initializer
list execute.
Declare a constructor by creating a function with the same name as
its class (plus, optionally, an additional identifier as described in
the section called “Named constructors”). The most common form of
constructor, the generative constructor, creates a new instance of a
class:
class Point {
num x;
num y;
Point(num x, num y) {
// There's a better way to do this, stay tuned.
this.x = x;
this.y = y;
}
}The this keyword refers to the
current instance.
Note
Use this only when there is a
name conflict. Otherwise, Dart style omits the this.
The pattern of assigning a constructor argument to an instance
variable is so common, Dart has syntactic sugar to make it easy:
class Point {
num x;
num y;
// Syntactic sugar for setting x and y before the constructor body runs.
Point(this.x, this.y);
}If you don’t declare a constructor, a default constructor is
provided for you. The default constructor has no arguments and invokes
the no-argument constructor in the superclass.
Constructors aren’t inherited
Subclasses don’t inherit constructors from their superclass. A
subclass that declares no constructors has only the default (no
argument, no name) constructor.
Use a named constructor to implement multiple constructors for a
class or to provide extra clarity:
class Point {
num x;
num y;
Point(this.x, this.y);
// Named constructor
Point.fromJson(Map json) {
x = json['x'];
y = json['y'];
}
}Remember that constructors are not inherited, which means that a
superclass’s named constructor is not inherited by a subclass. If you
want a subclass to be created with a named constructor defined in the
superclass, you must implement that constructor in the
subclass.
Invoking a non-default superclass constructor
By default, a constructor in a subclass calls the superclass’s
unnamed, no-argument constructor. If the superclass doesn’t have such
a constructor, then you must manually call one of the constructors in
the superclass. Specify the superclass constructor after a colon
(:), just before the constructor body (if
any).
class Person {
Person.fromJson(Map data) {
print('in Person');
}
}
class Employee extends Person {
// Person does not have a default constructor;
// you must call super.fromJson(data).
Employee.fromJson(Map data) : super.fromJson(data) {
print('in Employee');
}
}
main() {
var emp = new Employee.fromJson({});
// Prints:
// in Person
// in Employee
}Besides invoking a superclass constructor, you can also
initialize instance variables before the constructor body runs.
Separate initializers with commas.
class Point {
num x;
num y;
Point(this.x, this.y);
// Initializer list sets instance variables before the constructor body runs.
Point.fromJson(Map json) : x = json['x'], y = json['y'] {
print('In Point.fromJson(): ($x, $y)');
}
}Warning
The right-hand side of an initializer does not have access to
this.
Sometimes a constructor’s only purpose is to redirect to another
constructor in the same class. A
redirecting constructor’s body is empty, with the constructor call
appearing after a colon
(:).
class Point {
num x;
num y;
Point(this.x, this.y); // The main constructor for this class.
Point.alongXAxis(num x) : this(x, 0); // Delegates to the main constructor.
}If your class produces objects that never change, you can make
these objects compile-time constants. To do this, define a const constructor and make sure that all
instance variables are final or
const.
class ImmutablePoint {
final num x;
final num y;
const ImmutablePoint(this.x, this.y);
static final ImmutablePoint origin = const ImmutablePoint(0, 0);
}Use the factory keyword when
implementing a constructor that doesn’t always create a new instance
of its class. For example, a factory constructor might return an
instance from a cache, or it might return an instance of a
subtype.
The following example demonstrates a factory constructor
returning objects from a cache:
class Logger {
final String name;
bool mute = false;
// _cache is library-private, thanks to the _ in front of its name.
static final Map<String, Logger> _cache = <String, Logger>{};
factory Logger(String name) {
if (_cache.containsKey(name)) {
return _cache[name];
} else {
final logger = new Logger._internal(name);
_cache[name] = logger;
return logger;
}
}
Logger._internal(this.name);
void log(String msg) {
if (!mute) {
print(msg);
}
}
}Note
Factory constructors have no access to this.
To invoke a factory constructor, you use the new keyword:
var logger = new Logger('UI');
logger.log('Button clicked');Methods are functions that provide behavior for an object.
Instance methods on objects can access instance variables and
this. The
distanceTo() method in the following sample is an
example of an instance method:
import 'dart:math';
class Point {
num x;
num y;
Point(this.x, this.y);
num distanceTo(Point other) {
var dx = x - other.x;
var dy = y - other.y;
return sqrt(dx * dx + dy * dy);
}
}Getters and setters are special methods that provide read and
write access to an object’s properties. Recall that each instance
variable has an implicit getter, plus a setter if appropriate. You can
create additional properties by implementing getters and setters,
using the get and set keywords:
class Rectangle {
num left;
num top;
num width;
num height;
Rectangle(this.left, this.top, this.width, this.height);
// Define two calculated properties: right and bottom.
num get right => left + width;
set right(num value) => left = value - width;
num get bottom => top + height;
set bottom(num value) => top = value - height;
}
main() {
var rect = new Rectangle(3, 4, 20, 15);
assert(rect.left == 3);
rect.right = 12;
assert(rect.left == -8);
}With getters and setters, you can start with instance variables,
later wrapping them with methods, all without changing client
code.
Note
Operators such as increment (++) work in the expected way,
whether or not a getter is explicitly defined. To avoid any
unexpected side effects, the operator calls the getter exactly
once, saving its value in a temporary variable.
Instance, getter, and setter methods can be abstract, defining
an interface but leaving its implementation up to other classes. To
make a method abstract, use a semicolon (;) instead of a method
body:
abstract class Doer {
// ...Define instance variables and methods...
void doSomething(); // Define an abstract method.
}
class EffectiveDoer extends Doer {
void doSomething() {
// ...Provide an implementation, so the method is not abstract here...
}
}Calling an abstract method results in a run-time error.
Also see the section called “Abstract Classes”.
You can override the operators shown in Table 2.11, “Operators that can be overridden”. For example, if you define a
Vector class, you might define a + method to add
two vectors.
Table 2.11. Operators that can be overridden
| < | + | | | [] |
| > | / | ^ | []= |
| <= | ~/ | & | ~ |
| >= | * | << | == |
| – | % | >> | |
Here’s an example of a class that overrides the + and -
operators:
class Vector {
final int x;
final int y;
const Vector(this.x, this.y);
Vector operator +(Vector v) { // Overrides + (a + b).
return new Vector(x + v.x, y + v.y);
}
Vector operator -(Vector v) { // Overrides - (a - b).
return new Vector(x - v.x, y - v.y);
}
}
main() {
final v = new Vector(2,3);
final w = new Vector(2,2);
assert(v.x == 2 && v.y == 3); // v == (2,3)
assert((v+w).x == 4 && (v+w).y == 5); // v+w == (4,5)
assert((v-w).x == 0 && (v-w).y == 1); // v-w == (0,1)
}For an example of overriding ==, see the section called “Implementing map keys”.
Use the abstract modifier to
define an abstract class—a class that can’t be
instantiated. Abstract classes are useful for defining interfaces, often
with some implementation. If you want your abstract class to appear to
be instantiable, define a factory constructor.
Abstract classes often have abstract methods. Here’s an
example of declaring an abstract class that has an abstract
method:
// This class is declared abstract and thus can't be instantiated.
abstract class AbstractContainer {
// ...Define constructors, fields, methods...
void updateChildren(); // Abstract method.
}The following class isn’t abstract, and thus can be instantiated
even though it defines an abstract method:
class SpecializedContainer extends AbstractContainer {
// ...Define more constructors, fields, methods...
void updateChildren() {
// ...Implement updateChildren()...
}
// Abstract method causes a warning but doesn't prevent instantiatation.
void doSomething();
}Every class implicitly defines an interface containing all the
instance members of the class and of any interfaces it implements. If
you want to create a class A that supports class B’s API without
inheriting B’s implementation, class A should implement the B
interface.
A class implements one or more interfaces by declaring them in an
implements clause and then providing
the APIs required by the interfaces. For example:
// A person. The implicit interface contains greet().
class Person {
final _name; // In the interface, but visible only in this library,
Person(this._name); // Not in the interface, since this is a constructor.
String greet(who) => 'Hello, $who. I am $_name.'; // In the interface.
}
// An implementation of the Person interface.
class Imposter implements Person {
final _name = ""; // We have to define this, but we don't use it.
String greet(who) => 'Hi $who. Do you know who I am?';
}
greetBob(Person person) => person.greet('bob');
main() {
print(greetBob(new Person('kathy')));
print(greetBob(new Imposter()));
}Here’s an example of specifying that a class implements multiple
interfaces:
class Point implements Comparable, Location {
// ...
}Use extends to create a
subclass, and super to refer to the
superclass:
class Television {
void turnOn() {
_illuminateDisplay();
_activateIrSensor();
}
...
}
class SmartTelevision extends Television {
void turnOn() {
super.turnOn();
_bootNetworkInterface();
_initializeMemory();
_upgradeApps();
}
...
}Subclasses can override instance methods, getters, and setters.
Here’s an example of overriding the Object class’s
noSuchMethod() method, which is called whenever code
attempts to use a non-existent method or instance variable:
class A {
// Unless you override noSuchMethod, using a non-existent member
// results in a NoSuchMethodError.
void noSuchMethod(Invocation mirror) {
print('You tried to use a non-existent member: ${mirror.memberName}');
}
}Class Variables and Methods
Use the static keyword to
implement class-wide variables and methods.
Static variables (class variables) are useful for class-wide
state and constants:
class Color {
static const RED = const Color('red'); // A constant static variable.
final String name; // An instance variable.
const Color(this.name); // A constant constructor.
}
main() {
assert(Color.RED.name == 'red');
}Static variables aren’t initialized until they’re used.
Static methods (class methods) do not operate on an instance,
and thus do not have access to this. For example:
import 'dart:math';
class Point {
num x;
num y;
Point(this.x, this.y);
static num distanceBetween(Point a, Point b) {
var dx = a.x - b.x;
var dy = a.y - b.y;
return sqrt(dx * dx + dy * dy);
}
}
main() {
var a = new Point(2, 2);
var b = new Point(4, 4);
var distance = Point.distanceBetween(a,b);
assert(distance < 2.9 && distance > 2.8);
}Note
Consider using top-level functions, instead of static methods,
for common or widely used utilities and functionality.
You can use static methods as compile-time constants. For
example, you can pass a static method as a parameter to a constant
constructor.
If you look at the API documentation for the basic array type,
List,
you’ll see that the type is actually List<E>. The <...> notation marks
List as a generic (or
parameterized) type—a type that has formal type
parameters. By convention, type variables have single-letter names, such
as E, T, S, K, and V.
Because types are optional in Dart, you never
have to use generics. You might
want to, though, for the same reason you might want
to use other types in your code: types (generic or not) let you document
and annotate your code, making your intent clearer.
For example, if you intend for a list to contain only strings, you
can declare it as List<String>
(read that as “list of string”). That way you, your fellow
programmers, and your tools (such as Dart Editor and the Dart VM in
checked mode) can detect that assigning a non-string to the list is
probably a mistake. Here’s an example:
var names = new List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
// ...
names.add(42); // Fails in checked mode (succeeds in production mode).
Another reason for using generics is to reduce code duplication.
Generics let you share a single interface and implementation between
many types, while still taking advantage of checked mode and static
analysis early warnings. For example, say you create an interface for
caching an object:
abstract class ObjectCache {
Object getByKey(String key);
setByKey(String key, Object value);
}You discover that you want a string-specific version of this
interface, so you create another interface:
abstract class StringCache {
String getByKey(String key);
setByKey(String key, String value);
}Later, you decide you want a number-specific version of this
interface... You get the idea.
Generic types can save you the trouble of creating all these
interfaces. Instead, you can create a single interface that takes a type
parameter:
abstract class Cache<T> {
T getByKey(String key);
setByKey(String key, T value);
}In this code, T is the stand-in type. It’s a placeholder that you
can think of as a type that a developer will define later.
Using Collection Literals
List and map literals can be parameterized. Parameterized literals
are just like the literals you’ve already seen, except that you add
<type> (for
lists) or <keyType,
valueType> (for maps) before the
opening bracket. You might use parameterized literals when you want type
warnings in checked mode. Here is example of using typed
literals:
var names = <String>['Seth', 'Kathy', 'Lars'];
var pages = <String, String>{
'index.html':'Homepage',
'robots.txt':'Hints for web robots',
'humans.txt':'We are people, not machines' };Note
Map literals always have string keys, so
their type is always <String,
SomeType>.
To specify one or more types when using a constructor, put the
types in angle brackets (<...>)
just after the class name. For example:
var names = new List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
var nameSet = new Set<String>.from(names);
The following code creates a map that has integer keys and values
of type View:
var views = new Map<int, View>();
Generic Collections and the Types they Contain
Dart generic types are reified, which means
that they carry their type information around at runtime. For example,
you can test the type of a collection, even in production mode:
var names = new List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true
However, the is expression
checks the type of the collection only—not of the
objects inside it. In production mode, a
List<String> might have some non-string items
in it. The solution is to either check each item’s type or wrap
item-manipulation code in an exception handler (see the section called “Exceptions”).
Note
In contrast, generics in Java use erasure,
which means that generic type parameters are removed at runtime. In
Java, you can test whether an object is a List, but you can’t test
whether it’s a List<String>.
For more information about generics, see Optional Types in
Dart.
The import, part, and library directives can help you create a modular
and shareable code base. Libraries not only provide APIs, but are a unit
of privacy: identifiers that start with an underscore (_) are visible only
inside the library. Every Dart app is a library, even
if it doesn’t use a library directive.
Libraries can be distributed using packages. See the section called “pub: The Dart Package Manager” for information about pub, a package manager
included in the SDK.
Use import to specify how a
namespace from one library is used in the scope of another
library.
For example, Dart web apps generally use the dart:html library, which
they can import like this:
import 'dart:html';
The only required argument to import is a URI specifying the library. For built-in libraries, the URI
has the special dart: scheme. For
other libraries, you can use a file system path or the package: scheme. The package: scheme specifies libraries provided
by a package manager such as the pub tool. For example:
import 'dart:io';
import 'package:mylib/mylib.dart';
import 'package:utils/utils.dart';
Specifying a library prefix
If you import two libraries that have conflicting identifiers,
then you can specify a prefix for one or both libraries. For example,
if library1 and library2 both have an Element class, then you might
have code like this:
import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;
// ...
var element1 = new Element(); // Uses Element from lib1.
var element2 = new lib2.Element(); // Uses Element from lib2.
Importing only part of a library
If you want to use only part of a library, you can selectively
import the library. For example:
import 'package:lib1/lib1.dart' show foo, bar; // Import only foo and bar.
import 'package:lib2/lib2.dart' hide foo; // Import all names EXCEPT foo.
Use library to name a library,
and part to specify additional files
in the library.
Note
You don’t have to use library in an app (a
file that has a top-level main() function), but doing so lets you
implement the app in multiple files.
Use library
identifier to specify the name of
the current library:
library ballgame; // Declare that this is a library named ballgame.
import 'dart:html'; // This app uses the HTML library.
// ...Code goes here...
Associating a file with a library
To add an implementation file, put part
fileUri in the file that has the
library statement, where
fileUri is the path to the implementation file.
Then in the implementation file, put part of
identifier, where
identifier is the name of the library. The
following example uses part and part
of to implement a library in three files.
The first file, ballgame.dart, declares the
ballgame library, imports other libraries it needs, and specifies that
ball.dart and util.dart are
parts of this library:
library ballgame;
import 'dart:html';
// ...Other imports go here...
part 'ball.dart';
part 'util.dart';
// ...Code might go here...
The second file, ball.dart, implements part
of the ballgame library:
part of ballgame;
// ...Code goes here...
The third file, util.dart, implements the
rest of the ballgame library:
part of ballgame;
// ...Code goes here...
You can combine or repackage libraries by re-exporting part or
all of them. For example, you might have a huge library that you
implement as a set of smaller libraries. Or you might create a library
that provides a subset of methods from another library.
// In french.dart:
library french;
hello() => print('Bonjour!');
goodbye() => print('Au Revoir!');
// In togo.dart:
library togo;
import 'french.dart';
export 'french.dart' show hello;
// In another .dart file:
import 'togo.dart';
void main() {
hello(); //print bonjour
goodbye(); //FAIL
}Modern web browsers, even on mobile platforms, run on multi-core
CPUs. To take advantage of all those cores, developers traditionally use
shared-memory threads running concurrently. However, shared-state
concurrency is error prone and can lead to complicated code.
Instead of threads, all Dart code runs inside of
isolates. Each isolate has its own memory heap,
ensuring that no isolate’s state is accessible from any other
isolate.
Learn more about isolates in the section called “dart:isolate - Concurrency with Isolates”.
In Dart, functions are objects, just like strings and numbers are
objects. A typedef, or function-type
alias, gives a function type a name that you can use when
declaring fields and return types. A typedef retains type information when
a function type is assigned to a variable.
Consider the following code, which does not use a typedef:
class SortedCollection {
Function compare;
SortedCollection(int f(Object a, Object b)) {
compare = f;
}
}
int sort(Object a, Object b) => ... ; // Initial, broken implementation.
main() {
SortedCollection collection = new SortedCollection(sort);
// All we know is that compare is a function, but what type of function?
assert(collection.compare is Function);
}Type information is lost when assigning f to compare.
The type of f is (Object, Object) →
int (where → means returns), yet the type of compare is Function. If we change the code to
use explicit names and retain type information, both developers and tools
can use that information.
typedef int Compare(Object a, Object b);
class SortedCollection {
Compare compare;
SortedCollection(this.compare);
}
int sort(Object a, Object b) => ... ; // Initial, broken implementation.
main() {
SortedCollection collection = new SortedCollection(sort);
assert(collection.compare is Function);
assert(collection.compare is Compare);
}Note
Currently, typedefs are restricted to function types. We expect
this to change.
Because typedefs are simply aliases, they offer a way to check the
type of any function. For example:
typedef int Compare(int a, int b);
int sort(int a, int b) => a - b;
main() {
assert(sort is Compare); // True!
}Use metadata to give additional information about your code. A
metadata annotation begins with the character @,
followed by either a reference to a compile-time constant (such as
deprecated) or a call to a constant constructor.
The meta
package defines the API for two common annotations:
@deprecated and @overrides. Here’s
an example of using the @deprecated annotation:
import 'package:meta/meta.dart'; // Defines deprecated.
class Television {
/// _Deprecated: Use [turnOn] instead._
@deprecated // Metadata; makes Dart Editor warn about using activate().
void activate() {
turnOn();
}
/// Turns the TV's power on.
void turnOn() {
print('on!');
}
}
You can define your own metadata annotations. For example, here’s
how the meta package defines the deprecated
constant:
const deprecated = const _Deprecated();
class _Deprecated {
const _Deprecated();
}
Here’s an example of defining a @todo annotation that takes two
arguments:
library todo;
class todo {
final String who;
final String what;
const todo(this.who, this.what);
}And here’s an example of using that @todo annotation:
import 'todo.dart';
@todo('seth', 'make this do something')
void doSomething() {
print('do something');
}Metadata can appear before a library, class, typedef, type
parameter, constructor, factory, function, field, parameter, or variable
declaration and before an import or export directive. In the future,
you’ll be able to retrieve metadata at runtime using reflection.
Dart supports single-line comments, multi-line comments, and
documentation comments.
A single-line comment begins with //. Everything between // and the end of line is ignored by the Dart
compiler.
main() {
// TODO: refactor into an AbstractLlamaGreetingFactory?
print('Welcome to my Llama farm!');
}A multi-line comment begins with /* and ends with */. Everything between /* and */
is ignored by the Dart compiler (unless the comment is a documentation
comment; see the next section). Multi-line comments can nest.
main() {
/*
* This is a lot of work. Consider raising chickens.
Llama larry = new Llama();
larry.feed();
larry.exercise();
larry.clean();
*/
}Documentation comments are multi-line or single-line comments that
begin with /** or ///. Using /// on consecutive lines has the same effect
as a multi-line doc comment.
Inside a documentation comment, the Dart compiler ignores all text
unless it is enclosed in brackets. Using brackets, you can refer to
classes, methods, fields, top-level variables, functions, and
parameters. The names in brackets are resolved in the lexical scope of
the documented program element.
Here is an example of documentation comments with references to
other classes and arguments:
/**
* The llama (Lama glama) is a domesticated South American
* camelid, widely used as a meat and pack animal by Andean
* cultures since pre-Hispanic times.
*/
class Llama {
String name;
/**
* Feeds your llama [Food].
*
* The typical llama eats one bale of hay per week.
*/
void feed(Food food) {
// ...
}
/// Exercises your llama with an [activity] for
/// [timeLimit] minutes.
void exercise(Activity activity, int timeLimit) {
// ...
}
}In the generated documentation, [Food] becomes a link to the API docs for the
Food class.
To parse Dart code and generate HTML documentation, you can use
Dart Editor, which in turn uses the SDK’s dartdoc package. For an
example of generated documentation, see the Dart API documentation.