Description:

This is the seventh part of the Dart Grammar article on type systems and generics, which we set up with a translation of nullable and non-nullable types in Dart. In fact, the type system in Dart is not strict enough for historical reasons. Dart was originally conceived as a dynamic language like javascript. Dynamic languages have a looser type system, so dart types are optional. However, a loose dynamic language type system is not a good thing for developers. Once the program logic is complex, loose types can become messy and painful to analyze, but static type checking can quickly locate problems at compile time.

In fact, the DART type system is not strict enough, not only in terms of optional types and not yet separating nullable and non-nullable types, but also in terms of generic type safety in DART, which I’ll do by comparing Kotlin with generics in DART. You’ll find that Dart and Kotlin generic security go completely different ways, and Dart generic security is unreliable, but you’ll also find that Dart 2.0 has greatly improved this area.

One, optional type

The types in Dart are actually optional, meaning that function types, parameter types, and variable types can be omitted.

sum(a, b, c, d) {// Function parameter types and return value types can be omitted
  return a + b + c + d;
}

main() {
  print('${sum(10.12.14.12)}');// It is running properly
}
Copy the code

The sum function has neither a return type nor a parameter type. Some people might wonder what happens if the sum function takes a String as its last parameter.

The answer is: the static type checking analysis is fine but the compilation run is not.

sum(a, b, c, d) {
  return a + b + c + d;
}

main() {
  print('${sum(10.12.14."12312")}');Static check The type check is normal, but the operation is abnormal
}

// Run the result
Unhandled exception:
type 'String' is not a subtype of type 'num' of 'other' // Keep this subtype mismatch problem in mind, as we will analyze the subtype in more detail later and see this exception frequently in Dart and Flutter development.

Process finished with exit code 255
Copy the code

While the optional type makes the entire code concise and dynamic on one hand, it makes static checking types difficult to parse on the other. But it also removes the type-based function overloading feature from DART. We all know that function overloading is a common syntactic feature in static languages, but it is not supported in DART. For example, in other languages we use constructor reloads for scenarios where objects are constructed in multiple ways. Dart does not support constructor reloads, so dart introduces the concept of named constructors to address this problem. So why does the optional type syntax feature conflict with function overloading?

We can use contradiction and assume that Dart supports function overloading, so we might have the following code:

class IllegalCode {
  overloaded(num data) {

  }
  overloaded(List data){// Assume that function overloading is supported, which is actually illegal

  }
}

main() {
    var data1 = 100; 
    var data2 = ["100"];
    // Because the type in DART is optional, there is no way to tell which overloaded function the code below actually calls.
    overloaded(data1);
    overloaded(data2);
}
Copy the code

On a personal note, dart actually supports type-based function overloading now, if only in terms of optional types, because DART has type derivation. If DART can derive the datA1 and datA2 types described above, then the overloaded functions can be matched based on the derived types. That’s what Kotlin did. Take Kotlin for example:

fun overloaded(data: Int) {
    //....
}

fun overloaded(data: List<String>) {
   //....
}

fun main(args: Array<String>) {
    val data1 = 100 // Here Kotlin also uses type derivation to Int
    val data2 = listOf("100")// Kotlin also uses type derivation as List
      
    // So the following overloaded function calls make sense in Kotlin
    overloaded(data1)
    overloaded(data2)
}
Copy the code

In fact, Dart has the ability to support function overloading, as mentioned on Github. For details, please refer to the dartlang issue: github.com/dart-lang/s…

But why doesn’t DART support function overloading? In fact, not lack of ability to support, but not necessary. In fact, many modern languages like GO and Rust have no function overloading. Kotlin also recommends using default parameters instead of function overloading. For those interested, check out my previous article juejin.cn/post/684490… . However, dart also supports default parameters, so function overloading can be confusing, such as the seven or eight constructor overloading in Java’s Thread class. Specific reference to this discussion: groups.google.com/a/dartlang….

2. Interface type

There is no way to declare an interface directly in Dart. There is no keyword like interface to declare an interface. Instead, it is implicitly introduced through a class declaration. So each class has an interface with an implicit name, and the type in DART is the interface type.

// Define an abstract class Person that is also an implicit Person interface
abstract class Person {
  final String name;
  final int age;

  Person(this.name, this.age);

  get description => "My name is $name, age is $age";
}

// Define a Student class that implements the Person interface using the implements keyword
class Student implements Person {
  @override
  // TODO: implement age
  int get age => null;// Rewrite the age getter function. Since it is final in the Person interface, it has only getter accessors. As an interface implementation, you need to rewrite all of its functions, including its getter or setter methods.

  @override
  // TODO: implement description
  get description => null;// Override to define the description method

  @override
  // TODO: implement name
  String get name => null;// Override the name getter function. Since it is final in the Person interface, it only has getter accessors. As an interface implementation, you need to override all of its functions, including its getter or setter methods.
}

// Define a Student2 class that extends the Person abstract class using the extends keyword
class Student2 extends Person {
  Student2(String name, int age) : super(name, age);// Call the constructor in the parent class

  @override
  get description => "Student: The ${super.description}";// Override the description method in the parent class
}
Copy the code

Third, the generic

1. Basic introduction to generics

Generics in Dart are similar to other languages, but types in Dart are optional and can be qualified using generics; Using generics can reduce a lot of template code.

Here’s an example:

// This is a PrintMsg that prints an int MSG
class PrintMsg {
  int _msg;

  set msg(int msg) {
    this._msg = msg;
  }

  void printMsg() {
    print(_msg); }}// Now we need MSGS that support String, double, and even other custom classes
class Msg {
  @override
  String toString() {
    return "This is Msg"; }}class PrintMsg {
  int _intMsg;
  String _stringMsg;
  double _doubleMsg;
  Msg _msg;

  set intMsg(int msg) {
    this._intMsg = msg;
  }

  set stringMsg(String msg) {
    this._stringMsg = msg;
  }

  set doubleMsg(double msg) {
    this._doubleMsg = msg;
  }

  set msg(Msg msg) {
    this._msg = msg;
  }

  void printIntMsg() {
    print(_intMsg);
  }

  void printStringMsg() {
    print(_stringMsg);
  }

  void printDoubleMsg() {
    print(_doubleMsg);
  }

  void printMsg() {
    print(_msg); }}// But with generics, we can make this code much simpler:
class PrintMsg<T> {
  T _msg;

  set msg(T msg) {
    this._msg = msg;
  }
  
  void printMsg() {
    print(_msg); }}Copy the code

The actual generic parameter types can be specified or omitted in Dart. The omission is essentially the same as specifying that the generic parameter is of type dynamic.

class Test {
  List<int> nums = [1.2.3.4];
  Map<String.int> maps = {'a': 1.'b': 2.'c': 3.'d': 4};

// The above definition can be shortened to the following form, but this form is not recommended and is used only when necessary and appropriate
  List nums = [1.2.3.4];
  Map maps = {'a': 1.'b': 2.'c': 3.'d': 4};

// The above definition is equivalent to the following form
  List<dynamic> nums = [1.2.3.4];
  Map<dynamic.dynamic> maps = {'a': 1.'b': 2.'c': 3.'d': 4};
}
Copy the code

2. Use of generics

  • Use of class generics

    // It is easy to define the generics of a class, just add: 
            
              after the class name; If you need more than one generic type parameter, you can append it in Angle brackets, separated by commas
            
    class List<T> {
      T element;
    
      void add(T element) {
        / /...}}Copy the code
  • Use of function generics

    // Define function generics
    void add(T elememt) {// Function parameter types are generic types
        / /...
    }
    
    T elementAt(int index) {// Function parameter return value type is generic type
        / /...
    }
    
    E transform(R data) {// Function parameter types and function parameter return value types are generic types
       / /...
    }
    Copy the code
  • The use of collection generics

    var list = <int> [1.2.3];
    // Equivalent to the following form
    List<int> list = [1.2.3];
    
    var map = <String.int> {'a':1.'b':2.'c':3};
    // Equivalent to the following form
    Map<String.int> map = {'a':1.'b':2.'c':3};
    Copy the code
  • The upper bound of a generic type

    As in Java, generic upper bounds can be implemented using the extends keyword
    class List<T extends num> {
     T element;
     void add(T element) {
     / /...}}Copy the code

3. Subclasses, subtypes, and subtyping relationships

  • Generic and non-generic classes

Classes in Dart can be divided into two broad categories: generic classes and non-generic classes

Non-generic classes are the most common classes in development. When a generic class defines a variable, its class is actually the type of the variable. For example, if we define a Student class, we get a Student type

Generics are more complex than non-generic classes, in fact, one generic class can correspond to an infinite number of types. It’s easy to see why. As we know from the previous article, we define generic parameters when we define a generic class. To get a valid generic type, we need to replace the type parameters in the definition with the concrete type arguments in the external use place. We know that in Dart List is a class, it’s not a type. An infinite number of generic types can be derived from it. For example, List

, List

, List >, List

>




  • What is a subtype

We may often encounter errors with subtype subtypes in Flutter development: type ‘String’ is not a subtype of type ‘num’ of ‘other’. What exactly is a subtype? Is it the same thing as subclasses?

Firstly, a mathematical induction formula is given:

If G is A generic class with n type parameters and A[I] is A subtype of B[I] and belongs to 1.. Range of n, then can be expressed as G<A[1]… ,A[n]> * G<B[1],… B[n]>, where A * B can indicate that A is A subtype of B.

The Dart subtype concept is very similar to the Kotlin neutron type concept.

A subclass is a derived class that inherits its parent class (also known as the base class). Example: Class Student extends Person{//… }, Student is generally called a subclass of Person.

Subtypes are different. We know from above that a class can have many types, so subtypes are not just as strict as subtypes. The general rule for subtype definition is that whenever A value of type A is needed, it can be replaced with A value of type B, and then type B can be said to be A subtype of type A or A supertype of type B. It is obvious that rules for subtypes are looser than rules for subtypes. So we can analyze the following examples:

Note that a type is also a subtype of its own, so it is obvious that String must be replaceable anywhere a value of String appears. Subclass relationships are also subtype relationships in general. Values of type double certainly do not replace values of type int, so they do not have a subtype relationship.

  • Subtyping relationships:

If A value of type A occurs anywhere and anytime and can be replaced by A value of type B, which is A subtype of type A, then the mapping substitution between type B and type A is A subtyping relationship

4. Covariant

When it comes to covariant, maybe we have another word for contravariant. In fact, dart1. x supports both covariant and contravariant versions, but dart2. x only supports covariant versions. With the concept of subtyping relations, then covariation is better understood. Covariation is actually preserving the subtyping relations. First of all, we need to clarify who is preserving the subtyping relations mentioned here for?

For example, int is a subtype of num. Since all generic classes in Dart are covariant by default, List

is a subtype of List

.

Let’s look at an example:

class Fruit {
  final String color;

  Fruit(this.color);
}

class Apple extends Fruit {
  Apple() : super("red");
}

class Orange extends Fruit {
  Orange() : super("orange");
}

void printColors(List<Fruit> fruits) {
  for (var fruit in fruits) {
    print('${fruit.color}');
  }
}

main() {
  List<Apple> apples = <Apple>[];
  apples.add(Apple());
  printColors(apples);//Apple is a Fruit subtype, so List
      
        is a List
       
         subtype.
       
      
  // So the printColors function accepts a List
      
        type and can use a List
       
         type instead
       
      
  List<Orange> oranges = <Orange>[];
  oranges.add(Orange());
  printColors(oranges);/ / in the same way

  List<Fruit> fruits = <Fruit>[];
  fruits.add(Fruit('purple'));
  printColors(fruits);//Fruit itself is a subtype of Fruit, so List
      
        must be a subtype of List
       
      
}
Copy the code

5. Application of covariation in Dart

In fact, covariant is used by default for generic types in Dart and is actually used for another scenario for covariant method parameter types. If you’re a little confused by the technical terms, here’s an example:

// Define the base animal class
class Animal {
  final String color;

  Animal(this.color);
}

// define Cat to inherit from Animal
class Cat extends Animal {
  Cat() : super('black cat');
}

// define Dog to inherit from Animal
class Dog extends Animal {
  Dog() : super('white dog');
}

// Define a cage class for animals
class AnimalCage {
  void putAnimal(Animal animal) {
    print('putAnimal: ${animal.color}'); }}// Define a cat cage class
class CatCage extends AnimalCage {
  @override
  void putAnimal(Animal animal) {// Note that the method argument here is of type Animal
    super.putAnimal(animal); }}// Define a dog cage class
class DogCage extends AnimalCage {
    @override
    void putAnimal(Animal animal) {// Note that the method argument here is of type Animal
      super.putAnimal(animal); }}Copy the code

We need to override the putAnimal method, which inherits from the AnimalCage class, so its parameter type is Animal. What’s the problem with that? Take a look:

main() {
  // Create a cat cage object
  var catCage = CatCage();
  // Then you can put a dog in the cage. If you follow the design principle, the cat cage should only put the cat.
  catCage.putAnimal(Dog());// This line of static checks and runs can pass.
  
  // Create a dog cage object
  var dogCage = DogCage();
  // Then you can put a cat in the cage. If you follow the design principle, you should only put dogs in the cage.
  dogCage.putAnimal(Cat());// This line of static checks and runs can pass.
}
Copy the code

In fact, for the above problems, we prefer putAnimal parameters more specific, to solve the above problems you need to use the covariant keyword.

// Define a cat cage class
class CatCage extends AnimalCage {
  @override
  void putAnimal(covariant Cat animal) {// Note that the putAnimal method in the CatCage object accepts only Cat objects
    super.putAnimal(animal); }}// Define a dog cage class
class DogCage extends AnimalCage {
    @override
    void putAnimal(covariant Dog animal) {// Note that the putAnimal method in the DogCage object accepts only Dog objects, using the covariant keyword
      super.putAnimal(animal); }}/ / call
main() {
  // Create a cat cage object
  var catCage = CatCage();
  catCage.putAnimal(Dog());Error: The argument type 'Dog' can't be assigned to The parameter type 'Cat'.
}
Copy the code

To further verify the conclusion, take a look at this example:

typedef void PutAnimal(Animal animal);

class TestFunction {
  void putCat(covariant Cat animal) {}// Use the covariant keyword

  void putDog(Dog animal) {}

  void putAnimal(Animal animal) {}
}

main() {
  var function = TestFunction()
  print(function.putCat is PutAnimal);//true because covariant keywords are used
  print(function.putDog is PutAnimal);//false
  print(function.putAnimal is PutAnimal);// True is itself a subtype
}
Copy the code

6. Why is Kotlin safer than Dart’s generic type changes

In fact, Dart, like Java, has security issues with generic variants. And the List collection, which is both mutable and covariant in Dart, which creates security issues. However, Kotlin is different. In Kotlin, the set is divided into the mutable set MutableList

and the read-only set List

. In Kotlin, List

is immutable and covariant, so there is no security problem. The following example compares the implementation of Dart and Kotlin:


  • Implementation in Dart
class Fruit {
  final String color;

  Fruit(this.color);
}

class Apple extends Fruit {
  Apple() : super("red");
}

class Orange extends Fruit {
  Orange() : super("orange");
}

void printColors(List<Fruit> fruits) {// The List is not secure.
  for (var fruit in fruits) {
    print('${fruit.color}');
  }
}

main() {
  List<Apple> apples = <Apple>[];
  apples.add(Apple());
  printColors(apples);//printColors is passed in as a List
      
        because it is covariant
      
}
Copy the code

Why is it unsafe to say List

in printColors when you pass in a List

in the external main function? So fruits in the printColors function is actually a List

. But what about printColors?


void printColors(List<Fruit> fruits) {// The List is not secure.
  fruits.add(Orange());// Static checks are all ok, and dart1. x is also ok, but fortunately dart2. x is optimized,
  Type 'Orange' is not a subtype of type 'Apple' of 'value'
  // Since Dart List is mutable, adding Orange() to fruits actually adds Orange object to List
      
        causes security issues.
      
  for (var fruit in fruits) {
    print('${fruit.color}'); }}Copy the code
  • Implementation in Kotlin

Kotlin, however, doesn’t have that problem. Kotlin makes a fine distinction between mutable and read-only collections. Read-only and covariant generic types are more secure. Let’s see how Kotlin did it.

open class Fruit(val color: String)

class Apple : Fruit("red")

class Orange : Fruit("orange")

fun printColors(fruits: List<Fruit>) {
    fruits.add(Orange())// The List
      
        is read-only in Kotlin. There is no add, remove method to modify the List.
      
    // So it doesn't have to add an Orange to the List
      
       .
      
    for (fruit in fruits) {
        println(fruit.color)
    }
}

fun main(a) {
    val apples = listOf(Apple())
    printColors(apples)
}
Copy the code

Four, specific type

1. Type detection

Dart generally uses the IS keyword for type detection, which is consistent with Kotlin. If a Dart is not of a type, Dart uses IS! In Kotlin, the opposite is used! Is said. Type detection tests the dynamic type of the result value of an expression against the target type.

main() {
  var apples = [Apple()];
  print(apples is List<Apple>);
}
Copy the code

2. Enforce type casting

Casting generally uses the AS keyword in Dart, which is also consistent with Kotlin. A cast casts the value of an expression to the target type and raises a CastError exception if the cast fails.

Object o = [1.2.3];
o as List;
o as Map;// Throw an exception
Copy the code

Five, the summary

This is the end of Dart’s type system and generics. I believe this article will give you a fuller picture of Dart’s type system. Dart2.x really optimizes a lot of things by looking at generics in Dart. For example, generics safety is a problem. Dart2.x will continue to be more rigorous and refined, showing that Dart is changing is a good thing, and looking forward to more features. In the next installment, we’ll dive into the more core part of Dart’s asynchronous programming series. Thanks for watching.

My official account

Welcome to: Mr. Xiong Meow