Contravarianza degli argomenti, scopo e uso del mondo reale?

4

Ho letto alcune domande su SO e altrove e ancora non capisco bene dove questo "allargamento" di un tipo di parametro può essere utile, ad esempio il rispetto del principio di sostituzione di Liskov. Il seguente codice ho preso da una risposta sul SO, spiegando la controvarianza:

//Contravariance of parameter types: OK
class Food
class FastFood extends Food

class Person { eat(FastFood food) }
class FatPerson extends Person { eat(Food food) }

Quindi capisco che il metodo sottoposto a override accetta un parametro più generico rispetto al metodo nel suo antenato. Ma in pratica, come aiuta? Voglio dire, se il metodo originale funziona con certe proprietà del tipo derivato, niente di questo sarà disponibile nella derivata usando il supertipo del parametro. Pertanto, potrei avere problemi con l'adempimento delle post-condizioni del contratto, se quelle si riferiscono al sottotipo in qualche modo. Come:

class Animal {}
class Cat { void Meow() void CatSpecificThing()}
...

class A
{
   List<Cat> ListOfCats;
   void X(Cat c)
   {
      c.Meow()
      c.CatSpecificThings()
      ListOfCats.Add(c)

   }
}
class B : A
{
   void X(Animal a)
   {
       //how is this now useful? I cannot do anything that needed Cat
   }
}

Supponiamo che la post-condizione del metodo X sia quella di aggiornare ListOfCats. Ma nel metodo sovrascritto nella classe derivata, non sarei in grado di farlo se ci fosse solo il supertipo ..?

Sarei estremamente felice per un semplice esempio che dimostra come sia utile.

    
posta John V 30.01.2018 - 13:47
fonte

2 risposte

3

Penso che un punto in cui potresti confonderti è che la contravarianza include il caso in cui i tipi sono uguali. Non è una violazione di Liskov utilizzare lo stesso tipo negli argomenti del metodo derivato, quindi nel tuo esempio è perfettamente corretto usare Cat invece di Animal in B . La stragrande maggioranza dei casi d'uso nel mondo reale utilizzerà esattamente la stessa classe piuttosto che una più ampia.

Come esempio di dove è utile la controvarianza, considera il seguente esempio di Scala:

class Pet
class Cat extends Pet
class Kitten extends Cat

class Cats(cats: Cat*) {
  def sorted[B >: Cat](ordering: math.Ordering[B]): Seq[Cat] =
    cats.sorted(ordering)
}

class Kittens(kittens: Kitten*) extends Cats {
  override def sorted[B >: Kitten](ordering: math.Ordering[B]): Seq[Kitten] =
    kittens.sorted(ordering)
}

In Scala, >: è il modo in cui si specifica la controvarianza. Qui puoi ordinare Kittens utilizzando Ordering[Kitten] , ma puoi anche utilizzare Ordering[Cat] o Ordering[Pet] , il che ha senso. Cats può essere ordinato solo usando Ordering[Cat] o Ordering[Pet] , quindi il loro argomento ordering è più ristretto, può accettare un numero inferiore di tipi. Non avrebbe senso ordinare Cats di Ordering[Kitten] .

Cosa accadrebbe se riducessimo l'argomento accettando solo Ordering[Kitten] nella sorted per Kittens ? Ciò renderebbe impossibile il controllo del seguente codice:

val cats: Cats = new Kittens(new Kitten, new Kitten)
val ordering: math.Ordering[Cat] = math.Ordering.by(_.toString)
cats.sorted(ordering)

Questo non verrebbe controllato perché l'ultima riga vuole chiamare sorted nella classe Kittens , ma quel metodo non consente un Ordering[Cat] . Il mancato controllo del tipo è il motivo per cui restringere gli argomenti del metodo in una sottoclasse viola il Principio di sostituzione di Liskov.

    
risposta data 30.01.2018 - 19:38
fonte
0

In questo caso sembra inutile, ma microsoft docs stesso dà un caso abbastanza utile:

Action<Base> b = (target) => { Console.WriteLine(target.GetType().Name); };
Action<Derived> d = b;
d(new Derived());

In questo caso sei in grado di assegnare un'azione parametrica più generica a una più derivata. Perché è utile? la tua azione di classe base potrebbe già esistere, in questo modo non devi avvolgerla in un'altra funzione (lambda) per creare il desiderato: Action<Derived>

Invece di dire che ho questa interfaccia perché ho bisogno di questo metodo di classe base. Trasforma la tua domanda in giro: ho questa classe base con questo metodo (che è già usato), voglio implementare l'interfaccia. senza controversione dovresti aggiungere in modo specifico un metodo aggiuntivo.

Altre ragioni potrebbero essere che: la classe Derived / Cat è in una dipendenza non raggiungibile, ma la classe de base è raggiungibile. Essendo in grado di utilizzare la classe di base, il codice è meno abbinato e specifico come deve essere (più riutilizzabile).

    
risposta data 30.01.2018 - 15:32
fonte

Leggi altre domande sui tag