0Pricing
Scala for Backend Engineering & Functional Programming · Lekcja

Typy generyczne i parametry typów

Dowiedz się, jak pisać generyczne klasy i metody, aby tworzyć wielokrotnego użytku komponenty z bezpieczeństwem typów.

Typy generyczne i parametry typów to bezpłatna lekcja Scala for Backend Engineering & Functional Programming na CoddyKit. To lekcja 1 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 są typy generyczne?

Wyobraźmy sobie, że trzeba utworzyć kontener, który może przechowywać elementy dowolnego typu, na przykład String, Int lub własny obiekt. Bez typów generycznych trzeba byłoby napisać osobny kontener dla każdego typu, co prowadziłoby do powielania kodu, albo użyć bardzo ogólnego typu, takiego jak Any.

Typy generyczne pozwalają pisać elastyczny i wielokrotnego użytku kod, który działa z różnymi typami danych, a jednocześnie zapewnia bezpieczeństwo typów na etapie kompilacji. To jak szablon, do którego można później wstawić różne materiały!

Pułapka typu `Any`

Używanie Any jako zastępnika typów może prowadzić do problemów. Choć pozwala przechowywać dowolne wartości, traci się wtedy informacje o ich konkretnym typie. Oznacza to, że często trzeba ponownie rzutować element na jego pierwotny typ. Jest to podatne na błędy i może powodować awarie w czasie działania programu, jeśli rzutowanie będzie nieprawidłowe.

Uruchom ten przykład i zastanów się nad potencjalnymi problemami:

class ItemHolder(val item: Any)

object Main {
  def main(args: Array[String]): Unit = {
    val stringHolder = new ItemHolder("Hello Scala")
    val numberHolder = new ItemHolder(123)

    // We need to cast, and it's unsafe if we get it wrong
    val retrievedString = stringHolder.item.asInstanceOf[String]
    println(s"Retrieved String: $retrievedString")

    // If we uncomment the line below, it compiles but will crash at runtime!
    // val wrongType = numberHolder.item.asInstanceOf[String]
    // println(s"Wrong Type: $wrongType")
  }
}

Wprowadzenie parametrów typów

Typy generyczne rozwiązują problem typu Any, wprowadzając parametry typów. Są one odpowiednikami symboli zastępczych dla rzeczywistych typów, które określa się podczas tworzenia instancji klasy lub wywoływania metody.

W Scali parametry typów definiuje się za pomocą nawiasów kwadratowych ([]) umieszczanych po nazwie klasy lub metody. Zwykle używa się pojedynczych wielkich liter, takich jak T, A, B itd. Na przykład class Box[T] deklaruje generyczną klasę Box, która przyjmuje jeden parametr typu T.

Tworzenie generycznego obiektu Box

Utwórzmy prostą generyczną klasę o nazwie Box, która może przechowywać zawartość dowolnego typu. Zwróć uwagę, jak symbol T jest używany w całej definicji klasy do reprezentowania typu zawartości.

Dzięki temu można tworzyć obiekty Box przechowujące napisy, liczby całkowite lub dowolne inne typy bez pisania osobnych klas dla każdego z nich.

class Box[T](val content: T) {
  def get: T = content
  override def toString: String = s"Box($content)"
}

object Main {
  def main(args: Array[String]): Unit = {
    val stringBox = new Box("Scala is fun!")
    val intBox = new Box(42)
    val booleanBox = new Box(true)

    println(stringBox)
    println(intBox)
    println(booleanBox)
  }
}

Bezpieczeństwo typów dzięki typom generycznym

Największą zaletą typów generycznych jest bezpieczeństwo typów w czasie kompilacji. Gdy tworzy się obiekt Box[String], kompilator wie, że powinien on zawierać wyłącznie napisy. Jeśli spróbuje się wstawić do niego liczbę całkowitą albo pobrać z niego wartość jako liczbę całkowitą, kompilator natychmiast wykryje błąd.

Zapobiega to wielu typowym błędom występującym w czasie działania programu przy mniej bezpiecznych typach podejściach, takich jak używanie Any.

class Box[T](val content: T) {
  def get: T = content
}

object Main {
  def main(args: Array[String]): Unit = {
    val myStringBox: Box[String] = new Box("CoddyKit")
    val myIntBox: Box[Int] = new Box(100)

    // This works, type is String
    val s: String = myStringBox.get
    println(s"String from box: $s")

    // This works, type is Int
    val i: Int = myIntBox.get
    println(s"Int from box: $i")

    // The compiler prevents this line from compiling:
    // val wrong: String = myIntBox.get // Type mismatch error!
  }
}

Typy generyczne także dla metod!

Podobnie jak klasy, również pojedyncze metody mogą być generyczne. Metoda generyczna może przyjmować parametry typów lokalne dla tej metody, dzięki czemu działa z różnymi typami bez konieczności uogólniania całej klasy.

Jest to przydatne, gdy istnieje konkretna operacja, którą można zastosować do różnych typów, ale sama klasa nie jest generycznym kontenerem.

Elastyczna metoda `printPair`

Oto przykład metody generycznej printPair. Przyjmuje ona dwa parametry typów, A i B, dzięki czemu może wypisywać parę dowolnych dwóch typów. Zwróć uwagę, że parametry typów są deklarowane bezpośrednio po nazwie metody.

Dzięki temu metoda może być wielokrotnie używana dla różnych kombinacji danych.

object Util {
  def printPair[A, B](first: A, second: B): Unit = {
    println(s"Pair: ($first, $second)")
    println(s"Type of first: ${first.getClass.getName}")
    println(s"Type of second: ${second.getClass.getName}")
  }
}

object Main {
  def main(args: Array[String]): Unit = {
    Util.printPair("Hello", 123)
    Util.printPair(true, List(1, 2, 3))
    Util.printPair(4.5, 'C')
  }
}

Inteligentne wnioskowanie typów

Jedną z zaawansowanych funkcji Scali jest wnioskowanie typów. Podczas korzystania z generycznych klas lub metod często nie trzeba jawnie określać parametrów typów. Kompilator Scali potrafi wywnioskować je na podstawie przekazanych argumentów.

Dzięki temu kod jest bardziej zwięzły i łatwiejszy do czytania, a bezpieczeństwo typów pozostaje zachowane.

class Container[T](val item: T) {
  def getItem: T = item
}

object Main {
  def main(args: Array[String]): Unit = {
    // Scala infers T as String
    val stringContainer = new Container("Inferred String")
    println(s"Content: ${stringContainer.getItem}, Type: ${stringContainer.getItem.getClass.getName}")

    // Scala infers T as Double
    val doubleContainer = new Container(3.14)
    println(s"Content: ${doubleContainer.getItem}, Type: ${doubleContainer.getItem.getClass.getName}")

    // You can also specify explicitly, but often not needed
    val explicitIntContainer: Container[Int] = new Container(500)
    println(s"Content: ${explicitIntContainer.getItem}, Type: ${explicitIntContainer.getItem.getClass.getName}")
  }
}

Ograniczanie typów generycznych za pomocą ograniczeń

Czasami generyczna klasa lub metoda powinna działać wyłącznie z typami, które mają określone możliwości. Na przykład metoda dodająca liczby powinna przyjmować tylko typy liczbowe.

Ograniczenia typów pozwalają ograniczyć typy, których można użyć jako parametrów typów. Ograniczenie górne (<:) oznacza, że parametr typu musi być podtypem danego typu. Na przykład [T <: Animal] oznacza, że T musi być typem Animal lub jego podtypem.

class Animal { def speak(): String = "..." }
class Dog extends Animal { override def speak(): String = "Woof!" }
class Cat extends Animal { override def speak(): String = "Meow!" }

// This box can only hold types that are Animal or its subtypes
class AnimalShelter[T <: Animal](val animal: T) {
  def getAnimalSound: String = animal.speak()
}

object Main {
  def main(args: Array[String]): Unit = {
    val dogShelter = new AnimalShelter(new Dog())
    println(s"Dog says: ${dogShelter.getAnimalSound}")

    val catShelter = new AnimalShelter(new Cat())
    println(s"Cat says: ${catShelter.getAnimalSound}")

    // This would NOT compile because Car is not an Animal:
    // class Car
    // val carShelter = new AnimalShelter(new Car()) 
  }
}

Zadanie: klasa generyczna

Czas sprawdzić, jak dobrze rozumiesz typy generyczne!

Uważnie przeczytaj poniższe stwierdzenia i wybierz wszystkie prawdziwe informacje dotyczące generycznych klas i metod w Scali.

Typy generyczne: najważniejsze informacje

W tej lekcji omówiliśmy typy generyczne w Scali — zaawansowaną funkcję umożliwiającą pisanie elastycznego i bezpiecznego typowo kodu.

  • Typy generyczne pozwalają klasom i metodom działać z różnymi typami.
  • Wykorzystują parametry typów (np. [T]) jako symbole zastępcze dla rzeczywistych typów.
  • Zapewniają bezpieczeństwo typów w czasie kompilacji, zapobiegając błędom w czasie działania programu.
  • Wnioskowanie typów w Scali często pozwala zachować zwięzłość kodu generycznego.
  • Ograniczenia typów (np. <:) mogą zawężać parametry typów do określonych podtypów.

Dzięki typom generycznym można pisać wielokrotnego użytku komponenty, które są niezawodne i można je dostosować do różnych typów danych. Kod Scala staje się dzięki temu bardziej zaawansowany i mniej podatny na błędy.

Często zadawane pytania

Czy lekcja „Typy generyczne i parametry typów” jest bezpłatna?

Tak — pełny tekst „Typy generyczne i parametry typów” 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 „Typy generyczne i parametry typów”?

Dowiedz się, jak pisać generyczne klasy i metody, aby tworzyć wielokrotnego użytku komponenty z bezpieczeństwem typów. Ć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 1 z 3.

Ile czasu zajmuje lekcja „Typy generyczne i parametry typów”?

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