0Pricing
Scala for Backend Engineering & Functional Programming · Lezione

Varianza: covarianza e controvarianza

Padroneggi covarianza e controvarianza per gestire correttamente le relazioni di sottotipizzazione nei tipi generici.

Varianza: covarianza e controvarianza è una lezione Scala for Backend Engineering & Functional Programming gratuita su CoddyKit. Questa è la lezione 2 di 3. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Scala for Backend Engineering & Functional Programming, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Scala for Backend Engineering & Functional Programming include 3 lezioni in totale.

Che cos'è la varianza dei tipi?

In Scala, quando si dispone di una gerarchia di tipi (ad esempio, Cat è un sottotipo di Animal), come si comportano i tipi generici?

List[Cat] è considerato un sottotipo di List[Animal]? Non sempre, per impostazione predefinita!

La varianza dei tipi consente di definire come vengono mantenute (o invertite) le relazioni di sottotipizzazione per i tipi generici. È fondamentale per scrivere codice flessibile e sicuro dal punto di vista dei tipi.

Covarianza: tipi "produttori" (+T)

La covarianza è indicata inserendo un + prima del parametro di tipo (ad esempio, trait Box[+T]).

  • Se A è un sottotipo di B, allora Box[A] diventa un sottotipo di Box[B].
  • Consideri i tipi covarianti come "produttori" di T. Possono comparire solo in posizioni di output (come i tipi restituiti dai metodi).
  • Ciò significa che, se è previsto un Box[Animal], è possibile fornire un Box[Cat] perché "produce" qualcosa di più specifico (un Cat è un Animal).

La covarianza in azione

Provi a eseguire questo esempio di trait Producer covariante:

class Animal
class Cat extends Animal

trait Producer[+T] {
  def produce: T // Output position
}

class CatProducer extends Producer[Cat] {
  def produce: Cat = new Cat
}

object Main {
  def main(args: Array[String]): Unit = {
    val catProducer: Producer[Cat] = new CatProducer
    // Because Producer is covariant, Producer[Cat] is a subtype of Producer[Animal]
    val animalProducer: Producer[Animal] = catProducer 
    println("Assigned CatProducer to AnimalProducer.")
    println(s"Produced: ${animalProducer.produce.getClass.getSimpleName}")
  }
}

Quando utilizzare la covarianza

La covarianza è sicura quando il tipo generico "produce" solo valori di tipo T o non accetta mai T come argomento.

  • Le collezioni immutabili come List[+T] ne sono un esempio fondamentale. È possibile trattare una List[Cat] come una List[Animal] perché da essa si "leggono" soltanto Animal (o i relativi sottotipi).
  • Non è possibile aggiungere un Animal qualsiasi a una List[Cat] se questa è tipizzata come List[Animal], mantenendo così la sicurezza dei tipi.

Controvarianza: tipi "consumatori" (-T)

La controvarianza è indicata inserendo un - prima del parametro di tipo (ad esempio, trait Consumer[-T]).

  • Se A è un sottotipo di B, allora Consumer[B] diventa un sottotipo di Consumer[A].
  • Consideri i tipi controvarianti come "consumatori" di T. Possono comparire solo in posizioni di input (come i tipi dei parametri dei metodi).
  • Ciò significa che, se è previsto un Consumer[Cat], è possibile fornire un Consumer[Animal] perché "consuma" qualcosa di più generale (può gestire qualsiasi Animal, incluso un Cat).

La controvarianza in azione

Provi a eseguire questo esempio di trait Consumer controvariante:

class Animal
class Cat extends Animal

trait Consumer[-T] {
  def consume(item: T): Unit // Input position
}

class AnimalConsumer extends Consumer[Animal] {
  def consume(item: Animal): Unit = {
    println(s"Consumed an animal: ${item.getClass.getSimpleName}")
  }
}

object Main {
  def main(args: Array[String]): Unit = {
    val animalConsumer: Consumer[Animal] = new AnimalConsumer
    // Because Consumer is contravariant, Consumer[Animal] is a subtype of Consumer[Cat]
    val catConsumer: Consumer[Cat] = animalConsumer 
    catConsumer.consume(new Cat)
    println("Assigned AnimalConsumer to CatConsumer.")
  }
}

Quando utilizzare la controvarianza

La controvarianza è sicura quando il tipo generico "consuma" solo valori di tipo T o non restituisce mai T.

  • Un esempio comune sono le funzioni, in particolare il tipo del parametro di input. Se una funzione può elaborare qualsiasi Animal (Animal => Unit), può certamente elaborare un Cat. Pertanto, (Animal => Unit) è un sottotipo di (Cat => Unit).
  • Ciò offre maggiore flessibilità quando si passano funzioni come argomenti.

Invarianza: il comportamento predefinito

Se non specifica + o -, il parametro di tipo è invariante. Questo è il comportamento predefinito in Scala.

  • Box[A] è un sottotipo di Box[B] solo se A è esattamente dello stesso tipo di B.
  • Ciò è spesso necessario per le collezioni mutabili (ad esempio, Array[T]) per prevenire problemi di sicurezza dei tipi, poiché si potrebbero sia leggere sia scrivere sottotipi diversi.
class Food
class Apple extends Food

// Invariant Box
class Box[T](val item: T) {
  def getContent: T = item
}

object Main {
  def main(args: Array[String]): Unit = {
    val appleBox = new Box(new Apple)
    // The following line would cause a compile error:
    // val foodBox: Box[Food] = appleBox 
    println(s"An Apple Box contains: ${appleBox.getContent.getClass.getSimpleName}")
    println("Box[Apple] is NOT a subtype of Box[Food] (invariant).")
    println("The types must match exactly for invariant types.")
  }
}

Funzioni: covarianti e controvarianti

I tipi funzione di Scala, Function1[-A, +B], combinano elegantemente entrambi i tipi di varianza:

  • Il parametro di input A è controvariante (-A). Ciò significa che una funzione che accetta un tipo più generale (ad esempio, Animal) può essere utilizzata dove è prevista una funzione che accetta un tipo più specifico (ad esempio, Cat).
  • Il tipo restituito B è covariante (+B). Ciò significa che una funzione che restituisce un tipo più specifico (ad esempio, Cat) può essere utilizzata dove è prevista una funzione che restituisce un tipo più generale (ad esempio, Animal).
class Vehicle
class Car extends Vehicle

object Main {
  def main(args: Array[String]): Unit = {
    // Contravariance for input: (Vehicle => Unit) is a subtype of (Car => Unit)
    val printVehicle: Vehicle => Unit = (v: Vehicle) => println(s"Printing vehicle: ${v.getClass.getSimpleName}")
    val printCar: Car => Unit = printVehicle // OK: A general printer can print a specific car
    printCar(new Car)

    // Covariance for output: (() => Car) is a subtype of (() => Vehicle)
    val getCar: () => Car = () => new Car
    val getVehicle: () => Vehicle = getCar // OK: A specific producer can fulfill a general request
    println(s"Got vehicle: ${getVehicle().getClass.getSimpleName}")
  }
}

Verifica rapida: regole della varianza

Consideri il trait seguente:

trait Handler[T] {
  def handle(item: T): Unit
}

Per consentire l'utilizzo di Handler[Animal] dove è previsto un Handler[Cat] (dove Cat estende Animal), quale annotazione di varianza dovrebbe avere T?

Riepilogo: padronanza della varianza

Ha acquisito la padronanza della varianza in Scala! Ecco un breve riepilogo:

  • Covarianza (+T): consente a Container[Subtype] di essere un sottotipo di Container[Supertype]. È utile per i tipi "produttori" che restituiscono solo T.
  • Controvarianza (-T): consente a Container[Supertype] di essere un sottotipo di Container[Subtype]. È utile per i tipi "consumatori" che accettano solo T come input.
  • Invarianza: è il comportamento predefinito. I tipi devono corrispondere esattamente.

Comprendere la varianza La aiuta a creare codice generico Scala più flessibile e sicuro dal punto di vista dei tipi!

Domande Frequenti

La lezione «Varianza: covarianza e controvarianza» è gratuita?

Sì — il testo completo di «Varianza: covarianza e controvarianza» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Scala for Backend Engineering & Functional Programming, passa a CoddyKit PRO. Il corso Scala for Backend Engineering & Functional Programming include 3 lezioni in totale.

Cosa imparerò in «Varianza: covarianza e controvarianza»?

Padroneggi covarianza e controvarianza per gestire correttamente le relazioni di sottotipizzazione nei tipi generici. Eserciti Scala for Backend Engineering & Functional Programming con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.

Ho bisogno di esperienza per iniziare Scala for Backend Engineering & Functional Programming?

Non è richiesta alcuna esperienza precedente. Scala for Backend Engineering & Functional Programming su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 2 di 3.

Quanto tempo richiede la lezione «Varianza: covarianza e controvarianza»?

La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.

Posso scrivere ed eseguire codice in questa lezione Scala for Backend Engineering & Functional Programming?

Sì. Ogni lezione Scala for Backend Engineering & Functional Programming include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.

Tutte le lezioni di questo corso

  1. Generics e parametri di tipo
  2. Varianza: covarianza e controvarianza
  3. Type class e impliciti
← Torna a Scala for Backend Engineering & Functional Programming