Home > Back-end >  Dart : Why should overriding method's parameter be "wider" than parent's one? (p
Dart : Why should overriding method's parameter be "wider" than parent's one? (p

Time:06-20

https://dart.dev/guides/language/language-tour#extending-a-class

Argument types must be the same type as (or a supertype of) the overridden method’s argument types. In the preceding example, the contrast setter of SmartTelevision changes the argument type from int to a supertype, num.


I was looking at the above explanation and wondering why the arguments of subtype member methods need to be defined more "widely"(generally) than the original class's one.

https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)#Function_types

class AnimalShelter {

    Animal getAnimalForAdoption() {
        // ...
    }
    
    void putAnimal(Animal animal) {
        //...
    }
}



class CatShelter extends AnimalShelter {
//↓ Definitions that are desirable in the commentary
    void putAnimal(Object animal) {
        // ...
    }


//↓Definitions that are not desirable in the commentary
    void putAnimal(Cat animal) {
        // ...
    }
//I can't understand why this definition is risky.
//What specific problems can occur?
}

I think this wikipedia sample code is very easy to understand, so what kind of specific problem (fault) can occur if the argument of the member method of the subtype is defined as a more "narrower"(specific) type?

Even if it is explained in natural language, it will be abstract after all, so it would be very helpful if you could give me a complete working code and an explanation using it.

CodePudding user response:

void main(){

  Animal a1 = Animal();
  Cat c1 = Cat();
  Dog d1 = Dog();

  AnimalCage ac1 = AnimalCage();
  CatCage cc1 = CatCage();

  AnimalCage ac2 = CatCage();

  ac2.setAnimal(d1);

  //cc1.setAnimal(d1);

}

class AnimalCage{
  Animal? _animal;

  void setAnimal(Animal animal){
    print('animals setter');
    _animal = animal;
  }
}

class CatCage extends AnimalCage{
  Cat? _cat;

  @override
  void setAnimal(covariant Cat animal){
    print('cats setter');

    _cat = animal;
    /*
    if(animal is Cat){
      _cat = animal;
    }else{
      print('$animal is not Cat!');
    }
     */
  }
}

class Animal {}

class Cat extends Animal{}

class Dog extends Animal{}
Unhandled Exception: type 'Dog' is not a subtype of type 'Cat' of 'animal'

In the above code, even if the setAnimal method receives a Dog instance, a compile error does not occur and a runtime error occurs, so making the parameter the same type as the superclass's one and checking the type inside the method is necessary.

CodePudding user response:

Let's consider an example where you have a class hierarchy:

      Animal
     /     \
  Mammal   Reptile
  /   \
Dog   Cat

with superclasses (wider types) above subclasses (narrower types).

Now suppose you have classes:

class Base {
  void takeObject(Mammal mammal) {
    // ...
  }

  Mammal returnObject() {
    // ...
  }
}

class Derived extends Base {
  // ...
}

The public members of a class specify an interface: a contract to the callers. In this case, the Base class advertises a takeObject method that accepts any Mammal argument. Every instance of a Base class thus is expected to conform to this interface.

Following the Liskov substitution principle, because Derived extends Base, a Derived instance is a Base, and therefore it too must conform to that same Base class interface: its takeObject method also must accept any Mammal argument.

If Derived overrode takeObject to accept only Dog arguments:

class Derived extends Base {
  @override
  void takeObject(Dog mammal) { // ERROR
    // ...
  }
}

that would violate the contract from the Base class's interface. Derived's override of takeObject could be invoked with a Cat argument, which should be allowed according to the interface declared by Base. Since this is unsafe, Dart's static type system normally prevents you from doing that. (An exception is if you add the covariant keyword to disable type-safety and indicate that you personally guarantee that Derived.takeObject will never be called with any Mammals that aren't Dogs. If that claim is wrong, you will end up with a runtime error.)

Note that it'd be okay if Derived overrode takeObject to accept an Animal argument instead:

class Derived extends Base {
  @override
  void takeObject(Animal mammal) { // OK
    // ...
  }
}

because that would still conform to the contract of Base.takeObject: it's safe to call Derived.takeObject with any Mammal since all Mammals are also Animals.

Note that the behavior for return values is the opposite: it's okay for an overridden method to return a narrower type, but returning a wider type would violate the contract of the Base interface. For example:

class Derived extends Base {
  @override
  Dog returnObject() { // OK, a `Dog` is a `Mammal`, as required by `Base`
    // ...
  }
}

but:

class Derived extends Base {
  @override
  Animal returnObject() { // ERROR: Could return a `Reptile`, which is not a `Mammal`
    // ...
  }
}
  • Related