RubyDev Ruby Tutorial #5 > Методы и процедуры в Ruby

апреля 13, 2011  |  Published in Ruby, Основы  |  7 Comments

Что есть метод и что есть процедура?
Метод — это определенное действие объекта.
Процедура — это просто действие не привязанное к объектам.

Методы свойственны только объектно-ориентированному программированию, а поцедуры функциональному.

Ruby является объектно-ориентированным языком программирования однако имеет в своем арсенале процедуры и лямбда-функции, которые являются объектами класса Proc.

Зачем необходимы процедуры
Без процедур в Ruby можно отлично обходиться, однако Ruby — язык широких возможностей и предоставляет вам множество способов реализовать один и тот же функционал.

Метод является частью объекта, и метод представляет действие объекта, однако иногда необхоимо просто выполнить определенное действие. В таком случае лучше и удобнее создать процедуру, чем определять класс и метод в нем. Еще одним важным преимуществом и полезной возможностью процедур является то, что они являются объектами, а значит могут присваиваться переменным.

Методы

Объявление метода происходит при помощи выражения def после которого следует имя метода и перечень аргументов необходимых для него, пример:

def sum(a, b)
  puts "#{a} + #{b} = #{a+b}"
end

sum(1, 9)
#1 + 9 = 10
#=>nil

Для методов не нужно явно указывать возвращаемое значение, они вернут результат выполнения посленей строки своего кода, однако иногда полезно явно указывать возвращаемое значение при помощи выражения return. Это «иногда» наступает тогда, когда код слишком сложен и сложно сразу определить, что будет возвращаться (например, когда метод содержит много условий). Еще return следует использовать тогда, когда вас не устаивает то, что метод возвращает результат выполнения последней строки. Пример использования return:

def sum(a, b)
  puts "#{a} + #{b} = #{a+b}"
  return a + b
end

sum(10, 5)
#10 + 5 = 15
#=> 15

Аргументы методов
Аргументы методов — это параметры, которые передаются в метод и с которыми метод выполняет определенные действия или из-за которых метод изменяет свое поведение. Аргументы бывают предопределенными (необязательными) и не предопределенными. Предопределенные аргументы используются тогда, когда, например значение аргумента в большинстве случаев одинаково, однако, пользователю метода может понадобится его изменить, непредопределенные аргументы были приведены в примерах выше. Когда вы используете непредопределенные аргументы метода, например в количестве 2, но при вызове метода передаете только один аргумент, то метод вернет ошибку: ArgumentError: wrong number of arguments (1 for 2), если же один из двух аргументов будет предопределенным, то значение автоматически присвоится тому аргументу, который является не предопределенным. Пример предопределенных аргументов:

def sum(a,b=5,c=10)
  puts "a = #{a}\nb = #{b}\nc = #{c}"
  puts a + b + c
end

sum(10)
#a = 10
#b = 5
#c = 10
#25

sum(10,15)
#a = 10
#b = 15
#c = 10
#35

sum(10,15,20)
#a = 10
#b = 15
#c = 20
#45

Обратите внимание на последовательность аргументов метода! Я сначала указываю не предопределенные аргументы, а затем предопределенные.Это делается не просто так, просто так легче понять, какой значение какому аргументу будет передано. Если вы будете использовать другой порядок следования аргументов, то присвоение аргументам значения будет происходить несколько неожиданно:

def sum(a=5,b=6,c)
  puts a,b,c
end

sum 2
#5
#6
#2

sum 2,3
#2
#6
#3

Методы в Ruby могут принимать не только предопределенное количество аргументов, но и произвольное их количество. Если быть совсем точным, то один из аргументов метода мы объявляем коллекцией значений при помощи оператора splat «*». В методе может быть всего один такой аргумент. Пример:

def do_something_with_collection(m,*strings)
  result = []
  strings.each{|str| result << str.send(m)}
  return result
end

collection = %w{hello rubydev readers}
#=> ["hello", "rubydev", "readers"]

result_collection = do_something_with_collection(:upcase,*collection)
#=> ["HELLO", "RUBYDEV", "READERS"]

result_collection = do_something_with_collection(:downcase,*result_collection)
#=> ["hello", "rubydev", "readers"]

При передаче аргументов в метод в виде массива мы также должны использовать оператор *, который превращает массив в набор значений, иначе аргументу strings будет определено одно значение — массив, что нас не устраивает. При создании методов с произвольным количеством аргументов следует обратить внемание на такое правило: аргумент реализующий прием множественного количества аргументов должен быть представлен последним в списке аргументов метода. Примеры:

def multisum(a,b=5,*c)
  sum = c.inject{|sum, num| sum += num}
  sum*a*b
end

multisum(5, *[1, 2, 3, 4, 5]) #=> 70

В метод на самом деле передается набор аргументов: 5,1,2,3,4,5, поэтому аргумент a получает значение 5, аргумент b получает значение 1, а аргумент с получает все остальные значения. При таком положении дел предопределенные аргументы становятся бесполезными, так как если метод получает произвольное количество параметров, то они могут быть переписаны. Выход из ситуации простой: не использовать предопределенные аргументы и аргументы получающие коллекцию значений в одном методе. Произвольное количество значений вы можете изначально передавать в виде коллекции. Пример:

def multisum(arg)
  sum = arg[:collection].inject{|sum, num| sum += num}
  sum * arg[:a] * arg[:b]
end

multisum({a:5, b:1, collection:[2,3,4,5]}) #=> 70

Приемники методов
Метод пренадлежа объекту имеет приемник — объект, которому он принадлежит. Если метод вызывается без указания приемника, то это означает, что приемником является текущий объект,то есть тот, в контексте которого вызывается метод. Примеры:

class A
  def self.hello
    puts 'hello'
  end
  def bye
    puts 'bye'
  end
end

A.hello #hello

A.new.bye #bye

Приемник self указывает на текущий объект, то есть метод .hello является методом класса, а метод #bye — методом объекта.

Трюки с методами

В Ruby существует возможность вызывать методы не на прямую, а через метод #send передавая ему в качестве аргумента символ соответствующий имени метода, пример:

Вызов:

"hello".upcase #=> "HELLO"

…соответствует:

"hello".send(:upcase) #=> "HELLO"

Одной из сильных сторон Ruby является метапрограммирование, которое позволяет создавать методы непосредственно во время выполнения программы. Существует несколько вариантов создания методов «на лету» при помощи .define_method в контексте класса, при помощи #define_singleton_method в контексте объекта и обработкой ошибок вызова несуществующего метода при помощи #method_missing. Примеры:

class A
  define_method :hello do
    puts 'hello'
  end
end

A.new.hello # hello


a = A.new
a.define_singleton_method(:bye) do |name|
  puts "Hello, #{name}"
end #=> #<Proc:0x86df814@(irb):18 (lambda)>

a.bye('Vladimir') #Hello, Vladimir

class A
  def method_missing(name, arg, &block)
    if name =~ /count_to/
      (arg + 1).times {|n| puts n}
    end
  end
end

a = A.new #=> #<A:0x86a0ba0>

a.count_to(5)
#0
#1
#2
#3
#4
#5

.define_method и #define_singleton_method принимают имя метода в виде строки или символа и блок кода, который превращается в процедуру, которая вызывается в контексте объявляемого метода. .define_method и #define_singleton_method следует использовать только в случае реальной необходимости, когда, например, имя метода задается переменной. Во всех остальных случаях следует использовать стандартный синтаксис объявления методов при помощи def.

#method_missing — используется для обработки ошибки NoMethodError, которая возвращается при попытке вызват несуществующий метод. То есть #method_missing не объявляет методы, а обрабатывает ошибки. В контексте #method_missing вы можете на основании имени вызываемого метода обработать ошибку, то есть симитировать существование метода или нескольких методов.

singleton методы — это индивидуальные методы объекта, которые хранятся в singleton классе объекта по той простой причине, что объекты не хранят методы. Подробней singleton классы рассматривались в предыдущей главе учебника.


Процедуры, лямбды и блоки кода


Процедуры и лямбды, в отличие от методов являются полноценными значениями, так как, являются объектами класса Proc. Благодаря этому их можно передавать в качестве аргументов, либо заворачивать в них вызов метода в такой способ передавая метод. Кроме того, процедуры и лямбды не принадлежат объектам, они представляют действие само по себе. Примеры процедуры и лямбды:

p = Proc.new{|name| puts "Hello #{name}"} #=> #<Proc:0x95c5cd4@(irb):5>
p.call("Richard") #Hello Richard

a = lambda{|name| puts "Hello #{name}"} #=> #<Proc:0x95e0fd4@(irb):10 (lambda)>
a.call("Ralph") #Hello Ralph

Процедура имеет одно важное отличие от лямбда-функции, которое заключается в том, как она принимает аргументы. При передаче процедуре большего или меньшего количества аргументов, ошибки не возникает, а аргументы с не указанным для них значением получают значение nil. Если же вы передадите большее или меньшее количество значений аргументам лямбды, то будет возращена ошибка ArgumentError. Примеры:

l = lambda do |a,b,c|
  print a,b,c,"\n"
end

p = Proc.new do |a,b,c|
  print a,b,c,"\n"
end

l.call(1,2,3)#123
l.call(1,2) #wrong number of arguments (2 for 3) (ArgumentError)
p.call(1,2,3)#123
p.call(1,2) #12
p.call(1,2,3,4,5) #123
l.call(1,2,3,4,5) #wrong number of arguments (5 for 3) (ArgumentError)

Блок кода — не является объектом, как то многие думают. Блок кода — это просто фрагмент кода, который заключен между { и }, или do и end. Блоки кода используются в качестве аргументов методов, где потом вызываются при помощи выражения yield, используются при объявлении процедур и лямбд и т.д. Использование методов для создания процедур и лямбд не представляет собой чего-то особенного, все вышепредставленные примеры демонстрируют это, поэтому мы рассмотрим тему использования блоков кода в качестве аргументов метода.

def method_with_block(hello)
  yield(hello)
end

method_with_block("Hello!"){|msg| puts msg} #Hello

Выражение yield используется для вызова блока в том месте кода, где оно вставлено и передает в блок аргументы, которые переданы в yield. Стоит заметить, что при использовании выражеия yield вызывается блок, который передается в метод, не как аргумент, а особым образом, как показано в предыдущем примере. Метод раздельно принимает аргументы и блоки, однако у вас есть возможность объявить аргумент, который должен принимать блок кода при помощи оператора &, такой аргумент должен следовать последним в списке аргументов метода. Пример:

def method_with_block(hello, &block)
  block.call(hello)
end

method_with_block("Hello!") {|msg| puts msg} #Hello!

Если вы еще не понимаете пользы существования блоков кода, то вспомните мощные методы — итераторы, с которыми мы знакомились изучая предопределенные типы данных в Ruby. Каждый итератор принимает блок кода, проходит по коллекции вызывая блок кода и передавая в него элемент коллекции. Блоки кода плезны не только вкупе с итераторами, они служат в первую очередь для создания областей видимости и инкапсуляции простых фрагментов кода, при этом, в отличие от методов, могут быть переданы в качестве аргументов.

Символы и волшебный метод #to_proc
Мы уже рассматривали метод #to_proc, когда изучали предопределенные типы данных, в частности символы. Метод #to_proc якобы преобразует символ в процедуру, однако на самом деле он создает процедуру в контексте которой вызывается метод с соответствующим символу именем, пример:

cap = :capitalize.to_proc

Теперь переменная cap будет содержать процедурный объект приблизительно следущего вида:

Proc.new{|arg| arg.send(:capitalize)}

Вот пример работы этого кода:

cap.call("rubydev") #=> "Rubydev"

Еще более волшебный оператор амперсанд &

У многих начинающих Ruby программистов оператор амперсанд вызывает некоторое затруднение в изучении, хотя если разобраться, то выполняет он совсем простую работу — преобразование в процедуру. Чтобы понять, как он работает давайте рассмотрим простой пример:

["hello", "good bye"].collect{|arg| arg.upcase} #=> ["HELLO", "GOOD BYE"]

["hello", "good bye"].collect(&:upcase) #=> ["HELLO", "GOOD BYE"]

Как видите, оба примера выполняют одну и ту же работу. Код &:upcase во втором примере можно заменить на :upcase.to_proc. Теперь вы знаете как работает этот магический оператор — унарный амперсанд и как с его помощью можно упростить код. Лично мое мнение по поводу использования унарного амперсанда следущее: он безусловно делает код более читабельным, но польза от него не значительна. Кроме того, я бы не рекомендовал его использовать так как его использование понижает производительность программы. Выполнение &:upcase занимает почти в 2 раза больше времени, чем выполнение метода #upcase, некоторые назовут это экономией на спичках, но мне кажется, что использование синтаксического сахара должно быть более-менее аргументированным, а не просто делом вкуса программиста.

Спонсор проекта RubyDev — компания : Rails — ориентированный хостинг и VDS с удобной панелью управления и отличной ценовой политикой.

Лучшая благодарность автору — ваши комментарии, доброжелательная критика и помощь в исправлении и улучшении материала изложенного в статьях.

Tags: ,

Responses

  1. Arion says:

    апреля 13, 2011 at 19:08 (#)

    Может стоит сказать, что в ruby 1.9 лямбды и вызови блоков кода осуществляются по новому?

    before 1.9:

    l = lambda do |a,b,c|
    print a,b,c,»\n»
    end
    l.call(1,2,3)

    after 1.9:

    l = ->(a,b,c) { print a,b,c,»\n» }
    l.(1,2,3)

  2. Ion says:

    апреля 14, 2011 at 00:25 (#)

    [quote]Выполнение &:upcase занимает почти в 2 раза больше времени, чем выполнение метода #upcase[/quote]
    В 1.9 это не так:
    % ruby —version ruby 1.9.2p136 (2010-12-25 revision 30365) [x86_64-linux]

    #!/usr/bin/env ruby
    require "benchmark"
    
    time = Benchmark.measure do
      5000000.times do |i|
        ['yahoo', 'google', 'yandex'].collect(&:upcase)
      end
    end
    puts time
    
    time = Benchmark.measure do
      5000000.times do |i|
        ['yahoo', 'google', 'yandex'].collect{|s| s.upcase}
      end
    end
    puts time
    

    [~/projects/tmp] % ./test1.rb [1:06]
    1.680000 0.000000 1.680000 ( 1.628572)
    1.720000 0.000000 1.720000 ( 1.660906)
    [~/projects/tmp] % ./test1.rb [1:06]
    1.690000 0.000000 1.690000 ( 1.643957)
    1.720000 0.010000 1.730000 ( 1.665091)
    [~/projects/tmp] % ./test1.rb [1:06]
    8.280000 0.040000 8.320000 ( 7.972513)
    8.390000 0.030000 8.420000 ( 8.029995)
    [~/projects/tmp] % ./test1.rb [1:09]
    8.370000 0.010000 8.380000 ( 8.151639)
    8.480000 0.020000 8.500000 ( 8.178738)

  3. Alexey says:

    апреля 14, 2011 at 15:20 (#)

    «У многих начинающих Ruby программистов оператор амперсанд вызывает некоторое затруднение в изучении, хотя если разобраться, то выполняет он совсем простую работу – преобразование в процедуру.»

    Нет, он делает не только это.

    «Как видите, оба примера выполняют одну и ту же работу. Код &:upcase во втором примере можно заменить на :upcase.to_proc.»

    Нет, нельзя.

    ["hello", "good bye"].collect(:upcase.to_proc) #=> ArgumentError: wrong number of arguments (1 for 0)
  4. admin says:

    апреля 14, 2011 at 18:25 (#)

    Ion, Alexey спасибо за правки,чуть позже поправлю. Погуглил, нашел информацию, более низкая производительность характерна только для 1.8.x версий.

    Arion, спасибо за пример нового синтаксиса, совсем о нем забыл!

  5. dre3k says:

    апреля 14, 2011 at 23:05 (#)

    Аффтар ты не прав.

    > Еще более волшебный оператор амперсанд &
    > …хотя если разобраться, то выполняет он совсем простую работу – преобразование в процедуру…

    Вот ты сам не разобрался(наверно думал что разобрался) и учиш новичков ерунде. Рекомендую прочитать Programming Ruby 1.9 (3rd edition): The Pragmatic Programmers’ Guide от корки до корки. Там всего 940 страниц ;)

    When you say names.map(&xxx), you’re telling Ruby to pass the Proc object in xxx to the
    map method as a block. If xxx isn’t already a Proc object, Ruby tries to coerce it into one
    by sending it a to_proc message.

    Когда мы пишем names.map(&xxx), вы говорим интерпретатору Руби передать методу map объект Proc на который ссылается переменная xxx, В КАЧЕСТВЕ БЛОКА. Если объект в xxx еще сам по себе не является Proc-объектом, то интерпретатор Руби пытается преобрасовать его в Proc-объект посылая ему сообщение to_proc.

    Т.е. грубо говоря если xxx не Proc то мы пытаемся сделать из него Proc вызывая xxx.to_proc, а потом передать его итератору как БЛОК. И вся эта магия работает потомучто метод #to_proc класса Symbol возвращает Proc объект. Этот метод написан на C, но есле бы он писался на Ruby то выглядел бы он так

    def to_proc
      proc { |obj, *args| obj.send(self, *args) }
    end
    

    По приведённой мною ссылке, можно найти популярное объяснение этого «волшебства» Дэйвом Томасом.

    2 Alexey

    Вот тебе пища для размышления:

    >> ["hello", "good bye"].collect(&lambda { |e| e.upcase } )
    [ "HELLO", "GOOD BYE" ]
    
  6. admin says:

    апреля 14, 2011 at 23:22 (#)

    dre3k, знаю, знаю, немножко на сонную голову писал статью, на RubyDev.ru уже была статья рассказывающая о том, что делает оператор амперсанд. Чуть позже поправлю. Это пока черновые версии статей, которые будут еще очень сильно правиться.

  7. says:

    ноября 12, 2012 at 11:09 (#)

    Вам большой респект за все!!!! Уже какой урок читаю и смотрю что многие ищут какието баги. Сами пусть пишут а потом критикуют. БОЛЬШОЕ СПАСИБО ЗА УРОКИ!!!!!!!!!!1

Leave a Response

Для подсветки кода используйте BB - коды: [language]...[/language], где language может быть: ruby, javascript, css, html.