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.

  1. Why use generics?
  2. 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:


    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.


  3. Using collection literals
  4. 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>{123};

    var steps = <intString>{1"dart"2"flutter"3"apps"};



  5. Using parameterized types with constructors
  6. 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 = [14513466];

    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<intPoint>();


  7. Generic collections and the types they contain
  8. 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


  9. Restricting the parameterized type
  10. 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