Miałam do zrobienia prostą funkcjonalność. Należało przedstawić różne obiekty, znajdujące się w tablicy, w postaci jednego łańcucha znaków. Sam problem jest trywialny, ale podczas jego implementacji zaczęłam się zastanawiać: Jak obiekty będą wyglądać po rzutowaniu na łańcuchy znaków? Tu akurat odpowiedź jest krótka - dobrze ;) Ciekawsza jest jednak odpowiedź na pytanie: Dlaczego po rzutowaniu obiekty wyglądają w określony sposób? To właśnie tym zagadnieniem zajmiemy dzisiaj się. Poszukiwania odpowiedzi czas zacząć. Aha nie zapominajmy, że to będzie niezła zabawa. Zapraszam!
Rzutowanie obiektów na łańcuchy znaków
Zacznijmy od prostego przykładu. Mamy tablice, w której znajdują się dwa obiekty. Jeden obiekt typu String
, a drugi typu Symbol
:
[:symbol, 'string'].join(' ')
=> "symbol string"
Wszystko wygląda dobrze. Można się było tego spodziewać. Po połączeniu tych dwóch obiektów, dostajemy łańcuch znaków. No dobrze, a co w przypadku tablicy tablic? Jak wtedy zachowa się metoda join
?
[[:symbol], 'string'].join(' ')
=> "symbol string"
Super! Nie musimy się przejmować tablicą. Dostajemy dokładnie to, co chciałybyśmy dostać. Podobnie jest w przypadku metody puts
. Na ekranie zostają nam wyświetlone elementy tablicy. Każdy element w osobnej linii.
puts [:one, :two, :three]
one
two
three
=> nil
I tu pojawia się najważniejsze pytanie! Dlaczego metody join
i puts
zachowują się w ten sposób? Jeżeli samodzielnie zamienimy tablice na łańcuch znaków to wygląda to inaczej niż w przypadku metody join
czy puts
.
[:symbol].to_s
=> "[:symbol]"
Moja logika podpowiada: Skoro metoda to_s
zwraca "[:symbol]"
, to metoda join
powinna zwracać "[:symbol] string"
. Tak niestety nie jest. Dlaczego? Tu zaczyna się prawdziwy research. Na początek skupimy się na różnicy między metodą to_s
a join
.
Co kryje się wewnątrz metody join?
Najpierw odkryłam, że metoda join
nie używa do rzutowania obiektów metody to_s
. A przynajmniej tak mi się na początku wydawało, bo jak się później przekonamy jest to troszkę bardziej skomplikowane. Na razie idźmy tym tropem. Metoda join
używa do rzutowania metody to_str
. Zobaczmy to na przykładzie. Deklarujemy nowa klasę i sprawdzimy jak zachowa się na niej metoda join
.
class RubyStringTest
def to_s
'calls to_s'
end
def to_str
'calls to_str'
end
end
string_test = RubyStringTest.new
string_test.to_s
=> "calls to_s"
[:it, string_test, :method].join(' ')
=> "it calls to_str method"
Widzimy, że przy metodzie join
zostaje wywołana metoda to_str
. Tylko dlaczego mamy te dwie metody? Wygląda na to, że robią to samo. To po co się powtarzać? Otóż nie zawsze robią to samo i nie maja tego samego przeznaczenia.
"String".to_s
=> "String"
"String".to_str
=> "String"
Różnica między metodą to_s a metodą `to_str
Metoda to_s
jest zdefiniowana w klasie Object
, więc wszystkie obiekty w języku Ruby posiadają tę metodę. Natomiast metoda to_str
nie jest zaimplementowana we wszystkich klasach Rubiego. Kiedy spróbujemy wywołać ją na tablicy dostaniemy wyjątek:
[:symbol].to_str
Traceback (most recent call last):
4: from /home/agnieszka/.rvm/rubies/ruby-2.6.0/bin/irb:23:in `<main>'
3: from /home/agnieszka/.rvm/rubies/ruby-2.6.0/bin/irb:23:in `load'
2: from /home/agnieszka/.rvm/rubies/ruby-2.6.0/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
1: from (irb):7
NoMethodError (undefined method `to_str' for [:symbol]:Array)
Did you mean? to_set
to_s
Wynika to ze znaczenia tych dwóch metod. Metoda to_s
pozwala nam na rzutowanie obiektów na łańcuchy znaków. Metoda to_str
pozwala naszemu obiektowi zachowywać się jak łańcuch znaków. By to zademonstrować wróćmy do przykładu z RubyStringTest
:
string_test = RubyStringTest.new
"This test #{string_test}"
=> "This test calls to_s"
"This test " + string_test
=> "This test calls to_str"
Widzisz różnicę? W pierwszym przypadku obiekt jest rzutowany na łańcuch znaków, w drugim chcemy by zachowywał się jak łańcuch znaków. Chcemy by tak samo jak łańcuch znaków odpowiadał na metodę :+
. Kiedy spojrzymy na przykład z tablicą zobaczymy, że tablica nie zachowuje się jak łańcuch znaków. Tablica co prawda ma zaimplementowaną metodę :+
ale metoda ta nie łączy się z łańcuchami znaków.
"This is an array #{[:symbol]}"
=> "This is an array [:symbol]"
'This is an array' + [:symbol]
Traceback (most recent call last):
6: from /home/agnieszka/.rvm/rubies/ruby-2.6.0/bin/irb:23:in `<main>'
5: from /home/agnieszka/.rvm/rubies/ruby-2.6.0/bin/irb:23:in `load'
4: from /home/agnieszka/.rvm/rubies/ruby-2.6.0/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
3: from (irb):8
2: from (irb):8:in `rescue in irb_binding'
1: from (irb):8:in `+'
TypeError (no implicit conversion of Array into String)
Oczywiście w języku Ruby możemy sprawić by tak się stało. Nie jest to jednak domyślne zachowanie.
class Array
def to_str
to_s
end
end
'This is an array ' + [:symbol]
=> "This is an array [:symbol]"
No dobrze, ale to nie jest odpowiedź na nasze pytanie. W końcu w poprzednim przykładzie widać było, że tablica nie zachowuje się jak łańcuch znaków i domyślnie nie ma zaimplementowanej metody to_str
. Jeżeli spróbujemy wywołać metodę to_str
na tablicy dostaniemy wyjątek:
[:symbol].to_s
=> "[:symbol]"
[:symbol].to_str
Traceback (most recent call last):
4: from /home/agnieszka/.rvm/rubies/ruby-2.6.0/bin/irb:23:in `<main>'
3: from /home/agnieszka/.rvm/rubies/ruby-2.6.0/bin/irb:23:in `load'
2: from /home/agnieszka/.rvm/rubies/ruby-2.6.0/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
1: from (irb):2
NoMethodError (undefined method `to_str' for [:symbol]:Array)
Did you mean? to_set
to_s
Jak metoda join działa na tablicach?
By zrozumieć metodę join
musimy udać się do źródła, a dokładnie do kodu źródłowego języka Ruby. Zobaczymy tam następujące wyjaśnienie:
/*
* call-seq:
* ary.join(separator=$,) -> str
*
* Returns a string created by converting each element of the array to
* a string, separated by the given +separator+.
* If the +separator+ is +nil+, it uses current <code>$,</code>.
* If both the +separator+ and <code>$,</code> are +nil+,
* it uses an empty string.
*
* [ "a", "b", "c" ].join #=> "abc"
* [ "a", "b", "c" ].join("-") #=> "a-b-c"
*
* For nested arrays, join is applied recursively:
*
* [ "a", [1, 2, [:x, :y]], "b" ].join("-") #=> "a-1-2-x-y-b"
*/
To jest opis, jak działa metoda join
. Możemy tu zauważyć dwa istotne dla nas zdania: Returns a string created by converting each element of the array to a string, separated by the given separator. For nested arrays, join is applied recursively. Tłumacząc na język polski: Zwraca łańcuch znaków konwertując każdy element tablicy na łańcuch znaków odzielony podanym separatorem. Dla zagnieżdżonej tablicy metoda join zachowuje się rekurencyjnie. Znaczy to, że metoda to_str
nie jest wywoływana na całej tablicy, ale na każdym jej elemencie. Co pasuje do następującego przykładu:
[[:symbol], 'string'].join(' ')
=> "symbol string"
:symbol.to_str
Traceback (most recent call last):
4: from /home/agnieszka/.rvm/rubies/ruby-2.6.0/bin/irb:23:in `<main>'
3: from /home/agnieszka/.rvm/rubies/ruby-2.6.0/bin/irb:23:in `load'
2: from /home/agnieszka/.rvm/rubies/ruby-2.6.0/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
1: from (irb):17
NoMethodError (undefined method `to_str' for :symbol:Symbol)
Did you mean? to_s
to_sym
Moment! Przecież nie pasuje! Klasa Symbol
podobnie jak tablica nie implementuje metody to_str
. Wróćmy jeszcze raz do naszego przykładu RubyStringTest
, otwórzmy nową konsole i zróbmy to powoli:
class RubyStringTest
end
string_test = RubyStringTest.new
string_test.to_s
=> "#<RubyStringTest:0x000055567edb0bd0>"
[:it, string_test, :method].join(' ')
=> "it #<RubyStringTest:0x000055567edb0bd0> method"
W tym momencie join
używa metody to_s
, a nie to_str
jak na początku zakładałyśmy. Kiedy nadpiszemy metodę to_s
zobaczymy naszą implementację:
class RubyStringTest
def to_s
'calls to_s'
end
end
string_test = RubyStringTest.new
string_test.to_s
=> "calls to_s"
[:it, string_test, :method].join(' ')
=> "it calls to_s method"
Natomiast kiedy zaimplementujemy jeszcze metodę to_str
, to join
zacznie jej używać.
class RubyStringTest
def to_s
'calls to_s'
end
def to_str
'calls to_str'
end
end
string_test = RubyStringTest.new
string_test.to_s
=> "calls to_s"
[:it, string_test, :method].join(' ')
=> "it calls to_str method"
Wyciągając wniosek z tego przykładu, metoda join
na początku swojego działania próbuje wywołać metodę to_str
. Gdy ta metoda nie jest zaimplementowana w obiekcie, to wykorzystuje metodę to_s
. To wytłumaczenie pasuje do naszych obserwacji:
[[:symbol], 'string'].join(' ')
=> "symbol string"
:symbol.to_s
=> "symbol"
Jak na tablicy działa metoda puts?
Podobne zachowanie możemy zauważyć dla metody puts
. Kiedy wywołamy ją na tablicy, na ekranie zobaczymy elementy tej tablicy. Każdy z elementów będzie się znajdował w osobnej linii.
[1, 2, 3].to_s
=> "[1, 2, 3]"
puts [1, 2, 3]
1
2
3
=> nil
Zarówno join
jak i puts
wywołują metodę to_str
czy metodę to_s
na elementach tablicy, a nie na samej tablicy. Podobnie jak w przypadku metody join
, dla metody puts
też możemy to sprawdzić w kodzie Rubiego.
/*
* call-seq:
* ios.puts(obj, ...) -> nil
*
* Writes the given object(s) to <em>ios</em>.
* Writes a newline after any that do not already end
* with a newline sequence. Returns +nil+.
*
* The stream must be opened for writing.
* If called with an array argument, writes each element on a new line.
* Each given object that isn't a string or array will be converted
* by calling its +to_s+ method.
* If called without arguments, outputs a single newline.
*
* $stdout.puts("this", "is", ["a", "test"])
*
* <em>produces:</em>
*
* this
* is
* a
* test
*
* Note that +puts+ always uses newlines and is not affected
* by the output record separator (<code>$\\</code>).
*/
Podsumowanie
- Istnieją dwie metody pozwalające nam być jak łańcuch znaków:
- metoda
to_s
- pozwala rzutować obiekty na łańcuch znaków. Każdy obiekt ma tą metodę. - metoda
to_str
- pozwala na zachowywanie się jak łańcuch znaków. Nie wszystkie obiekty posiadają tą metodę.
- metoda
- Metoda
join
używa metodyto_str
, gdy ta metoda jest zaimplementowana. W przeciwnym wypadku używa metodyto_s
. - Metoda
puts
zawsze używa pod spodem metodyto_s
. - W przypadku tablicy metody
join
iputs
wywołują metodyto_str
ito_s
na każdym elemencie tablicy. - Klasy
Symbol
iArray
nie implementują metodyto_str
z automatu.
Potrzebujesz pomocy?
Jeśli szukasz doświadczonej programistki Ruby z ponad dziesięcioletnim stażem, śmiało skontaktuj się ze mną.
Mam doświadczenie w różnych domenach, a szczególną wagę przykładam do szybkiej reakcji na opinie użytkowników i pracy zespołowej. Pomogę Ci stworzyć świetny produkt.