Collections and Iterables
Learning Objectives
- You know how to create and manipulate lists, maps, and sets in Dart.
- You know how to use type annotations to define the type of data that a collection can hold.
- You know how to use collections as function parameters and return values.
- You know how to iterate over collections using iterables and know common methods like forEach, where, and map.
Most programming languages provide a set of data structures that can be used to manipulate collections of data. The most commonly used data structures include lists (arrays), maps (key-value pairs), and sets. In Dart, these data structures are available as part of the standard library.
Common data collections
The common data collections in Dart include lists, maps, and sets.
Lists and sets
Lists are created using []
and sets are created using {}
. The following program shows how to create a list and a set in Dart.
Lists and sets have a similar syntax, but lists allow duplicate values, while sets do not. Both also have methods for adding, removing, and clearing values, as shown below.
Lists can be turned into sets and vice versa using the toSet
and toList
methods. The following example demonstrates this.
Both lists and sets have a length, and values in lists can be accessed with the indexing operator []
. Sets on the other hand are not indexed. Iterating over values in a list can be done using a for loop and an index, or using a for loop and the in
keyword. For loop and an index does not work for sets, and when iterated over with a for loop, the in
keyword is used.
The following example shows iterating over the values in both a list and a set.
Maps
Maps are created using {}
, where each key-value pair is separated by a colon. The following program shows how to create a map in Dart.
Maps have a remove
and clear
, and a [key] = value
shorthand for adding key-value pairs. The following example demonstrates these.
To iterate over a map, the for ... in
loop is used, where the loop variable is a MapEntry
object. The following example demonstrates iterating over a map.
Type inference
Dart uses type inference to infer the data type of a variable at compile time. However, there are times when the type of a variable cannot be inferred, or when the type of a variable needs to be flexible.
As an example, if a set does not contain values during creation, the type must be explicitly stated during creation, or otherwise Dart will infer the type as Map
. This is demonstrated in the following example.
When you run the above program, you will see the type of the data collection as well as the type of data that the collection can hold. In the above example, the type of data is dynamic
. To specify the type of data that the collection can hold, type annotations are used.
Type annotations
The type of data that a collection can hold can be defined using type annotations. Type annotations are provided within smaller <
than and bigger than >
symbols, and the data type within the symbols. In the following example, we create a list that contains only integers, a map that has integers as keys and strings as values, and a set that can only contain strings.
Using type annotations also leads to a situation, where we do not explixitly need to declare the type Set, as the type can be inferred from the type annotation that shows that there are individual values. This is demonstrated in the following example.
Defining the type annotations increases type-safety, which then allows providing type-specific warnings and errors to the developer. As an example, compiling or running the following application shows an error indicating that a string cannot be assigned to the parameter type int — this stems from us attempting to place a string into a list that can only contain integers.
Collections as function parameters and return values
When using collections as function parameters or return values, the type of the data collection and the type of data that the collection can hold should be explicitly defined.
For lists, the type is List
, for maps, the type is Map
, and for sets, the type is Set
. Type annotations are placed immediately after the data type using the smaller than and larger than symbols, which then include the data type. As an example, the following outlines a function that could be used to print a list that contains integers.
Similarly, the following example outlines a function that could be used to create a map that contains integers as keys and strings as values.
There are some cases, where the data type of a collection is not declared. As an example, if we would create a function that can be used to swap two items in a list, it likely would not be meaningful to restrict the type of data that the list can contain. The following example outlines such a function.
As a reminder, we previously learned that Dart is pass-by-value
. The above example outlines a program where the value that is passed is a reference to the list. As the parameter list
in the function swap
holds a reference as the value, the function has access to the list that is being referenced, and can thus manipulate it.
Collections as iterables
Collections are iterable, which means that objects in the collection can be iterated over one at a time. In Dart, this functionality is implemented through the Iterable-class, which provides the functionality for iterating over — or going over the contents of — a collection.
Both List and Set implement the Iterable, which makes it easy to iterate over the values in the collections. Iterating over values in a Map is also possible, although due to maps containing key-value -pairs, iteration is somewhat different.
Here, we briefly visit some of the methods used when working with iterables: where
, map
, and fold
.
Filtering with where
When iterating over a collection, we can filter the iterated values using the where
method. The method returns an iterable, which allows using its methods to decide what to do with the remaining values. In the following example, the where
method is used to limit the iterable to values that are larger than 1.
The where
method returns an iterable, which means that they can be further iterated over. An iterable can also be turned into a list or a set using the toList
and toSet
methods. The following example demonstrates this.
Maps cannot be directly iterated over using the where
method, as the method is not available for maps. Instead, we can iterate over the entries using the entries
property, which has an iterable collection of MapEntry
objects with key
and value
properties. The following example demonstrates how to filter a map based on the value of the key-value pair.
There is no toMap
method in an iterable, but the named constructor fromEntries
of Map can be used to create a map from an iterable of MapEntry
objects. The following example demonstrates this.
Iterators also have firstWhere
and lastWhere
methods that return the first or last element that satisfy a given condition. The following example demonstrates how to find the first odd number from a list.
The methods firstWhere
and lastWhere
also have an optional parameter orElse
, which is a function that is called when no element satisfies the condition. The following example demonstrates how to find the first odd number from a list, and if no odd number is found, the value -1
is returned.
Transforming with map
Another useful method when handling iterables is map, which allows transforming values from one to another. In the following example, we multiply numbers in an iterable by two and then print them.
Folding an iterable
Iterables can be reduced to a single value using the fold
function. The fold
function iteratively combines values of the iterable, leading to a single value. The function takes two parameters: a starting value and a two-parameter function describing how values in the iterable should be combined.
The following function demonstrates merging a list containing strings to a single string variable.
Dart also has a function
join
that can be used to concatenate strings in a list. Thejoin
function takes a separator as an argument, which is placed between the strings in the list.
Similarly, iterables containing numbers could be processed. The following example demonstrates calculating the sum of numbers in a list.