Wariancja: kowariancja i kontrawariancja
Opanuj kowariancję i kontrawariancję, aby poprawnie obsługiwać relacje podtypów w typach generycznych.
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
Ajest podtypemB, wtedyBox[A]staje się podtypemBox[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 (aCatjest typemAnimal).
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]jakoList[Animal], ponieważ z listy odczytuje się wyłącznie wartości typuAnimallub ich podtypów. - Nie można dodać dowolnego
AnimaldoList[Cat], jeśli jest ona traktowana jakoList[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
Ajest podtypemB, wtedyConsumer[B]staje się podtypemConsumer[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 typAnimal, w tymCat).
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żeCat. 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 podtypemBox[B]tylko wtedy, gdyAiBsą 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
Ajest 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
Bjest 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, abyContainer[Subtype]było podtypemContainer[Supertype]. Jest przydatna w przypadku typów „producentów”, które tylko zwracająT. - Kontrawariancja (
-T): Pozwala, abyContainer[Supertype]było podtypemContainer[Subtype]. Jest przydatna w przypadku typów „konsumentów”, które przyjmująTwyłą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!
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
- Typy generyczne i parametry typów
- Wariancja: kowariancja i kontrawariancja
- Klasy typów i implicity