Dart: Generics
Generics are a concept from the generic programming paradigm whereby types can be specified when required later, in this way they act as a placeholder for a concrete type
. Imagine, without generics, you'd have to write a List
class for every single type of object it can contain: IntList
, StringList
, WhateverList
!
You will most likely have already encountered generics in Dart when working with collection classes. List<>
allows any object to be stored within it because it has those funny little angle brackets after it's name:
abstract class List<E> { }
These <>
angle brackets indicate the class has a parameterised type which will be used in place of E
in classes that extend List<>
. Anywhere that E
is found, it will use this parameterised type.
Example
To examine how generics work, lets define our own simple container that can store values:
class ValueObject<T> {
final T storage;
ValueObject(T value);
// ...
}
💡 Tip
The letterT
above used in the parameterised type is simply a placeholder for the eventual type. This can be anything but Dart frequently usesE
,T
,S
,K
andV
.
To create an instance of ValueObject
, we now need to specify the parameterised type:
ValueObject<int> age = ValueObject<int>();
These kinds of immutable value objects are common in OOP and allow you to define compound objects that store commonly associated data, say like addresses or GPS coordinates. They also allow you to model behaviour and make sense of data by making it a concrete "thing", rather than solely relying on a smattering of simple types. More on these in another article.
Creating an instance of ValueObject<int>
will effectively replace all references of T
with the parameterised type int
:
class ValueObject<int> {
final int storage;
ValueObject(int value);
// ...
}
Restricting types
You can restrict the generic parameter to specific allowed types by using extends
within the parameter itself. The below example restricts T
to a num
or descendent of it:
class ValueObject<T extends num> { }
Now, our ValueObject
class can only store int
and double
values.
Generic methods
What if you need a method or function to be generic, without defining a parameterised type on the class itself?
T multiply<T extends num>(T a, T b) {
return a * b;
}
Since the parameter is defined on the method, this allows it to take in a completely different parameterised type to it's parent.
Calling this method would then look very similar to how we define the parameterised type on a generic class:
double value = multiply<double>(9.2325666, 192.432);
1776.6412559711998
Attempting to use a different type than what the parameter expects will result in an error:
String value = multiply<String>('This', "won't work");
'String' doesn't extend 'num'
Reified generics
Dart's generic system supports reification. Some other languages like Java that support generics only do so at compile time, effectively throwing away the type information at runtime. Reified generics allow you to pretend that the generic is still there.
ValueObject<List<int>> shopping = ValueObject<List<int>>();
The real parameterised type T
of ValueObject<T>
can be found out at runtime by
just printing T
:
print(T);
List
🚧 Warning
Trying to test the type usingT is String
will always returnfalse
. This is because Dart treatsT
as an expression which evaluates toType
, not it's concrete class. This is detailed further in Dart SDK issue #11923.
Using generics can greatly reduce the amount of code you have to write by giving you the ability to think about an issue in a more abstract way. As we have seen, this is particularly useful for classes that are agnostic about the different types of values it can hold or work with.