9.4. Niezmienniczość

Plik ReaderWriter.scala zawiera definicję klasy ReaderWriter, która umożliwia odczyt i zapis wartości pewnego typu reprezentowanego przez parametr T.

Plik ReaderWriter.scala:
class ReaderWriter[T](var content: T) {
  def read: T = content
  def write(x: T) { content = x }
}

Spójrzmy na pierwszy wiersz definicji tej klasy. Parametr typu T jest umieszczony w nawiasach kwadratowych i nie jest poprzedzony żadnym innym znakiem, w szczególności nie jest poprzedzony znakiem + lub - (w następnych punktach zostanie wyjaśnione ich znaczenie w sytuacji, gdy pojawiają się przy parametrze typu). Tak zdefiniowany typ ogólny ReaderWriter będziemy nazywali niezmienniczym ze względu na parametr typu T, albo będziemy mówili, że ma on niezmienniczy parametr typu T. Oznacza to, że jeśli mamy dwie klasy A i B, gdzie A jest klasą nadrzędną dla B, to typ zdefiniowany jako ReaderWriter[A] nie jest ani nadtypem, ani podtypem typu ReaderWriter[B]. Ilustrują to poniższe przykłady.

scala> val a: ReaderWriter[AnyRef] = new ReaderWriter(new Object)
a: ReaderWriter[AnyRef] = ReaderWriter@1cf0d14

scala> val b: ReaderWriter[Any] = a
<console>:11: error: type mismatch;
 found   : ReaderWriter[AnyRef]
 required: ReaderWriter[Any]
Note: AnyRef <: Any, but class ReaderWriter is invariant in type T.
You may wish to define T as +T instead. (SLS 4.5)
       val b: ReaderWriter[Any] = a
                                  ^

scala> val c: ReaderWriter[String] = a
<console>:11: error: type mismatch;
 found   : ReaderWriter[AnyRef]
 required: ReaderWriter[String]
Note: AnyRef >: String, but class ReaderWriter is invariant in type T.
You may wish to define T as -T instead. (SLS 4.5)
       val c: ReaderWriter[String] = a
                                     ^

Wartość a ma typ ReaderWriter[AnyRef], a parametr T klasy ReaderWriter jest niezmienniczy. Zatem ReaderWriter[AnyRef] nie jest podtypem ani ReaderWriter[Any], ani ReaderWriter[String], co wyjaśnia dlaczego nie udało się przypisać wartości a wartościom b i c.

Przeanalizujmy to zagadnienie na innym przykładzie. Wyobraźmy sobie, że chcemy zamodelować dziecko tworzące rysunki na papierze. Rysowanie polegałoby na wykonaniu trzech czynności: pobraniu nowej kartki z pudełka, narysowaniu na niej rysunku i wyrzuceniu kartki do kosza. Zacznijmy modelowanie od klas reprezentujących kartki, na których będą rysowane rysunki. Klasa Paper reprezentuje kartkę papieru, natomiast klasa ColorPaper reprezentuje kartkę określonego koloru.

Plik Paper.scala:
class Paper {
  override def toString = "paper"
}
class ColorPaper(color: String) extends Paper {
  override def toString = color + " paper"
}

Kartki będą wyciągane z pudełka reprezentowanego przez klasę Box. Klasa Box jest klasą ogólną, posiadającą parametr typu określający rodzaj obiektów, które mogą się znajdować w pudełku. Pudełko udostępnia operację take reprezentującą wyciągnięcie kolejnego obiektu z pudełka. Dla uproszczenia zakładamy, że pudełko ma nieskończoną objętość, to znaczy, że można bez końca wyciągać z niego kolejne obiekty.

Plik Box.scala:
class Box[T](name: String, e: T) {
  def take: T = {
    println("Taking "+e+" from the "+this)
    e
  }
  override def toString = name
}

Zużyte kartki będą wyrzucane do kosza na śmieci. Podobnie jak w przypadku pudełka, parametryzujemy klasę reprezentującą kosz na śmieci za pomocą typu. Kosz na śmieci udostępnia operację wyrzucenia przedmiotu do kosza, o nazwie throwAway. Podobnie jak w przypadku pudełka, przyjmujemy że kosz ma nieskończoną pojemność.

Plik WasteBin.scala:
class WasteBin[T](name: String) {
  def throwAway(t: T) =
    println("Throwing away "+t+" into the "+this)
  override def toString = name
}

Teraz możemy już przystąpić do zamodelowania rysującego dziecka. Klasa DrawingChild, reprezentująca dziecko, przyjmuje parametry reprezentujące imię dziecka, pudełko, z którego pobierane będą kartki oraz kosz na śmieci.

Plik DrawingChild.scala:
class DrawingChild(name: String,
  box: Box[Paper],
  wasteBin: WasteBin[Paper]){
  def draw = {
    val paper = box.take
    println("Drawing on "+paper)
    wasteBin.throwAway(paper)
  }
  override def toString = name
}

Ponieważ z pudełka mają być wyciągane kartki, to pudełko parametryzujemy typem Paper. Oznacza to, że jedynie pudełka zawierające kartki papieru mogą być użyte przez dziecko. Pudełka z inną zawartością nie nadają się do tego. Z kolei kosz na śmieci musi pozwalać na wyrzucenie kartek. Jeśli stosujemy segregowanie śmieci, to możemy mieć różne rodzaje koszy na śmieci: na papier, na szkło, na plastik. Jedynie takie kosze na śmieci, do których można wrzucać papier, nadają się do użycia przez projektowaną klasę. Wobec tego typ parametru wasteBin oznaczamy jako WasteBin[Paper].

Możemy teraz przystąpić do użycia projektowanych klas. Stwórzmy zatem obiekt paperBox, reprezentujący pudełko z papierem, oraz obiekt paperWasteBin, reprezentujący kosz na śmieci służący do wyrzucania papierowych przedmiotów.

scala> val paperBox = new Box[Paper]("paper box",new Paper)
paperBox: Box[Paper] = paper box

scala> val paperWasteBin = new WasteBin[Paper]("paper waste bin")
paperWasteBin: WasteBin[Paper] = paper waste bin

Teraz możemy już zamodelować rysujące dziecko.

scala> val peter = new DrawingChild("Peter",paperBox,paperWasteBin)
peter: DrawingChild = Peter

scala> peter.draw
Taking paper from the paper box
Drawing on paper
Throwing away paper into the paper waste bin

Spróbujmy zamodelować inne dziecko, które lubi rysować na żółtym papierze. W tym celu musimy wyposażyć je w pudełko z żółtym papierem. Takie pudełko modelujemy następująco.

scala> val yellowPaperBox = new Box[ColorPaper](
     |   "yellow paper box",new ColorPaper("yellow"))
yellowPaperBox: Box[ColorPaper] = yellow paper box

Spróbujmy utworzyć obiekt reprezentujący drugie z rysujących dzieci.

scala> val john = new DrawingChild("John",yellowPaperBox,paperWasteBin)
<console>:12: error: type mismatch;
 found   : Box[ColorPaper]
 required: Box[Paper]
Note: ColorPaper <: Paper, but class Box is invariant in type T.
You may wish to define T as +T instead. (SLS 4.5)
       val john = new DrawingChild("John",yellowPaperBox,paperWasteBin)
                                          ^

Próba utworzenia obiektu nie powiodła się. Obiekt yellowPaperBox jest typu Box[ColorPaper], a nie Box[Paper]. Taki obiekt nie może być użyty w miejscu, w którym jest wymagany obiekt typu Box[Paper], gdyż typ Box[ColorPaper] nie jest podtypem typu Box[Paper].

Ponieważ parametr typu klasy Box jest niezmienniczy, jeśli mamy dwie klasy A i B, gdzie A jest klasą nadrzędną dla B, to typ zdefiniowany jako Box[A] nie jest ani nadtypem, ani podtypem typu Box[B]. W opisywanym przykładzie mamy klasę Paper, która jest nadtypem klasy ColorPaper. Jednak ze względu na wspomnianą niezmienniczość, Box[Paper] nie jest nadtypem Box[ColorPaper]. W konsekwencji, obiekt typu Box[ColorPaper] nie może być użyty w miejscu, w którym jest wymagany obiekt typu Box[Paper].

Język programowania Scala Wydanie 2. Copyright © Grzegorz Balcerek 2016

Licencja Creative Commons

Ten utwór jest dostępny na licencji Creative Commons Uznanie autorstwa-Na tych samych warunkach 4.0 Międzynarodowe.

Oracle and Java are registered trademarks of Oracle and/or its affiliates. Other names may be trademarks of their respective owners.