Metody

edytuj

Czym jest metoda?

edytuj

W programowaniu obiektowym nie myślimy o operowaniu na danych bezpośrednio spoza obiektu. Obiekty mają raczej pewne rozumienie tego jak należy operować na sobie samych (gdy ładnie poprosimy by to robiły). Można powiedzieć, że przekazujemy pewne wiadomości do obiektu i te wiadomości zazwyczaj powodują jakiegoś rodzaju akcję lub uzyskują znaczącą odpowiedź. Powinno to dziać się bez naszego zaangażowania w to, jak obiekt naprawdę działa od wewnątrz. Zadania, o wykonanie których mamy prawo prosić obiekt (lub równoważnie - wiadomości które on zrozumie) są właśnie owymi metodami obiektu.

W Rubim wywołujemy metody obiektu posługując się zapisem z kropką (tak jak w C++ lub Javie). Nazwa obiektu do którego mówimy znajduje się na lewo od kropki.

"abcdef".length #=> 6

Rozumując intuicyjnie, ten łańcuch pytany jest o swoją długość. Technicznie natomiast, wywołujemy metodę length na rzecz obiektu "abcdef".

Pozostałe obiekty mogą mieć nieco inną interpretację długości lub nawet nie mieć żadnej. Decyzje dotyczące tego, jak odpowiedzieć na wiadomość podejmowane są w locie, podczas wykonywania programu, i podejmowane działanie może być zmienione w zależności o tego, na co wskazuje zmienna.

a = "abc"
puts a.length #=> 3
a = ["abcde", "fghij"]
a.length #=> 2

To, co rozumiemy przez długość może się różnić w zależności od rodzaju obiektu do którego mówimy. Za pierwszym razem w powyższym przykładzie pytamy a o jej długość i a wskazuje na prosty łańcuch znakowy, więc jest tylko jedna sensowną odpowiedź. Za drugim razem a odnosi się do tablicy i możemy rozsądnie myśleć o jej długości jako o 2, 5 lub 10 - oczywiście w tym przypadku jest to 2. Różne obiekty mogą mieć różnego rodzaju długości.

a[0].length #=> 5
a[0].length + a[1].length #=> 10

Rzeczą godną uwagi jest to, że tablica wie o sobie to coś, co oznacza, że jest ona tablicą. W Rubim kawałki danych przechowują tę wiedzę. Zatem żądania, które wobec nich kierujemy mogą być automatycznie spełnione na wiele różnych sposobów. Zdejmuje to z programisty brzemię pamiętania olbrzymiej liczby wielu specyficznych nazw funkcji, ponieważ relatywnie mała liczba nazw metod (będących w zgodzie koncepcjami wyrażalnymi w języku naturalnym) może być zastosowana do różnych typów danych. W rezultacie programista otrzymuje to, czego się spodziewał. Ta cecha języków programowania obiektowego nazywana jest polimorfizmem.

Kiedy obiekt otrzymuje komunikat, którego nie rozumie, "podnoszony" jest błąd:

a = 5
a.length
ERR: (eval):1: undefined method `length' for 5(Fixnum)

Tak więc należy wiedzieć, które metody są akceptowane przez obiekt, chociaż nie trzeba analizować jak są one przetwarzane.

Jeżeli przekazujemy do metody jakieś argumenty, zazwyczaj otaczamy je nawiasami okrągłymi:

obiekt.metoda(arg1, arg2)

Można je pominąć, jeśli nie stanie się to przyczyną dwuznaczności[1].

obiekt.metoda arg1, arg2

Jest pewna specjalna zmienna w Rubim - self. Odnosi się ona tylko do obiektu na rzecz którego wywołujemy metodę. Dzieje się to tak często, że dla wygody "self." może być opuszczone w metodach odwołujących się z danego obiektu do samego obiektu:

self.nazwa_metody(argumenty...)

oznacza to samo co

nazwa_metody(argumenty...)

To co tradycyjnie nazwalibyśmy wywołaniem funkcji jest po prostu skróconą formą zapisu wywołań metod przez self. To właśnie czyni z Rubiego czysto obiektowy język programowania. Ponadto metody funkcyjne nadal zachowują się całkiem podobnie do funkcji w innych językach programowania. Jest to pewne ułatwienie dla tych, którym łatwiej jest traktować wywołania metod jak wywołania funkcji. Jeśli chcemy, możemy, np. w celach edukacyjnych, traktować funkcje tak jakby nie były one naprawdę metodami obiektów.

W rozdziale dotyczącym zmiennych klasowych zobaczymy zastosowanie słowa kluczowego self przy definiowaniu metod należących do całej klasy, czyli metod klasowych.

* czyli zmienna lista argumentów

edytuj

Czasami, analizując różne przykłady kodu w Rubim możemy natknąć się na taką definicję metody (albo wywołanie), w której ostatni parametr poprzedzony jest znakiem * lub &. Dla początkujących może to wyglądać enigmatycznie, a programistów C/C++/C# mogą dodatkowo mylić skojarzenia ze wskaźnikami i referencjami. Obydwa znaki mają jednak zupełnie inne znaczenie, a ponieważ nie ma nic gorszego od kodu, którego nie rozumiemy, wyjaśnijmy znaczenie obu tych symboli.

Gwiazdka (*) oznacza zmienną listę argumentów. Jeżeli * pojawia się w nagłówku definiowanej metody, poprzedzając ostatni parametr, oznacza to, że począwszy od tego argumentu do metody można przekazać dowolną ich ilość. Wszystkie te argumenty są widoczne w metodzie jako tablica.

def metoda(*args)
  wynik = ""
  args.each {|arg| wynik += "#{arg}, "}
  wynik[0...-2] # ucinamy 2 ostanie znaki: ", "
end

puts metoda("a", "b", 3) #=> "a, b, 3"

Gwiazdkę * można też stosować w wywołaniu metody, przed ostatnim argumentem - tablicą. Powoduje ona wtedy konwersję z tablicy na poszczególne argumenty:

def inna_metoda(a, b, c)
  "#{a}, #{b}, #{c}"
end

puts inna_metoda(*["a", "b", 3]) #=> "a, b, 3"
puts inna_metoda("a", *["b", 3]) #=> "a, b, 3"

& czyli przekazywanie bloku

edytuj

Poznaliśmy już domknięcia i sposoby przekazywania ich do metody. Domknięcie możemy przekazać, definiując je bezpośrednio za nazwą metody. Natomiast obiekt procedurowy możemy przekazywać jako parametr. Wiemy też, że sterowanie do domknięcia przekazujemy przez yield, natomiast procedurę obiektu procedurowego wywołujemy przez metodę call. Co jednak, gdy chcielibyśmy użyć bloku przekazanego jako domknięcie tak jakby był obiektem (stosując call zamiast yield)? Albo gdybyśmy chcieli utworzony już obiekt procedurowy przekazać tak jakby był blokiem?

Rozważmy naszą metodę powtorz z rozdziału o domknięciach:

def powtorz(ilosc)
  while ilosc > 0
    yield ilosc
    ilosc -= 1
  end
end

Aby przekazać do tej metody blok, który mamy w postaci np. lambdy, należy użyć symbolu & i przekazać nasz blok jako ostatni (niby fikcyjny) argument. Fikcyjny, bo nie jest on jawnie zdefiniowany w nagłówku metody.

l = lambda { |x| print x }
powtorz(3, &l) #=> 321

Efekt jest taki sam jakbyśmy przekazali blok tradycyjnie:

powtorz(3) { |x| print x } #=> 321

Symbolu & możemy też używać przed ostatnim parametrem w definicji metody. Dzięki temu, możemy uzyskać niejako odwrotne działanie: odwoływać się do bloku jak do obiektu procedurowego:

def powtorz(ilosc, &blok)
  while ilosc > 0
    blok.call(ilosc) # to samo co yield ilosc
    ilosc -= 1
  end
end

powtorz(3) { |x| print x } #=> 321

l = lambda { |x| print x }
powtorz(3, &l) #=> 321

Jak widzimy, jawne przekazanie obiektu l jako bloku również jest poprawne.

Wiele wartości w instrukcji return

edytuj

Na koniec powróćmy jeszcze do niuansów stosowania instrukcji return. Domyślnie metoda zwraca ostatnie obliczone wyrażenie, choć można oczywiście zastosować słowo kluczowe return, by metoda zwróciła konkretną wartość. Jednak w Rubim, w odróżnieniu od C/C++, Javy czy C# instrukcja return może zwracać więcej niż jedną wartość. Rozważmy taki przykład:

def metoda
   return "a", 0, "b"
end

tab = metoda
t1, t2 = metoda

puts tab.class   # => Array
puts t1.class    # => String
puts t2.class    # => Fixnum

Jak widzimy, jeżeli użyjemy jednej zmiennej metoda zwróci nam tablicę, w której będą wszystkie wartości wyrażeń przekazanych do instrukcji return. Jeżeli po lewej stronie przypisania wyniku metody umieścimy więcej niż jedną zmienną (t1, t2, itd.) będą do nich podstawione kolejne wartości zwracane przez return. Jeżeli zmiennych po lewej stronie będzie mniej niż wartości zwracanych przez metodę, "nadmiarowe" wartości zostaną zignorowane (jak ma to miejsce wyżej). Jeżeli natomiast będzie ich więcej, "nadmiarowe" zmienne dostaną wartości nil:

a1, a2, a3, a4 = metoda
puts a4.nil? #=> true

Przypisy

  1. Zaleca się jednak pomijanie nawiasów tylko w wywołaniach najprostszych i najbardziej oczywistych metod, jak np. puts.