toString
, hashCode
oraz equals
(wszystkie z java.lang.Object
). Jako powód powołał się na Joshua Bloch, który w Effective Java (Prentice Hall, 2008) napisał, że jest to dobra praktyka, która umożliwia obiektom uczestniczyć w kontrakcie określonym przez tablice haszujące (ang. hash tables) oraz przy ich wypisywaniu. Słusznie (aczkolwiek moje skromne programowanie nie obejmowało takich szczegółów).Może Wasze również? Jeśli tak, warto przyjrzeć się mechanizmowi klas przypadków (ang. case classes), dla których kompilator Scali generuje implementacje wspomnianych metod z pudełka oraz, co z pewnością ważniejsze, uczestniczą w mechaniźmie dopasowywania wzorców (ang. pattern matching).
Załóżmy, że mamy taką klasę
X
w Scali.case class X(x: Int)Na pierwszy rzut oka, jakby znajoma konstrukcja (szczególnie dla programistów javowych) do tworzenia definicji klas. Co rzuca się w oczy, to brak nawiasów klamrowych po definicji klasy, jeśli ciało będzie odpowiadało temu, które utworzy kompilator Scali (poniżej wyjaśnienie, czego należy oczekiwać).
Po kompilacji otrzymujemy zestaw użytecznych metod w klasie bez konieczności pisania ich własnoręcznie (!)
$ scalac X.scala $ ls X* X$.class X.class X.scala $ javap -classpath . X Compiled from "X.scala" public class X implements scala.Product,scala.Serializable { public static <A extends java/lang/Object> scala.Function1<java.lang.Object, A> andThen(scala.Function1<X, A>); public static <A extends java/lang/Object> scala.Function1<A, X> compose(scala.Function1<A, java.lang.Object>); public int a(); public X copy(int); public int copy$default$1(); public java.lang.String productPrefix(); public int productArity(); public java.lang.Object productElement(int); public scala.collection.Iterator<java.lang.Object> productIterator(); public boolean canEqual(java.lang.Object); public int hashCode(); public java.lang.String toString(); public boolean equals(java.lang.Object); public X(int); }Tworzenie obiektów klas przypadków jest możliwe bez słówka kluczowego
new
oraz domyślnie wszystkie parametry konstruktora stają się atrybutami obiektu jedynie do odczytu. Klasa przypadku jest niezmienna.scala> case class X(x: Int) defined class X scala> val x = new X(5) x: X = X(5) scala> x res0: X = X(5) scala> val y = X(5) y: X = X(5) scala> y res1: X = X(5) scala> x == y res2: Boolean = true scala> x equals y res3: Boolean = true scala> x.equals(y) res4: Boolean = trueI kontynuując dalej poszukiwania udogodnień związanych z klasami przypadku w Scala REPL:
scala> case class X(x: Int) defined class X scala> X.[wciśnij TAB] andThen apply asInstanceOf compose isInstanceOf toString unapply scala> val x = X(5) x: X = X(5) scala> x. asInstanceOf canEqual copy isInstanceOf productArity productElement productIterator productPrefix toString x scala> x.x res0: Int = 5 scala> x.toString res1: String = X(5) scala> x.hashCode res2: Int = -1267080172 scala> x.equals(x) res3: Boolean = true scala> x.equals(X(5)) res4: Boolean = trueSiłę klas przypadku można dostrzeć przy dopasowywaniu wzorców, z którymi stanowią nierozerwalną parę w Scali.
Martin Odersky, Lex Spoon oraz Bill Venners w swojej książce Programming in Scala, Second Edition (Artima, 2011) napisali, że klasy przypadku są odpowiedzią Scali na umożliwienie dopasowywania wzorców (znanego z języków funkcyjnych) na obiektach bez konieczności pisania nużącego, ale niezbędnego kodu wspierającego. Wystarczy przed nazwą klasy napisać "case" i po krzyku.
Tak na marginesie, jakby się tak zastanowić nad potencjalną genezą nazwy "case classes", to przy znajomości dopasowywania wzorców w Scali - konstrukcja
match
-case
- możnaby wysnuć wniosek, że one są dla siebie stworzone. Nawet nazwy je łączą. O pomyłkę trudno.scala> X(10) match { | case X(10) => "Obiekt X z 10 wewnątrz" | case _ => "Coś bliżej niezidentyfikowanego" | } res5: String = Obiekt X z 10 wewnątrz scala> X(3) match { | case X(num) => s"Obiekt X z ${num}" | case _ => "Coś bliżej niezidentyfikowanego" | } res6: String = Obiekt X z 3I inne temu podobne, które dotyczą dopasowywania wzorców. Ciekawy mechanizm.
Na zakończenie warto wspomnieć o uproszczeniach w tworzeniu nowych obiektów na bazie istniejących z utworzoną metodą
copy
.scala> case class Y(a: Int, b: String, c: X) defined class Y scala> val y = Y(5, "Five", x) y: Y = Y(5,Five,X(5)) scala> y.copy(a=500) res6: Y = Y(500,Five,X(5)) scala> y.copy(a=500, b="Five Hundred") res7: Y = Y(500,Five Hundred,X(5)) scala> y.copy(c=X(500)) res8: Y = Y(5,Five,X(500))Niskim kosztem (tworzenia klas przypadków) otrzymujemy stosunkowo bogatą funkcjonalność. To musi się podobać!
Warto przeczytać:
hashCode i equals to SZCZEGÓŁY? oO
OdpowiedzUsuńJaki programista, takie szczegóły :-) A Ty jak piszesz swoje implementacje? Korzystasz z Apache Commons, czy coś innego? Uchyl rąbka tajemnicy, proszę.
UsuńNie jestem, nie byłem i nigdy nie bede programista javy, ale nawet ja wiem ze hashCode i equals to so podstawowe podstawy obiektow w javie, rzeczy ktore po prostu trzeba znac. Mam nadzieje ze jedyne co robisz to wyklikiwanie aplikacji bo bałbym sie jakiegokolwiek Twojego kodu.
UsuńMój kod możesz obejrzeć na https://github.com/jaceklaskowski. Nie ma tam wiele projektów, ale chociaż te istniejące mogą dać Ci pogląd, ile należałoby zmienić. Może zechciałbyś wskazać miejsca do poprawki? Chętnie poprawię.
UsuńA może masz może pomysł na projekt, który zechciałbyś zweryfikować/porównać do swojego (niechby to był inny język programowania) ku poprawieniu mojego/naszego warsztatu programistycznego?! Właśnie rzucam rękawicę. Podniesiesz? ;-)
"domyślnie wszystkie parametry konstruktora stają się atrybutami obiektu jedynie do odczytu"
OdpowiedzUsuńDotyczy to jedynie pierwszej listy parametrów.
scala> case class X(x:Int)(y:Int)
defined class X
scala> val x = X(5)(2)
x: X = X(5)
scala> x.x
res0: Int = 5
scala> x.y
:11: error: value y is not a member of X
x.y
^
Dzięki Grzegorz! Moja wiedza scalowa na razie nie skaluje się tak efektywnie i niestety mój umysł nie obejmuje tego :( Wiem, że wielokrotne listy parametrów mają pomóc kompilatorowi we wnioskowaniu typów (tyle teoria). Czy w tym przypadku również? Możesz podać przykład, który unaoczniłby mi to? Proszę...
UsuńKilka możliwych zastosowań wielu list parametrów:
Usuń- listę z jednym parametrem można w wywołaniu objąć w nawiasy klamrowe zamiast okrągłych co może dać ładniejszą składnię wywołania
- jeśli mamy argument funkcyjny, może być wygodnie (z powodu składni znowu) mieć go w osobnej liście parametrów
- możliwość zastosowania wielu parametrów powtórzonych/domyślnych
- lista parametrów przekazywanych niejawnie (listy ze słowem kluczowym implicit)
- o wnioskowaniu typów sam już wspomniałeś
Akurat powyższy przykład z klasą przypadku jest sztuczny, teoretyczny, mający jedynie coś pokazać.
Ale jak coś jest możliwe to może i zastosowanie się kiedyś znajdzie.