20.4. Niejawne konwersje
Składowe klas, cech lub obiektów mogą definiować tak zwane niejawne konwersje z typu A na typ B. Mogą to być następujące rodzaje składowych pod warunkiem, że są zdefiniowane z użyciem modyfikatora implicit:
- wartość mająca typ funkcyjny (A)=>B, czyli funkcja jednoparametrowa, przyjmująca przekazany przez wartość argument typu A i zwracająca wartość typu B
- wartość mająca typ funkcyjny (=>A)=>B, czyli funkcja jednoparametrowa, przyjmująca przekazany przez nazwę argument typu A i zwracająca wartość typu B
- metoda przyjmująca przekazywany przez wartość lub przez nazwę parametr typu A i zwracająca wartość typu B
- klasa B mająca przekazywany przez wartość lub przez nazwę parametr typu A
Niejawne konwersje mogą być użyte w jednej z trzech sytuacji. Pierwsza taka sytuacja występuje, gdy wyrażenie a jest typu A, ale jest użyte w miejscu, w którym spodziewane jest wyrażenie typu B. W poniższym przykładzie wyrażenie 1 (typu Int) jest użyte w miejscu, w którym spodziewane jest wyrażenie typu String.
scala> var b: String = 1 <console>:10: error: type mismatch; found : Int(1) required: String var b: String = 1 ^
Z powodu niedopasowania typów przypisanie wartości 1 do zmiennej b nie udaje się. Przypisanie powiedzie się, gdy jawnie przekształcimy liczbę 1 na odpowiadający jej łańcuch znaków za pomocą metody toString, jak w poniższym przykładzie.
scala> var b: String = 1.toString b: String = 1
Tym razem przypisanie powiodło się, gdyż wyrażenie po prawej stronie znaku równości odpowiada typowi zmiennej definiowanej po lewej stronie. Taka jawna konwersja nie jest konieczna, jeśli dysponujemy niejawną konwersją z typu Int na typ String. Definicja takiej konwersji może wyglądać następująco.
scala> implicit val convIntToString = (x: Int) => x.toString convIntToString: Int => String = <function1>
Dysponując taką konwersją niejawną możemy zmienić wartość zmiennej b bez uciekania się do konwersji jawnej.
scala> b = 1 b: String = 1
Drugą sytuacją, w której może zostać użyta niejawna konwersja, jest przypadek wyrażenia a.b, w którym a jest typu A, natomiast b nie jest składową typu A. Konwersja może być zastosowana do wyrażenia a przed odwołaniem do składowej b, w celu konwersji wartości typu A na wartość typu B. Warunkiem koniecznym jest istnienie niejawnej konwersji z typu A na typ B taki, że typ B zawiera składową o nazwie b. Ilustruje to następny przykład. Następującego wyrażenia nie udaje się wywołać, gdyż w klasie Double (literał 123.45 jest typu Double) nie istnieje metoda length.
scala> (123.45).length <console>:12: error: value length is not a member of Double (123.45).length ^
Zdefiniujmy konwersję z typu Double na typ String. Obiekty typu String posiadają metodę length.
scala> import language.implicitConversions import language.implicitConversions scala> implicit def convDoubleToString(x: => Double):String = x.toString convDoubleToString: (x: => Double)String
Tym razem konwersja zdefiniowana jest nie za pomocą funkcji, jak poprzednio w przypadku konwersji z Int na String, tylko za pomocą metody. Użycie takiej formy wymaga aktywowania flagi funkcjonalności implicitConversions, co zostaje zrobione za pomocą klauzuli importu. Ponadto parametr metody jest przekazywany przez nazwę, a nie przez wartość. Podane wcześniej warunki dopuszczają taką postać definicji niejawnej konwersji. Teraz możemy wywołać poprzednie wyrażenie nie otrzymując komunikatu o błędzie.
scala> (123.45).length res1: Int = 6
To wyrażenie odpowiada następującemu wyrażeniu, które jawnie przekształca wartość typu Double na wartość typu String i dopiero na tej przekształconej wartości wywołuje metodę length.
scala> ((123.45).toString).length res2: Int = 6
Trzecią sytuacją, w której może zostać użyta niejawna konwersja, jest przypadek wyrażenia a.b(…), w którym a jest typu A, natomiast b jest nazwą składowej (składowych) istniejącej (istniejących) w typie A, ale z których żadna nie pasuje do argumentów użytych w wyrażeniu. Warunkiem koniecznym jest istnienie niejawnej konwersji z typu A na typ B taki, że typ B zawiera składową o nazwie b, która pasuje do użytych w wywołaniu argumentów. Ilustruje to następny przykład. Następującego wyrażenia nie udaje się wywołać, gdyż w klasie List nie istnieje metoda mkString przyjmująca dwa argumenty typu Char.
scala> List(1,2,3,4).mkString('|',',') <console>:14: error: overloaded method value mkString with alternatives: => String <and> (sep: String)String <and> (start: String,sep: String,end: String)String cannot be applied to (Char, Char) List(1,2,3,4).mkString('|',',') ^
Istnieje za to metoda mkString przyjmująca jeden argument. Istnieje także wersja przyjmująca trzy argumenty.
scala> List(1,2,3,4).mkString(",") res4: String = 1,2,3,4 scala> List(1,2,3,4).mkString("[",",","]") res5: String = [1,2,3,4]
Poniższa metoda definiuje konwersję listy na obiekt zawierający dwuargumentową metodę mkString.
scala> implicit class MkStringView(list: List[_]) { | def mkString(a: Char, b: Char) = | list.mkString(a.toString,b.toString,a.toString) | } defined class MkStringView
Po zdefiniowaniu takiej konwersji, wywołanie metody mkString z dwoma argumentami, które wcześniej nie działało, teraz zostaje wykonane.
scala> List(1,2,3,4).mkString('|',',') res6: String = |1,2,3,4|
Specyfikacja języka Scala opisuje niejawne konwersje w punkcie 7.3. |
Obiekt scala.Predef, którego składowe są standardowo importowane w programach Scali, zawiera definicje wielu niejawnych konwersji, co umożliwia na przykład wykonywanie poniższych wyrażeń.
scala> val r:Range = 1 to 5 r: Range = Range(1, 2, 3, 4, 5) scala> val a = (-1).abs a: Int = 1 scala> val b = 3 max 4 b: Int = 4 scala> 123.toHexString res7: String = 7b scala> "hello" -> "world" res8: (String, String) = (hello,world)
Klasa Range reprezentuje przedział liczb całkowitych. Metoda -> może być wywoływana nie tylko na napisach, ale na dowolnych wartościach.