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?
Properly specifying generic types results in better generated code.
You can use generics to reduce code duplication.
- Using collection literals
- Using parameterized types with constructors
- Generic collections and the types they contain
- Restricting the parameterized type
Generics are often required for type safety, but they have more benefits than just allowing your code to run:
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:
var words = List<String>();
words.addAll(["Hi", "Hello"]);
words.add(42); //Error
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 a class for caching an object of type Point:
class Point {
int x, y;
}
class Vector {
int x, y, z;
}
class PointCache {
String key;
Point point;
PointCache(this.key, this.point);
Point getByKey(String k) {
if (k == key)
return point;
else {
print("Invalid Key !");
return null;
}
}
void setByKey(String k, Point p) {
if (k == key)
point = p;
else
print("Invalid Key !");
}
}
You discover that you want a Vector-specific version of this class, so you create another class:
class VectorCache {
String key;
Vector vector;
VectorCache(this.key, this.vector);
Vector getByKey(String k) {
if (k == key)
return vector;
else {
print("Invalid Key !");
return null;
}
}
void setByKey(String k, Vector v) {
if (k == key)
vector = v;
else
print("Invalid Key !");
}
}
Later, you decide you want another version of this class... You get the idea.
Generic types can save you the trouble of creating all these classes. Instead, you can create a single class that takes a type parameter:
class Cache<T> {
String key;
T object;
Cache(this.key, this.object);
T getByKey(String k) {
if (k == key)
return object;
else {
print("Invalid Key !");
return null;
}
}
void setByKey(String k, T o) {
if (k == key)
object = o;
else
print("Invalid Key !");
}
}
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.
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:
var words = <String>["dart", "flutter", "apps"];
var uniqueNumbers = <int>{1, 2, 3};
var steps = <int, String>{1: "dart", 2: "flutter", 3: "apps"};
To specify one or more types when using a constructor, put the types in angle brackets (<...>) just after the class name. For example:
var numbers = [1, 4, 5, 1, 3, 4, 6, 6];
var uniqueNumber = Set<int>.from(numbers); //{1, 4, 5, 3, 6}
The following code creates a map that has integer keys and values of type Point:
var points = Map<int, Point>();
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:
var words = <String>["dart", "flutter", "apps"];
words.add("Pro");
print(words is List<String>); //true
When implementing a generic type, you might want to limit the types of its parameters. You can do this using extends .
class SomeBaseClass {
//Implementation of SomeBaseClass goes Here
}
class Point extends SomeBaseClass {
//Implementation of Point goes Here
}
class Vector extends SomeBaseClass {
//Implementation of Vector goes Here
}
class Cache<T extends SomeBaseClass> {
//Implementation goes Here
}
It’s OK to use SomeBaseClass or any of its subclasses as generic argument:
var p = Cache<Point>();
var v = Cache<Vector>();
var b = Cache<SomeBaseClass>();
It’s also OK to specify no generic argument:
var c = Cache();
print(c); //Instance of 'Cache<SomeBaseClass>'
Specifying any non-SomeBaseClass type results in an error:
var c = Cache<String>(); //Error