Chapter 8. Collections of richer classes

This chapter covers

  • Working with collections
  • Building type-safe, general-purpose classes with generics
  • Overloading class operators

In the previous two chapters, we discussed how you can use classes and interfaces in your Dart applications and libraries. In this final chapter on classes, we’ll look at how to make classes richer and even more flexible and descriptive by using some of their advanced features.

We’ll start by using the built-in collection classes, such as List and Map, which allow you to manipulate lists of data. By using generic types, you can help make accessing your collections type-safe and benefit from additional validations from the type checker. You’ll use the indexer syntax to access elements directly in lists and maps and discover how to build literal maps and lists from known, preexisting values. We’ll also look at the JavaScript Object Notation (JSON) methods to convert maps into strings and back to maps.

Next, we’ll examine how you can make your own classes available for developers to use in a generic fashion, so that rather than creating two nearly identical classes, you’ll be able to create a single class that can be used, strongly typed, in two ways.

Finally, we’ll cover operator overloading. Overloading happens when you provide a new implementation, customized to your particular class, which allows you to overload the common operators such as > (greater than) and < (less than). This function provides your classes with greater readability when they’re in use. We also revisit Map and look at overloading the [] and []= indexer operators, which let your own classes implement the Map interface and be converted to JSON by the built-in JSON library.

8.1. Working with collections of data

Much of software development is concerned with moving and processing collections of data in a type-safe manner. In the example authentication service from chapter 7, the user Alice provided her logon details to an instance of an AuthService, which returned a User object. In this section, you’ll expand on this example by retrieving user permissions for two roles: Reader and Administrator. When Alice logs on to the company blogging application to write a news item, the system needs to be able to identify that, as an Administrator, she can create and edit posts. Other users are Readers and can only read, comment on, and share blog posts. You’ll use the class hierarchy shown in figure 8.1 to achieve this.

Figure 8.1. Class hierarchy for example roles and permissions

When Alice logs on, the system will retrieve a list of Permissions, which will contain a mixture of ReaderPermission instances and AdminPermission instances that the running application assigns to Alice’s User object. The company blog app can use these permissions to allow access to part of the system, as shown in figure 8.2.

Figure 8.2. Alice’s User object is assigned permissions by the system.

The permissions will be constant instances of the ReaderPermission and AdminPermission classes. We discussed const classes in chapter 5; to recap, they have a const constructor that must fully initialize all fields at compile time. When you create an instance of a const class by using the const keyword (instead of the new keyword), you can be sure that when you compare two instances that have the same field values, they’re considered identical. You can use this feature to determine whether a user has a specific permission.

You need to create each of the permissions and assign it to a static variable in each class so you can refer to these variables throughout the application (static variables were also discussed in chapter 5). The following listing shows the boilerplate code to get your permissions working.

Listing 8.1. Permissions boilerplate code

Now that you have the boilerplate code containing two types of permissions (Admin and Reader), it’s time to look at how you can use it.

8.1.1. Collections of objects

Alice is an instance of a User object, and generally a User has many permissions. In other words, you want Alice to have a list of permissions associated with her. Other programming languages include the concept of an Array, which is used to store lists of items. Dart, on the other hand, doesn’t have a specific Array type; instead, it has Collection and List interfaces. The List interface extends the Collection interface. It has a default implementation, so you can use the new keyword with it, such as to create a specific instance of a list:

Collection permissionsCollection = new List();
List permissionsList = new List();

 

Using the as keyword

In some instances you want to treat a specific variable as an instance of another type. A classic case of this is where you have a Collection variable, but you’re instantiating it as a list. This is valid, but no add() method is defined on the base Collection interface.

In order to use the Collection typed variable as a list, you need to use the as keyword. This lets the type checker know that you intend for the Collection variable to contain a List instance, which lets you use the variable as a list:

(permissionsCollection as List).add(... some permission ...);

 

We’ll discuss the specific differences between the Collection and List interfaces a little later in the chapter. For now, add an extra field to your User from chapter 7 that will contain a collection of permissions, as shown in figure 8.3. When Alice logs on, her permissions are added to the collection.

Figure 8.3. You’ll add a collection of permissions to the User class. When Alice logs on, she’ll have permissions added to that collection.

Methods on the Collection Interface

The Collection interface provides a number of generalized methods and properties that you might want to use with any collection of objects, such as retrieving the length of the collection, determining whether it’s empty, and returning a subset of elements in the collection that match some condition. This ability can be useful, for example, to return a list of Admin permissions.

Listing 8.2 provides a function that returns a subset of Admin permissions from a collection by using the is keyword against each item in the collection to determine whether that item is an instance of AdminPermission. The filter() function of the Collection interface takes another anonymous function as a parameter, which is called for each permission in the collection. If the anonymous function returns true, then that permission is added to the result.

 

Tip

An anonymous function that’s called for each element in the collection and returns true or false is known as a predicate.

 

Listing 8.2. Returning a list of AdminPermissions

When you pass a User object containing Alice’s user into this extractAdminPermissions() function, you get back a new Collection object containing all the AdminPermission instances.

Another useful function of the Collection interface is the some() function, which you can use to determine whether a specific permission is contained in the collection. The some() function takes a predicate and returns true if the predicate returns true for an item in the collection. For example, to determine whether Alice has the ALLOW_CREATE permission, you can call some() and store the result in a containsAllowCreate variable:

Iterating a Collection

The Collection interface implements the Iterable interface, which allows you to use the Dart for( in ) keywords. Similar to foreach( in ) in C# and for( : ) in Java, Dart shares this syntax with JavaScript. This language construct allows you to iterate through the collection, assigning each item to a variable in turn, as shown in figure 8.4.

Figure 8.4. The for (... in ...) keywords allow you to iterate through each item in a list.

The for ( in ) pair of keywords constitute a powerful way to visit every item in the list. If you need to break out of the loop, perhaps because you’re looking for the first matching permission, you can use the keyword break, which exits the loop at that point, as shown in the following snippet:

Manually Iterating Through a Collection

The Iterable interface also returns an Iterator interface. The Iterator interface is used internally by the for (...in...) keywords, but you can use its hasNext and next() methods explicitly in your code to control when you move through a collection of items outside of an explicit loop. hasNext returns true if there’s another item to move to; next() returns the next item and moves the iterator pointer past the next item. If it has already returned the last item, it throws a StateError. The example in the following listing extracts the first two items from the collection by explicitly moving through it. Whenever you call the iterator() function on a collection, you get a new iterator that has the iterator’s pointer positioned before the first item.

Listing 8.3. Extracting the first two items from a collection using an iterator

8.1.2. Using the concrete implementations of the Collection interface

The Collection interface, combined with the Iterable and Iterator interfaces, is a powerful construct, but you can’t create an instance of a Collection. An interface, remember, is a way of describing the methods and properties that should appear on a class that implements that interface. The core Dart libraries provide a number of implementation classes for the Collection interface: List, Queue, and Set. These classes provide different specializations of the Collection interface. Figure 8.5 shows the Collection interface and how it’s related to its children (List, Queue, and Set) and its parent (Iterable).

Figure 8.5. Collection is the core interface in Dart for dealing with collections of objects, but you need to use a concrete instance of List, Queue, or Set.

Dart doesn’t have an Array type, but as mentioned earlier, it does have List. Lists are dual purpose, being either fixed-size or extendable. If a list is fixed-size, you can insert elements only into predefined slots by using the square bracket index [] operator. An extendable list, on the other hand, can also have items added to and removed from it, effectively adding and removing slots in the list.

Creating Lists

There are four ways to create a list, shown in listing 8.4. The simplest way is to create a literal list with the square bracket operator, which produces a prepopulated, extendable list. The second approach uses the List() constructor. If you don’t pass in a value, you get an empty, extendable list. If you do pass in a value, you get a fixed-size list containing the number of elements you specify, each of which is null. You must access each element using a numeric indexer that’s zero-based.

Finally, List defines another List.from(...) constructor that allows you to create a new extendable list from any class that implements Iterable. This feature is useful for converting an existing fixed-size list into an extendable list.

Listing 8.4. Different ways to create a list

All elements in a list can be read from and written to by using the index operators [] and []=. The [] operator lets you read a value out of a list at a specific (zero-based) index, such as var permission = growable[2]; the []= operator allows you to modify the value at a specific index, as in growable[2] = permission. These index operators are important; they crop up again when we look at maps, and you’ll use them in your own classes later in the chapter when we examine operator overloading. They also allow you one final way to iterate through a list, in addition to the two methods defined on the Collection interface (a List “is-a” Collection, after all). You can use the indexer to access each item in turn in a for loop. The for loop syntax is identical to the for loop in all C-based languages, such as Java, JavaScript, and C#:

for (int i = 0; i < fixedSize.length; i++) {
  Permission permission = fixedSize[i];
}

The other two built in types of collection, Queue and Set, don’t provide direct access to elements, but they can be converted into lists by using the List.from() constructor.

Creating Sets

A set is a specific type of collection that doesn’t allow duplicates. It has some specific methods, such as isSubsetOf(collection) and contains(value), and it has a Set.from() constructor, which means you can create an instance of a Set from any other Iterable class. The following snippet creates a set containing only one item:

Creating Queues

Queues, on the other hand, are useful when you want to build a first-in, first-out collection, such as when one part of a process adds items onto a list and another part of the process wants to take an item off the list, preserving the order in which they were added. The methods addLast() and removeFirst() let you add an item to the back of the queue and remove an item from the front of the queue. The following snippet creates a queue, adds one item to the back of the queue, and removes it from the front of the queue:

You can’t access elements in sets and queues directly by an index, as you can with lists. Instead, you can access elements only by iterating through the queue using the methods provided on the Collection interface or, in the case of a queue, with the additional addLast() and removeFirst() methods.

8.1.3. Making collections specific with generics

One of the problems with using the Collection interface the way you’re currently using it is that you can add any instance of an object to it. Thus it’s possible for you to end up with a list that contains a mixture of ReaderPermission and String, or int, for instance, by writing code such as this:

You can see that code like this is wrong or that the list should be a list of Permissions rather than just a list of anything. One way to fix this might be for you to inherit List, create a PermissionList, and provide methods such as add(Permission item) that take only a Permission object. But a typical application has lots of lists containing many different types that don’t share a common base class other than Object. It would be impractical (and a waste of code) to write a different list implementation for each list you might use.

Fortunately, Dart has generics built into the type system. A generic type is a way of defining a class in a general, or generic, way but allowing the user of the class to use it in a specific way, even though there’s no shared type hierarchy such as a list of Permissions, a list of Strings, and a list of ints.

All the types in the Collection hierarchy have been written using generics, which allows you to use them specifically for your Permission class. In other words, you can create a list of Permissions, and Dart’s type checker can then flag errors such as accidentally adding a String into your list.

Classes built using generics take the form Name<T>, where Name is the name of the class or interface and T is a comma-separated list of type placeholders. The collection classes that you’ve seen so far have generic signatures, as shown in figure 8.6, where E represents a placeholder for the Element type.

Figure 8.6. The Collection classes are generic types that contain a placeholder type E that you can replace with your own type.

Replacing the Placeholder Types

To use this generic type placeholder, specify the type you want to use in the type declaration and the constructor. For example, when Alice logs on, you know you’re getting a list of permissions, which is now declared as follows:

In use, all the methods on the list now work as though you had written a custom PermissionList that could hold only instances of Permission objects. The collection methods’ signatures all expect to receive a Permission, receive a list of Permissions, or return a Permission. Table 8.1 lists some of the method signatures defined on the List and shows what the Dart type checker expects when you use the Permission class in the constructor definition.

Table 8.1. A comparison of actual method signatures and how the Dart type checker interprets them

Actual method signature

What Dart expects in use

void add(E value); void add(Permission value);
E next(); Permission next();
Collection<E> filter(
bool f(E element)
);
Collection<Permission> filter (
bool f(Permission element)
);

This behavior is equally valid for any other class. For example, you can create a list of Strings or a queue of ints by using the type to replace the generic placeholder in the constructor:

List<String> stringList = new List<String>();
stringList.add("I am a string");
Queue<int> intQueue = new Queue<int>();
intQueue.addLast(12345);

In use, if you tried to add an int into a String list, the type system would warn you in the same way as if you tried to pass an int into a function that expected a String, because the List add() method is now expecting a String to be passed in.

There’s another positive side effect of using generic types: they allow you to perform type checking on method signatures that you define yourself. You may have a function called extractAdminPermissions() that takes a list of Permissions and returns a list of AdminPermissions. This method functions identically regardless of whether it’s using generic type placeholders:

With the first version of extractAdminPermissions(), you can pass in any collection, including a collection of String or a collection of int, and the type checker won’t know to warn you of a possible error. The second version of extractAdminPermissions() knows that you expect to receive a collection of Permission, and your calling code knows that it’s expecting a list of AdminPermissions to be returned. The calling code can be safe in the knowledge that the resulting collection from extractAdminPermissions() won’t contain any ReaderPermission instances.

So What “is-a” List?

In the previous two chapters, you’ve been using the is keyword to determine whether a variable “is-an” instance of a specific type. For example, AdminPermission, which extends Permission, “is-a” Permission, but AdminPermission isn’t a ReaderPermission. The following snippet returns the results you expect:

When you’re using generics, it turns out the same principle applies. A list of AdminPermissions “is-a” list of Permissions, because every item in the list “is-a” Permission. It isn’t a list of ReaderPermissions, which correctly follows the same logic:

This code works as expected, but a good question would be why the type checker returns true for adminList is List. Logically, any list of anything “is-a” List, but how does Dart achieve this when no type is specified? Behind the scenes, using an untyped generic is the same as using the dynamic type. The dynamic type, which we discussed in chapter 7, represents the untyped version of every class, and every instance of a class “is-a” dynamic.

Thus, all lists of any type or no type are also always lists of dynamic:

One final question arises, specifically with literal lists, because they have a specific syntax that Dart can use to create a list of known values (as you’ve seen). How do you define a list of known values to be a strongly typed list? Fortunately, you can do this using the generic syntax, providing <type name>[element, element, ...], as in

var permissions =
<Permission>[AdminPermission.ALLOW_CREATE, AdminPermission.ALLOW_EDIT];

8.1.4. Storing lists of key/value pairs with generic maps

The final built-in generic type that we’ll look at in this chapter is Map<K,V>. You can use it to store lists of key/value pairs; you access the values using a key as an indexer. This is similar to the way you access values in a list by using a numeric indexer, such as myList[1], except that you can specify a nonnumeric indexer (typically a String). The generic type placeholders K and V represent the Key type and the Value type, respectively—they don’t need to be the same, and as with the other generic types we’ve looked at previously, they can be ignored (in which case you get a Map<dynamic, dynamic>).

When Alice logs on to the system, you can retrieve her User object. You can do the same for Bob and Charlie, but perhaps retrieving the User object from an enterprise system is a time-consuming exercise. You can implement a simple cache by storing a list of username strings and their associated user objects in a map. Because the Map interface has a default implementation class, you can create and manipulate the map as shown in the following listing.

Listing 8.5. Creating and using a map of String and User

Accessing a nonexistent key such as charlieKey doesn’t throw an exception, as it does in other languages. Instead, it returns null.

Creating Predefined Map Literals

You can create maps just like lists, with a predefined list of known keys and values. The map literal uses {} to define the map, and it contains a comma-separated list of keys and values, as shown in figure 8.7.

Figure 8.7. A map can be defined in Dart as a list of key/value pairs.

A typical use when dealing with JavaScript Object Notation (JSON) data is a list of key/ value pairs defined in this format. The dart:json library provides two methods for converting a string into a map and a map into a string. The code in the following listing converts a string of usernames and their last logon date into a map and back to a string.

Listing 8.6. Using the dart:json library to convert between maps and strings

 

Tip

Converting JSON strings into maps and maps into JSON strings is common in web apps when you’re sending and receiving data between a client app and a server-side web service. We’ll deal with this in chapter 11 in more depth.

 

Accessing the Key and Value Collections

This raises another question. Suppose that you’ve stored charlieKey but the value is indeed null. You can’t use this information to determine whether charlieKey is a valid key or perhaps a typo.

Fortunately, the Map interface provides containsKey(K key) and containsValue(V value) methods that you can use to confirm whether a key or value exists in the map. You can also access a collection of keys and iterate through them to access the values, as in the following snippet:

Inserting New Items Into the Map

Using the indexer [ ] = operator, you can insert items into the map (as you’ve done in the previous example), but this has the effect of overwriting an existing value with the same key. Sometimes it’s useful to only insert new items into the map rather than replace existing items; for example, you could store a list of logon dates for each user, such as Map<String, List<Date>>, specifying that for each Username string, you’ll access a list of Date objects.

When Bob logs on (and he has already logged on in the past), you want to add the logon date to the existing list of dates. When Charlie logs on, you want to create a new list of dates. Figure 8.8 demonstrates this.

Figure 8.8. You want to create a new value only if the key doesn’t already exist.

The problem with using the indexer in the form userLogons["bobKey"] = new DateList(); is that doing so will always create a new, empty list for Bob, wiping out his existing logons. Likewise, using userLogons["charlieKey"].add(new Date.now()); for Charlie will cause a null-pointer exception, because there’s no List<Date> to add() a new date to.

Although you could use the containsKey("bob") check, the Map interface provides an alternative in the form of the putIfAbsent() method. This method adds a new item into the list only if the key doesn’t already exist. It takes a key and a function as its parameters, and the return value from the function is inserted into the map as a value if the key doesn’t yet exist in the list of keys. The following listing shows how this method works.

Listing 8.7. Using the Map putIfAbsent() method

This approach lets you access the properties on the object that represents the value in the map without replacing the object.

That was a long section, but generics and collections are powerful features of Dart and (like superheroes) become even more powerful when combined. Generics aren’t restricted to being used with collections, though. In the next section, we’ll look at how to create your own “generic” classes that let you use type placeholders in your class but let users of your class create their own strongly typed versions.

 

Remember

  • Collections can be created using the concrete instances of List, Queue, and Set.
  • Dart has no Array type, but you can use List in its place. A list can be fixed or dynamically expandable.
  • All the collection types are generic, and you can specify the type of a collection by using the <T> syntax with the type constructor, such as new List<String>() to create a list of Strings.
  • Lists can be accessed using zero-based indexers such as myList[2];, which accesses the third item in the list.
  • Maps contain a list of key/value pairs and also use the indexer syntax, but they take the key as the indexer. For example, putting a Date into a Map<String,Date> could look like this:
    myMap["aliceKey"] = new Date.now();

 

8.2. Building your own generic classes

You’ve seen that generics can be useful to provide strong typing for classes where there’s no shared base class. This is good news, because your bosses have seen your User classes and code and decided that they want it ported over to a different system for managing timesheets that uses a preexisting Role class rather than the Permission class for each user. Ideally, when Alice logs on to the blog post system, her User object will contain a list of Permissions, but when she logs onto the timesheet system, her User object will contain a list of Roles.

It turns out that roles and permissions are synonymous, but the classes are different. You rename the User.permissions list to be User.credentials and then start to think about how you could use strong typing to tell the difference between a list of Roles and a list of Permissions.

The first thought is to subclass User and have RolesUser and PermissionsUser, each with its own add() method and so on, as shown in figure 8.9. Fortunately, before going down this route, you remember generics and decide that this situation would be a perfect fit for the creation of a generic user class.

Figure 8.9. If you find yourself creating several similar classes that use slightly different objects but in the same way, then you could have a case for using generics.

8.2.1. Defining a generic class

Defining a class as a generic class involves using a generic type placeholder in the class declaration, such as class User<C> { ... }. It’s good practice to use a single letter as the type placeholder, because this convention is easily identifiable as a generic type placeholder, but you can use any value. You could use class User<Credential> { ... }, but this looks less like a placeholder and more like a real type. Common generic type placeholder letters that are used by convention are shown in table 8.2.

Table 8.2. Some type placeholder letters are used by convention.

Generic type placeholder

Common meaning

<T> Type
<E> Element
<K> Key
<V> Value

You don’t have to use these letters, and you could use them to mean something else, but please make sure you have a very good reason to do so, such as when the problem domain contains specific jargon. For example, the letter E, which typically refers to an Element, might instead logically refer to an edge in a graph structure.

Once you’ve defined your class name and the generic type placeholder <C>, you can reference that generic type placeholder throughout your class in method property definitions, method parameter lists, and return types.

The following listing shows how the User class might look now that you have a generic type placeholder rather than specific RolesUser and PermissionsUser classes.

Listing 8.8. User class that uses generic credentials

8.2.2. Using your custom generic class

Now that you have a generic class, you can use it in the same way as any other generic type. You can be sure the type checker will catch type errors (such as if you missed some code when porting to the timesheet system and are still trying to retrieve a Permission when you really mean a Role).

You’ve also opened the possibility of reusing the User class in different scenarios that you might not have first envisaged. You can now reuse User when the credentials are supplied as string or integer values. The following listing shows some ways you can use your new class in a type-safe manner.

Listing 8.9. Using your generic User class in a type-safe manner

8.2.3. Restricting the types that can be used as placeholders

Unfortunately, other developers think your new generic User class is great, and they’re using it all the time in scenarios where you weren’t expecting it to be used, such as storing the types of soft drinks users get from a vending machine: User<SoftDrink>. Your boss has started to notice and thinks people are using it as a shortcut to writing their own code that would better fit their solution. Your boss would like you to tighten it up and has given you permission to add a validate() method to the Role and Permission classes and have them both implement a CredentialsValidator interface.

The new rule is that developers can use your generic User class with any type, as long as that type implements the CredentialsValidator interface. Fortunately, generic typing allows you to implement this rule using the extends keyword in the generic definition. Change your class definition so that it reads

class User<C extends CredentialsValidator> { ... }

Now, wherever you try to use the User class, it must be used in conjunction with a class that implements or extends CredentialsValidator (which rules out String, int, and SoftDrink). It also means you can call the validate() method in the addCredential() function, as shown in the following example, without needing to check whether the class has a validate() method (as you’d do if you were still accepting Strings and SoftDrinks):

Well done! You’ve made it through the section on generics, which is an advanced topic in any language. Generics are a powerful feature of many modern class-based languages, and the principles here are very similar to those of Java and C#.

 

Remember

  • If you find yourself making a number of nearly identical classes, then you might want to think about using generics.
  • The generic type placeholder is used throughout the class to represent the generic type that will be specified by the class’s user.
  • You can restrict generic type placeholders by using the extends keyword.

 

In the next section, we’ll look at operator loading, such as providing your own equals == implementation and adding custom indexers to your classes so that users of those classes can access values by using Dart’s indexer syntax, [] and []=.

8.3. Operator overloading

When Alice logs on to the timesheet example app discussed in the previous section, the system retrieves the Roles that represent the way Alice might use the timesheet system. For example, Alice might be a timesheet user, meaning she enters her own time into the system. She might also be a timesheet reporter, meaning she can produce reports based on other people’s timesheets. Finally, she could be a timesheet administrator, meaning she can also edit any timesheet in the system.

Each of these three roles encompasses all the abilities of the previous role, such that the timesheet app needs to know only the role with the greatest access level in order to function correctly. If Alice has the TIMESHEET_ADMIN role, then she also has the abilities of the TIMESHEET_REPORTER and TIMESHEET_USER roles. You can order these roles by access-level value, as shown in figure 8.10.

Figure 8.10. Example of the levels of access that Alice could have in the timesheet app

8.3.1. Overloading comparison operators

There’s a natural ordering to these roles: one is greater or lesser than the other. To test two roles’ relation to each other, you can write code that compares each role’s accessLevel value, which works adequately. But it would aid readability if you could compare the role instances with each other directly, using the greater-than (>) and less-than (<) operators, as shown in the following listing.

Listing 8.10. Ways to compare roles

Fortunately, Dart allows this functionality with operator overloading, which means you can take the standard operators and let your own classes provide meaning for them. In this instance, you want to provide meaning for the greater-than and less-than operators in the context of the Role class. Dart lets you do this in the same way you created a new version of the toString() method in chapter 7, by providing your own version of the operators’ implementation. The operator keyword lets Dart know that your class is defining an operator implementation, as shown in the next listing.

Listing 8.11. Providing implementations of < and > with the operator keyword

When you overload an operator, provide a method containing your implementation of the operator. The operator’s method usually takes a single parameter containing another instance of the same class. Table 8.3 shows some common comparison operators that you can overload.

Table 8.3. Some common comparison operators

Operator method

Description

bool operator >(var other) {...} This instance is greater than the other.
bool operator <(var other) {...} This instance is less than the other.
bool operator >=(var other) {...} This instance is greater than or equal to the other.
bool operator <=(var other) {...} This instance is less than or equal to the other.
bool operator equals(var other) {...}
bool operator ==(var other) {...}
This instance is equal to the other. Note that there are two different versions of this method. At the time of writing, the language spec defines the word equals as the operator, but the implementations are currently using a double equal sign == to represent the equals operator.

8.3.2. Surprising use for operator overloading

When you’re overloading operators, the other value should be the same class, but there’s no requirement that it must be the same class. This situation provides for some interesting, if slightly unorthodox, syntax. For example, to add a role to a user, you could overload the Users + operator, allowing you to write the code shown in the following listing.

Listing 8.12. Overloading the addition operator to add Roles to a User

 

Warning

It’s good practice to overload operators only when it would be unsurprising to the reader to do so. The previous example would be more readable if it provided an add(Role) method instead. Developers don’t like surprises.

 

8.3.3. Overloading indexer operators

When you were dealing with lists and maps earlier in the chapter, you used the indexer operators to write []= and read [] a value in an instance of a class, such as

The [] operator allows you to read a value by index. []= allows you to write a value by index. And you can overload these in your classes to provide indexer access to underlying values. The [] operator method takes a single index parameter and returns a value, and []= takes both an index parameter and a value parameter that should be applied to that index item. Imagine a User class that could only have exactly two roles. You could use an indexer to allow reading and writing to those two roles. The following listing uses indexers to access the underlying _role1 and _role2 properties.

Listing 8.13. Overloading the indexer operators

A common reason to use indexers is to have a class implement a Map interface so that properties on the class can be read as though they were part of a map, when they actually form real properties. This method allows tools such as the JSON parser, which understands maps and lists, to convert your class into a JSON representation. When data is in a JSON format, it can be sent back and forth over the web. You can make your Role class implement a Map and convert it to JSON using the code shown in listing 8.14. Although the code has snipped some of the boilerplate methods required by the Map interface, you must provide all of them. Listing 8.14 also uses some of the other patterns you’ve seen in this chapter, such as returning list literals and returning typed and untyped generic collections.

Listing 8.14. Letting a class implement Map so it can be converted to JSON

Now that you’ve implemented Map in your Role class, you can use the JSON.stringify() method (defined in the dart:json library) to convert an instance of a role into a string, as in the following snippet:

Role adminRole = new Role("TIMESHEET_ADMIN",3);
var roleString = JSON.stringify(adminRole);

You can use this serialized string to send the Role data over the web (which we’ll explore in part 3, later in the book).

 

Remember

  • Use the operator keyword in conjunction with the operator symbol to provide a new method in your class to overload the operator.
  • Ensure that you overload operators only where doing so will aid readability of the code.
  • You can overload indexer operators to allow map-like access to properties of your class.
  • The dart:json library can convert classes that implement the Map interface into JSON strings.

 

8.4. Summary

In this chapter, we’ve taken a long look at manipulating collections of data and shown you the relationship between the Collection interface; some concrete implementations of collections in the form of List, Queue, and Set; and some of the methods exposed on the collection, such as forEach() and filter().

We also looked at the Map interface, which you can use to store key/value pairs of data, and you saw that the built-in JSON library can be used to convert strings into maps and back again.

By using your classes in place of a type placeholder, the generic collection classes can work in a type-safe manner, effectively giving you a “list of” your own class—for example, a list of Strings or a list of Users. You’ve seen how to create your own generic classes; you should try to create a generic class if you find yourself making a number of nearly identical classes that differ only by the method parameters and return types.

Finally, we looked at operator overloading, which allows you to aid readability when using your classes by providing your own versions of common operator symbols such as > (greater than) and < (less than). The culmination of operator overloading was to use the indexer operators [] and []= to provide your own implementation of the Map interface, which allows your class to be converted to JSON by the built-in JSON library.

In the next chapter, we’ll examine functions in depth. You’ll see how to use function callbacks and future values to achieve readable and performing asynchronous code.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.104.95