[Dart Language Tour] Classes, Enums, Callable Classes, Metadata
Classes, Enums, Callable Classes, Metadata
Classes
Dart is an object-oriented language with classes and mixin-based inheritance. Every object is an instance of a class, and all classes descend from Object
. Mixin-based inheritance means that although every class (except for Object) has exactly one superclass, a class body can be reused in multiple class hierarchies. Extension methods are a way to add functionality to a class without changing the class or creating a subclass.
Using class members methods and instance variables
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:
1 | var p = Point(2, 2); |
Use ?.
instead of .
to avoid an exception when the leftmost operand is null
:
1 | // If p is non-null, set a variable equal to its y value. |
Using constructors
You can create an object using a constructor. Constructor names can be either ClassName or ClassName.identifier. For example, the following code creates Point objects using the Point() and Point.fromJson() constructors:
1 | var p1 = Point(2, 2); |
The following code has the same effect, but uses the optional new
keyword before the constructor name:
1 | var p1 = new Point(2, 2); |
Version note: The new
keyword became optional in Dart 2.
Some classes provide constant constructors. To create a compile-time constant using a constant constructor, put the const
keyword before the constructor name:
1 | var p = const ImmutablePoint(2, 2); |
Constructing two identical compile-time constants results in a single, canonical instance:
1 | var a = const ImmutablePoint(1, 1); |
Within a constant context, you can omit the const
before a constructor or literal. For example, look at this code, which creates a const map:
1 | // Lots of const keywords here. |
You can omit all but the first use of the const
keyword:
1 | // Only one const, which establishes the constant context. |
If a constant constructor is outside of a constant context and is invoked without const
, it creates a non-constant object:
1 | var a = const ImmutablePoint(1, 1); // Creates a constant |
Version note: The const
keyword became optional within a constant context in Dart 2.
Getting an object’s type
To get an object’s type at runtime, you can use Object’s runtimeType
property, which returns a Type object.
1 | print('The type of a is ${a.runtimeType}'); |
Up to here, you’ve seen how to use classes. The rest of this section shows how to implement classes.
Instance variables
Here’s how you declare instance variables:
1 | class Point { |
All uninitialized instance variables have the value null
.
All instance variables generate an implicit getter method. Non-final instance variables also generate an implicit setter method. For details, see Getters and setters.
1 | class Point { |
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.
Constructors
Declare a constructor by creating a function with the same name as its class (plus, optionally, an additional identifier as described in Named constructors). The most common form of constructor, the generative constructor, creates a new instance of a class:
1 | class Point { |
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:
1 | class Point { |
Default constructors
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.
Named constructors
Use a named constructor to implement multiple constructors for a class or to provide extra clarity:
1 | class Point { |
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. The superclass’s constructor is called at the beginning of the constructor body. If an initializer list is also being used, it executes before the superclass is called. In summary, the order of execution is as follows:
-
initializer list
-
superclass’s no-arg constructor
-
main class’s no-arg constructor
If the superclass doesn’t have an unnamed, no-argument 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).
Because the arguments to the superclass constructor are evaluated before invoking the constructor, an argument can be an expression such as a function call:
1 | class Employee extends Person { |
Warning: Arguments to the superclass constructor do not have access to this
. For example, arguments can call static methods but not instance methods.
Initializer list
Besides invoking a superclass constructor, you can also initialize instance variables before the constructor body runs. Separate initializers with commas (:
).
1 | // Initializer list sets instance variables before |
Warning: The right-hand side of an initializer does not have access to this
.
During development, you can validate inputs by using assert
in the initializer list.
1 | Point.withAssert(this.x, this.y) : assert(x >= 0) { |
Initializer lists are handy when setting up final fields. The following example initializes three final fields in an initializer list.
1 | import 'dart:math'; |
Redirecting constructors
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 (:
).
1 | class Point { |
Constant constructors
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.
1 | class ImmutablePoint { |
Constant constructors don’t always create constants. For details, see the section on using constructors - https://dart.dev/guides/language/language-tour#using-constructors.
Factory constructors
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. Another use case for factory constructors is initializing a final variable using logic that can’t be handled in the initializer list.
In the following example, the Logger factory constructor returns objects from a cache, and the Logger.fromJson factory constructor initializes a final variable from a JSON object.
1 | class Logger { |
Note: Factory constructors have no access to this
.
Invoke a factory constructor just like you would any other constructor:
1 | var logger = Logger('UI'); |
Methods
Methods are functions that provide behavior for an object.
Instance methods
Instance methods on objects can access instance variables and this. The distanceTo() method in the following sample is an example of an instance method:
1 | import 'dart:math'; |
Operators
Operators are instance methods with special names. Dart allows you to define operators with the following names:
1 | < + | [] |
Note: You may have noticed that some operators, like !=
, are not in the list of names. That’s because they’re just syntactic sugar. For example, the expression e1 != e2 is syntactic sugar for !(e1 == e2).
An operator declaration is identified using the built-in identifier operator
. The following example defines vector addition (+
) and subtraction (-
):
1 | class Vector { |
Getters and setters
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:
1 | class Rectangle { |
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.
Abstract methods
Instance, getter, and setter methods can be abstract, defining an interface but leaving its implementation up to other classes. Abstract methods can only exist in abstract classes.
To make a method abstract, use a semicolon (;
) instead of a method body:
1 | abstract class Doer { |
Abstract classes
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:
1 | // This class is declared abstract and thus |
Implicit interfaces
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:
1 | // A person. The implicit interface contains greet(). |
Here’s an example of specifying that a class implements multiple interfaces:
1 | class Point implements Comparable, Location {...} |
Extending a class
Use extends to create a subclass, and super to refer to the superclass:
1 | class Television { |
Overriding members
Subclasses can override instance methods (including operators), getters, and setters. You can use the @override
annotation to indicate that you are intentionally overriding a member:
1 | class SmartTelevision extends Television { |
To narrow the type of a method parameter or instance variable in code that is type safe, you can use the covariant
keyword.
Warning: If you override ==
, you should also override Object’s hashCode
getter. For an example of overriding ==
and hashCode, see Implementing map keys - https://dart.dev/guides/libraries/library-tour#implementing-map-keys.
noSuchMethod()
To detect or react whenever code attempts to use a non-existent method or instance variable, you can override noSuchMethod():
1 | class A { |
You can’t invoke an unimplemented method unless one of the following is true:
-
The receiver has the static type dynamic.
-
The receiver has a static type that defines the unimplemented method (abstract is OK), and the dynamic type of the receiver has an implemention of noSuchMethod() that’s different from the one in class Object.
For more information, see the informal noSuchMethod forwarding specification - https://github.com/dart-lang/sdk/blob/master/docs/language/informal/nosuchmethod-forwarding.md.
Extension methods
Extension methods are a way to add functionality to existing libraries. You might use extension methods without even knowing it. For example, when you use code completion in an IDE, it suggests extension methods alongside regular methods.
Here’s an example of using an extension method on String named parseInt() that’s defined in string_apis.dart:
1 | // string_apis.dart |
Use parseInt method:
1 | import 'string_apis.dart'; |
For details of using and implementing extension methods, see the extension methods page - https://dart.dev/guides/language/extension-methods.
Enumerated types
Enumerated types, often called enumerations or enums
, are a special kind of class used to represent a fixed number of constant values.
Using enums
Declare an enumerated type using the enum keyword:
1 | enum Color { red, green, blue } |
You can use trailing commas when declaring an enumerated type.
Each value in an enum has an index getter, which returns the zero-based position of the value in the enum declaration. For example, the first value has index 0, and the second value has index 1.
1 | assert(Color.red.index == 0); |
To get a list of all of the values in the enum, use the enum’s values constant.
1 | List<Color> colors = Color.values; |
You can use enums in switch statements - https://dart.dev/guides/language/language-tour#switch-and-case, and you’ll get a warning if you don’t handle all of the enum’s values:
1 | var aColor = Color.blue; |
Enumerated types have the following limits:
-
You can’t subclass, mix in, or implement an enum.
-
You can’t explicitly instantiate an enum.
For more information, see the Dart language specification - https://dart.dev/guides/language/spec.
Adding features to a class: mixins
Mixins are a way of reusing a class’s code in multiple class hierarchies.
To use a mixin, use the with keyword followed by one or more mixin names. The following example shows two classes that use mixins:
1 | class Musician extends Performer with Musical { |
To implement a mixin, create a class that extends Object and declares no constructors. Unless you want your mixin to be usable as a regular class, use the mixin
keyword instead of class. For example:
1 | mixin Musical { |
Restrict mixin
Sometimes you might want to restrict the types that can use a mixin. For example, the mixin might depend on being able to invoke a method that the mixin doesn’t define. As the following example shows, you can restrict a mixin’s use by using the on
keyword to specify the required superclass:
1 | class Musician { |
In the preceding code, only classes that extend or implement the Musician class can use the mixin MusicalPerformer. Because SingerDancer extends Musician, SingerDancer can mix in MusicalPerformer.
Class variables and methods / Static variables and methods
Use the static
keyword to implement class-wide variables and methods.
Static variables
Static variables (class variables) are useful for class-wide state and constants:
1 | class Queue { |
Static variables aren’t initialized until they’re used.
Note: This page follows the style guide recommendation - https://dart.dev/guides/language/effective-dart/style#identifiers of preferring lowerCamelCase for constant names.
Static methods
Static methods (class methods) don’t operate on an instance, and thus don’t have access to this. They do, however, have access to static variables. As the following example shows, you invoke static methods directly on a class:
1 | import 'dart:math'; |
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.
Callable classes
To allow an instance of your Dart class to be called like a function, implement the call()
method.
In the following example, the WannabeFunction class defines a call()
function that takes three strings and concatenates them, separating each with a space, and appending an exclamation.
1 | class WannabeFunction { |
Metadata
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.
Two annotations are available to all Dart code: @deprecated
and @override
. For examples of using @override
, see Extending a class. Here’s an example of using the @deprecated
annotation:
1 | class Television { |
You can define your own metadata annotations. Here’s an example of defining a @todo
annotation that takes two arguments:
1 | library todo; |
And here’s an example of using that @todo
annotation:
1 | import 'todo.dart'; |
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. You can retrieve metadata at runtime using reflection.
References
[1] Language tour | Dart - https://dart.dev/guides/language/language-tour
[2] Effective Dart: Design | Dart - https://dart.dev/guides/language/effective-dart/design