[Dart Language Tour] Generics, Libraries and visibility
Generics, Libraries and visibility
Generics
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, most type variables have single-letter names, such as E
, T
, S
, K
, and V
.
Why use generics?
Generics are often required for type safety, but they have more benefits than just allowing your code to run:
-
Properly specifying generic types results in better generated code.
-
You can use generics to reduce code duplication.
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 can detect that assigning a non-string to the list is probably a mistake. Here’s an example:
1 | var names = List<String>(); |
Generic classes
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 static analysis. For example, say you create an interface for caching an object:
1 | abstract class ObjectCache { |
You discover that you want a string-specific version of this interface, so you create another interface:
1 | abstract class StringCache { |
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:
1 | abstract class Cache<T> { |
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, set, and map literals can be parameterized. Parameterized literals are just like the literals you’ve already seen, except that you add <type>
(for lists and sets) or <keyType, valueType>
(for maps) before the opening bracket. Here is an example of using typed literals:
1 | var names = <String>['Seth', 'Kathy', 'Lars']; |
Using parameterized types with constructors
To specify one or more types when using a constructor, put the types in angle brackets (<...>
) just after the class name. For example:
1 | var nameSet = Set<String>.from(names); |
The following code creates a map that has integer keys and values of type View:
1 | var views = 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:
1 | var names = List<String>(); |
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
Restricting the parameterized type
When implementing a generic type, you might want to limit the types of its parameters. You can do this using extends.
1 | class Foo<T extends SomeBaseClass> { |
It’s OK to use SomeBaseClass or any of its subclasses as generic argument:
1 | var someBaseClassFoo = Foo<SomeBaseClass>(); |
It’s also OK to specify no generic argument:
1 | var foo = Foo(); |
Specifying any non-SomeBaseClass type results in an error:
1 | var foo = Foo<Object>(); |
Generic methods and functions
Initially, Dart’s generic support was limited to classes. A newer syntax, called generic methods, allows type arguments on methods and functions:
1 | T first<T>(List<T> ts) { |
Here the generic type parameter on first (<T>
) allows you to use the type argument T
in several places:
-
In the function’s return type (
T
). -
In the type of an argument (
List<T>
). -
In the type of a local variable (
T tmp
).
Libraries and visibility
The import 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 - https://dart.dev/guides/packages.
Using libraries
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
- https://api.dart.dev/stable/dart-html library, which they can import like this:
1 | 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:
1 | import 'package:test/test.dart'; |
Note: URI stands for uniform resource identifier. URLs (uniform resource locators) are a common kind of URI.
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:
1 | import 'package:lib1/lib1.dart'; |
Importing only part of a library
If you want to use only part of a library, you can selectively import the library. For example:
1 | // Import only foo. |
Lazily loading a library
Deferred loading (also called lazy loading) allows a web app to load a library on demand, if and when the library is needed. Here are some cases when you might use deferred loading:
-
To reduce a web app’s initial startup time.
-
To perform A/B testing—trying out alternative implementations of an algorithm, for example.
-
To load rarely used functionality, such as optional screens and dialogs.
Only dart2js supports deferred loading. Flutter, the Dart VM, and dartdevc don’t support deferred loading. For more information, see issue #33118 - https://github.com/dart-lang/sdk/issues/33118 and issue #27776 - https://github.com/dart-lang/sdk/issues/27776.
To lazily load a library, you must first import it using deferred as.
1 | import 'package:greetings/hello.dart' deferred as hello; |
When you need the library, invoke loadLibrary() using the library’s identifier.
1 | Future greet() async { |
In the preceding code, the await
keyword pauses execution until the library is loaded. For more information about async
and await
, see asynchrony support - https://dart.dev/guides/language/language-tour#asynchrony-support.
You can invoke loadLibrary()
multiple times on a library without problems. The library is loaded only once.
Keep in mind the following when you use deferred loading:
-
A deferred library’s constants aren’t constants in the importing file. Remember, these constants don’t exist until the deferred library is loaded.
-
You can’t use types from a deferred library in the importing file. Instead, consider moving interface types to a library imported by both the deferred library and the importing file.
-
Dart implicitly inserts
loadLibrary()
into the namespace that you define usingdeferred
asnamespace
. TheloadLibrary()
function returns aFuture
- https://dart.dev/guides/libraries/library-tour#future.
Implementing libraries
See Create Library Packages - https://dart.dev/guides/libraries/create-library-packages for advice on how to implement a library package, including:
-
How to organize library source code.
-
How to use the export directive.
-
When to use the part directive.
-
When to use the library directive.
-
How to use conditional imports and exports to implement a library that supports multiple platforms.
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