Scala for Backend Engineering & Functional Programming · Lekcja

Wariancja: kowariancja i kontrawariancja

Opanuj kowariancję i kontrawariancję, aby poprawnie obsługiwać relacje podtypów w typach generycznych.

Lekcja 2 z 311 kroki

Wariancja: kowariancja i kontrawariancja to bezpłatna lekcja Scala for Backend Engineering & Functional Programming na CoddyKit. To lekcja 2 z 3. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Scala for Backend Engineering & Functional Programming, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Scala for Backend Engineering & Functional Programming zawiera 3 lekcji w sumie.

Czym jest wariancja typów?

W Scali, gdy istnieje hierarchia typów (np. Cat jest podtypem Animal), jak zachowują się typy generyczne?

Czy List[Cat] jest uznawany za podtyp List[Animal]? Domyślnie nie zawsze!

Wariancja typów pozwala określić, jak relacje podtypowania są zachowywane (lub odwracane) dla typów generycznych. Jest to kluczowe podczas pisania elastycznego i bezpiecznego typowo kodu.

Kowariancja: typy „producentów” (+T)

Kowariancję oznacza się, umieszczając + przed parametrem typu (np. trait Box[+T]).

  • Jeśli A jest podtypem B, wtedy Box[A] staje się podtypem Box[B].
  • Typy kowariantne można traktować jako „producentów” wartości typu T. Mogą występować wyłącznie w pozycjach wyjściowych, takich jak typy zwracane przez metody.
  • Oznacza to, że jeśli oczekiwany jest Box[Animal], można przekazać Box[Cat], ponieważ „produkuje” ono coś bardziej szczegółowego (a Cat jest typem Animal).

Kowariancja w działaniu

Uruchom ten przykład kowariantnego traitu Producer:

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}")
  }
}

Kiedy używać kowariancji

Kowariancja jest bezpieczna, gdy typ generyczny jedynie „produkuje” wartości typu T albo nigdy nie przyjmuje T jako argumentu.

  • Niemodyfikowalne kolekcje, takie jak List[+T], są doskonałym przykładem. Można traktować List[Cat] jako List[Animal], ponieważ z listy odczytuje się wyłącznie wartości typu Animal lub ich podtypów.
  • Nie można dodać dowolnego Animal do List[Cat], jeśli jest ona traktowana jako List[Animal], co pozwala zachować bezpieczeństwo typów.

Kontrawariancja: typy „konsumentów” (-T)

Kontrawariancję oznacza się, umieszczając - przed parametrem typu (np. trait Consumer[-T]).

  • Jeśli A jest podtypem B, wtedy Consumer[B] staje się podtypem Consumer[A].
  • Typy kontrawariantne można traktować jako „konsumentów” wartości typu T. Mogą występować wyłącznie w pozycjach wejściowych, takich jak typy parametrów metod.
  • Oznacza to, że jeśli oczekiwany jest Consumer[Cat], można przekazać Consumer[Animal], ponieważ „konsumuje” ono coś bardziej ogólnego (może obsłużyć dowolny typ Animal, w tym Cat).

Kontrawariancja w działaniu

Uruchom ten przykład kontrawariantnego traitu Consumer:

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.")
  }
}

Kiedy używać kontrawariancji

Kontrawariancja jest bezpieczna, gdy typ generyczny jedynie „konsumuje” wartości typu T albo nigdy nie zwraca T.

  • Typowym przykładem są funkcje, a konkretnie typ ich parametru wejściowego. Jeśli funkcja potrafi przetworzyć dowolny typ Animal (Animal => Unit), z pewnością potrafi przetworzyć także Cat. Zatem (Animal => Unit) jest podtypem (Cat => Unit).
  • Pozwala to zachować większą elastyczność podczas przekazywania funkcji jako argumentów.

Inwariancja: zachowanie domyślne

Jeśli nie określi się + ani -, parametr typu jest inwariantny. To zachowanie domyślne w Scali.

  • Box[A] jest podtypem Box[B] tylko wtedy, gdy A i B są dokładnie tym samym typem.
  • Jest to często konieczne w przypadku modyfikowalnych kolekcji (np. Array[T]), aby zapobiec problemom z bezpieczeństwem typów, ponieważ można z nich zarówno odczytywać różne podtypy, jak i je zapisywać.
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.")
  }
}

Funkcje: kowariancja i kontrawariancja

Typy funkcji w Scali, Function1[-A, +B], elegancko łączą oba rodzaje wariancji:

  • Parametr wejściowy A jest kontrawariantny (-A). Oznacza to, że funkcji przyjmującej bardziej ogólny typ (np. Animal) można użyć tam, gdzie oczekiwana jest funkcja przyjmująca bardziej szczegółowy typ (np. Cat).
  • Typ zwracany B jest kowariantny (+B). Oznacza to, że funkcji zwracającej bardziej szczegółowy typ (np. Cat) można użyć tam, gdzie oczekiwana jest funkcja zwracająca bardziej ogólny typ (np. 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}")
  }
}

Szybki test: reguły wariancji

Rozważ następujący trait:

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

Jakie oznaczenie wariancji powinien mieć parametr T, aby można było użyć Handler[Animal] tam, gdzie oczekiwany jest Handler[Cat] (gdzie Cat rozszerza Animal)?

Podsumowanie: wariancja opanowana

Wariancja w Scali została opanowana! Oto krótkie podsumowanie:

  • Kowariancja (+T): Pozwala, aby Container[Subtype] było podtypem Container[Supertype]. Jest przydatna w przypadku typów „producentów”, które tylko zwracają T.
  • Kontrawariancja (-T): Pozwala, aby Container[Supertype] było podtypem Container[Subtype]. Jest przydatna w przypadku typów „konsumentów”, które przyjmują T wyłącznie jako dane wejściowe.
  • Inwariancja: Zachowanie domyślne. Typy muszą być identyczne.

Zrozumienie wariancji pomaga tworzyć bardziej elastyczny i bezpieczny typowo kod generyczny w Scali!

Bezpłatny start

Ucz się Scala dzięki korepetycjom AI — za darmo

Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.

Kursy
39
Lekcje
143

Często zadawane pytania

Czy lekcja „Wariancja: kowariancja i kontrawariancja” jest bezpłatna?

Tak — pełny tekst „Wariancja: kowariancja i kontrawariancja” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Scala for Backend Engineering & Functional Programming, przejdź na CoddyKit PRO. Kurs Scala for Backend Engineering & Functional Programming zawiera 3 lekcji w sumie.

Co nauczysz się w „Wariancja: kowariancja i kontrawariancja”?

Opanuj kowariancję i kontrawariancję, aby poprawnie obsługiwać relacje podtypów w typach generycznych. Ćwiczysz Scala for Backend Engineering & Functional Programming z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć Scala for Backend Engineering & Functional Programming?

Nie wymagamy żadnego doświadczenia. Scala for Backend Engineering & Functional Programming w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 2 z 3.

Ile czasu zajmuje lekcja „Wariancja: kowariancja i kontrawariancja”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji Scala for Backend Engineering & Functional Programming?

Tak. Każda lekcja Scala for Backend Engineering & Functional Programming zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Typy generyczne i parametry typów
  2. Wariancja: kowariancja i kontrawariancja
  3. Klasy typów i implicity
← Powrót do Scala for Backend Engineering & Functional Programming