Ruby - ściągawka

Ruby jest dosyć rozbudowanym językiem pod względem składni. Sam język ma lukier składniowy (syntax sugar) prawie na każdy element. Dlatego poniżej wrzucam małą ściągawkę ze składni. Post jest dosyć rozbudowany, dlatego polecam go przeszukiwać, a nie czytać w całości.


Ten wpis jest jednym z serii o Ruby, które pojawiły się na blogu:

  1. Ruby - ściągawka
  2. Metody na metody w Ruby
  3. Bloki i moduły w Ruby

Spis treści wpisu

Warto zaznaczyć, że w Ruby wielkość znaków ma znaczenie. Nazwy zmiennych Ble oraz ble są rozróżniane i wykrywane jako dwie różne. Nazwy zmiennych, metod i symboli piszemy snake_casem. Klasy i moduły nazywamy CamelCasem. Jako tab używamy dwóch spacji. Więcej o konwencji nazewniczej można znaleźć tutaj

  • puts 'tekst' - wyświetlenie tekstu w konsoli
  • name = gets - pobranie danych z konsoli do zmiennej name
  • test.class - zwróci nazwę klasy zmiennej test
  • test.methods - lista wszystkich metod zmiennej test
  • test.public_methods - lista tylko metod publicznych
  • String.superclass - klasa nadrzędna danej klasy
  • "test " * 4 zwróci "test test test test"
  • [1, 2] * 2 zwróci [1, 2, 1, 2]
  • a = a.strip można zapisać jako a.strip! - wywołanie funkcji z wykrzyknikiem nadpisuje wartość
  • Liczby binarne zapisuje się jako 0b101010, szesnastkowe jako 0xFA5B.
  • W liczbach można używać podkreślników, Ruby je ignoruje, np. 564_8758_98 da nam wynik 564875898
  • Komentarze zaczyna się od hasha (znak #)
  • b = a.clone - klonuje zmienną lub obiekt. Zwykłe przypisanie tylko kopiuje referencję
  • p obiekt - wyświetla wszystkie wartości zmiennych danego obiektu. Jest to skrót od object.inspect
#<Spaceship:0x02a2f7e0 @name="serenity", @cargo=12, @weight=222.22>

Zmienne

  • zmienna = 10, b = 'test', c = false - zadeklarowanie zmiennych i przypisanie wartości
  • tablica = [1, 2, 3, 4] - zwykła tablica
  • $zmienna_globalna - zaczyna się od znaku dolara

Metody

Metody zawsze zwracają to, co jest w ostatniej linijce. Nie używa się słowa return.

def metoda(argument)
  argument * 2
end

Typy wbudowane

  • true to obiekt klasy TrueClass. false to obiekt klasy FalseClass
  • Po Integer dziedziczą dwie klasy: Fixnum (mniejsze liczby) oraz Bignum (duże liczby).
  • LiczbęFloat zapisujemy jako 2.0
  • Obiekty reprezentujące liczby są read only! Dlatego nie ma operatora ++ ani --
  • Wyświetlanie znaków specjalnych w stringach za pomocą %q:
%q('Zielony' buszmen) # => 'Zielony' buszmen
%q<'Zielony' buszmen>
%q{'Zielony' buszmen}
  • Podwójne cudzysłowy " umożliwiają interpolację:
counter = 20
qty = 2
puts "Licznik: #{counter}" # interpolacja
puts "Ilość: #{counter * qty}"
  • Metody i operatory klasy String:
puts "Hello world"[1] # => "e"
puts "Hello world"[1, 4] # => "ello"

a = "Hello world"
a["world"] = "sky"
puts a # => "Hello sky"

puts "ble " * 3 # => "ble ble ble "

puts "%05d" % 321 # => "00321"
puts "%.4g" % 3.14151692653 # => "3.142"

puts "aa" + "bbb" # => "aabbb"

Tablice

arr = [] # pusta tablica
arr = Array.new(3) # [nil, nil, nil]
arr = Array.new(3, "abc") # ["abc", "abc", "abc"], każdy element to ten sam obiekt
arr = Array.new(3) { "abc" } # ["abc", "abc", "abc"], każdy element to inny obiekt
arr[-1] # ostatni element tablicy
arr[1..3] # zwróci elementy od 1 do 3 włącznie
arr = [1, 2, 3]
arr = [:a, :bb, :ccc]
arr = ['info', 2, true]
arr = [[1, 2], [3, 4]]

Hashe

h = {} # pusty hash
h = {"min" => 0, "max" => 10}
h["max"]

z = {:min => 0, :max => 10}
z = {min: 0, max: 10}
z[:max]

z.each { |k, v| puts "#{k}: #{v}" } # wyświetlenie wszystkich wartości hasha

Zakresy (Range)

(1..7)  # [1, 7]
(1...7) # [1, 7)

Splat Operator

a, *b = get_values  # a == 1, b == [2, 3, 4]
a, *b, c = get_values  # a == 1, b == [2, 3], c == 4

Klasy i obiekty

class Spaceship
  def launch(destination)
    @destination = destination
  end
end

ship = Spaceship.new
ship.launch 'Andromeda'

Jest to klasa Spaceship, która zawiera metodę launch. Metoda ta przyjmuje jeden parametr - destination. Konstrukcja @destination to odwołanie się do stanu obiektu (odpowiednik this.zmienna z C#)

Na samym dole tworzymy obiekt tej klasy poprzez wywołanie metody new. Następnie wywołujemy metodę launch przekazując do niej string 'Andromeda'.

Metody w klasie są domyślnie publiczne, zmienne są domyślnie prywatne.

Akcesory

class Person
  def name
    @name
  end

  def name=(str)
    @name = str
  end
end

pablo = Person.new
pablo.name = 'Pablo' # Przypisanie wartości
p pablo.name # wyświetlenie wartości => "Pablo"

Pierwsza funkcja to getter, zwraca zmienną name, natomiast druga to stter - ustawia jej wartość.

Powyższy kod można skrócić - Ruby oferuje tzw. akcesory. Zobaczmy:

class Person
  attr_accessor :name
end

pablo = Person.new
pablo.name = 'Pablo'
p pablo.name # => "Pablo"

Powyższa konstrukcja (attr_accessor) umożliwia odczyt i zapis zmiennej. Aby umożliwić sam odczyt, możemy skorzystać z attr_reader. Istnieje analogiczny sposób na umożliwienie samego zapisu - attr_writer

Konstruktory - initialize

class Person
  def initialize(name, age, height)
    @name = name
    @age = age
    @height = height
  end
end

zielony = Person.new('Zielony', 128, 12.5)

Konstruktor tworzymy za pomocą metody initialize. Tworząc nowy obiekt, wywołanie metody new wywołuje także metodę initialize z naszej klasy.

Dziedziczenie - <

class Probe
  def deploy
    # wypuszcza próbnik
  end

  def take_sample
    # pobiera ogólną próbkę
  end
end


class MineralProbe < Probe
  def take_sample
    # pobiera próbkę minerałów
    super() # wywołanie przesłoniętej metody z klasy rodzica
  end
end


class SpaceProbe < Probe
  def take_sample
    # pobiera próbkę z przestrzeni kosmicznej
    super # wywołanie przesłoniętej metody z klasy rodzica
  end
end

Dziedziczenie jest realizowane operatorem <. Aby odwołać się do metody z klasy nadrzędnej, wywołujemy metodę super() (możliwa notacja bez nawiasów - super).

Zmienne i metody klasy

class Car
  @@amount = 0 # zmienna klasowa

  def initialize
    @@amount += 1
  end

  def self.count # metoda klasy
    @@amount
  end

  def count_normally # metoda instancji
    @@amount
  end
end

Car.new
Car.new
bla = Car.new

p bla.count_normally # => 3
p Car.count # => 3

Zmienna amount jest taka sama we wszystkich klasach, które by dziedziczyły po klasie Car

Inne:

Konstrola dostępu (private, public)

class Probe
  private # wszystkie metody będą prywatne

  def deploy 
    # ...
  end

  public # wszystkie metody będą publiczne

  def take_sample
    # ...
  end

  def turn_off
    # ...
  end
end

Identyczne działanie zapewnia konstrukcja:

class Probe
  def deploy
  end

  def take_sample
  end

  def turn_off
  end

  private :deploy # wpisujemy metody, które mają być prywatne
  public :take_sample, :turn_off # to jest nadmiarowe, bo domyślnie wszystkie metody są publiczne
end

Monkey patching & Open classes

Termin Open classes oznacza, że możemy deklarować jedną klasę w wielu miejscach, dodając jej po jednej metodzie. Możemy też dowolnie nadpisywać jakieś metody innymi. Dzięki temu, Ruby umożliwia tzw. Monkey Patching, czyli np. dodawanie do wbudowanej klasy String nowych metod. Możliwe też jest ich nadpisywanie:

class String
  def cos_tam
    'nic konkretnego'
  end
end

a = 'abcdef'
p a.length # => 6 - wbudowana metoda w string
p a.cos_tam # => "nic konkretnego" - nasza nowa metoda

Sterowanie przepływem

Warunki

if can_run?
  run
else
  wait
end

message = if counter > 10 then 'Go out' else 'Wait' end

run if can_run?

if fuel_level > 50
  set_fuel_light('green')
elsif fuel_level > 24
  set_fuel_light('yellow')
end
  • false i nil są traktowane jako false
  • reszta, czyli true, 0, '' (pusty string), [] (pusta tablica) - są traktowane jako true
  • konwertowanie do boola można dokonać operatorem !!, np. !![]
# if not == unless

unless fuel_level < 25
  launch
end

launch unless fuel_level < 25


# Ternary operator ?: (operator trójargumentowy)
can_launch ? launch : stop

Przypisywanie warunkowe

# przypisze się tylko wtedy, gdy ship nie ma wartości
ship ||= Spaceship.new
ship = Spaceship.new unless ship

Konstrukcja case

case distance_from_here
when 'far away'
  go_close
when 'close to'
  dock
else
  alarm 'error'
end

# case zwraca także wartości
result = case distance_from_here
         when 'far away'
           100
         when 'close to'
           50
         end
         
# bardziej zwięzła forma ze słówkiem then
result = case distance_from_here
         when 'far away' then 100
         when 'close to' then 50
         end

Pętle

  • pętla while - trzy sposoby zapisu:
while hight_alert?
  sound_system.alarm
end

while hight_alert? do sound_system.alarm end

sound_system.alarm while hight_alert?
  • pętla until, czyli while not (dopóki nie):
until no_fuel?
  accelerate
end

until no_fuel? do accelerate end

accelerate until no_fuel?
  • begin/end - wykona się przynajmniej raz, nawet jeśli warunek jest od początku fałszywy:
begin
  light.start_falshing
  sound.play_siren
end while alarm?
  • for:
for i in [3, 2, 1]
  puts i
end

# wyświetli liczby od 1 do 15 (z zakresu - rage)
for i in (1..15)
  puts i
end

Iteratory i bloki

Blok zaczyna się od do, kończy na end. Jest to specjalny argument przekazywany do funkcji. Konstrukcja |element| to parametr

[1, 2, 3].each do |element|
  puts element
end

# równoznaczne z wcześniejszym
[1, 2, 3].each {|element| puts element}


# nieskończona pętla
loop do
  puts 'Infinity loop'
end

Kontrola wykonywania pętli

  • next przechodzi do kolejnej iteracji:
while message = stream.get_message
  next if message.type == 'sync'
  message.process
end
  • break wychodzi z pętli:
while message = stream.get_message
  message.process
  break if message.type == 'undefined'
end

# instrukcja break może zwrócić wartość z pętli ('result')
result = while message = stream.get_message
           message.process
           break 'result' if message.type == 'undefined'
         end
  • redo powtarza iterację bez ponownego sprawdzania warunku:
i = 0
while i < 3
  print 'Podaj liczbę: '
  input = gets.to_i
  redo if input <= 0
  i += 1
end

Wyjątki

Obsługiwanie wyjątków

begin oraz end to blok obsługi wyjątków. Łapanie ich odbywa się w sekcji rescue:

def launch
  begin
    not_implemented_method # ta metoda nie istnieje
  rescue 
    puts 'Nie mozna wykonac'
    return false
  end
end

# to samo co wyżej, tylko blok (begin ... end) obejmuje całą funkcję
def launch
  not_implemented_method
  turn_on_lights
  true
rescue
  puts 'Nie mozna wykonac'
  false 
end

Możliwe jest łapanie wyjątków określonego typu i przechwytywanie ich właściwości. (RuntimeError dziedziczy po StandardError, a StandardError dziedziczy po Exception)

def launch
  not_implemented_method
  turn_on_lights
rescue StandardError => e
  puts e.message
  puts e.backtrace
  false
end

# łapanie kilku wyjątków
def launch
  not_implemented_method
  turn_on_lights
rescue LightsError
  puts 'Lights error'
  true
rescue StandardError => e
  puts e.backtrace
  false
end

Rzucanie wyjątków

# Wyrzucenie wyjątku z wiadomością
def turn_on_lights
  # ...
  raise 'No energy' # ==> RuntimeError
  # ...
end

# Wyrzucenie własnego wyjątku z wiadomością
def turn_on_lights
  # ...
  raise LightError, 'No energy'
  # ...
end

ensure & else

Kilka możliwych wyjść z metod utrudnia np. zamknięcie pliku. Z pomocą przychodzi blok ensure, który wykona się zawsze, nie ważne czy został złapany wyjątek, czy też nie. Sekcja else wykona się tylko wtedy, gdy funkcja nie rzuci żadnego wyjątku (ensure wykona się zaraz po else).

def read_book
  book = File.open('book.txt')
  # ...
  raise BookError, 'Bad book' if review.bad? # wyjście z metody
  # ...
  true # wyjście z metody
rescue
  # łapiemy błędy z funkcji
  false # wyjście z metody
else
  puts 'Brak wyjątków, wszystko poszło jak trzeba'
ensure
  book.close if book # zamykamy plik
end

Ponawianie prób

def fetch_book
  book = API.request('/book/1')
  # ...
rescue RuntimeError => e
  attempts ||= 0
  attempts += 1
  if attempts < 3 # próbujemy 3 razy połączyć się z API
    puts e.message + '. Ponawiam próbę'
    retry # przenosi wykonywanie na początek metody (bloku begin..end)
  else
    puts 'Niepowodzenie.'
    raise
  end
end
comments powered by Disqus