Ruby/Подробнее о методах

Подробнее о методах

править

Все функции в Ruby являются методами, то есть свойственны объектам. При программировании на это можно не обращать внимания, поскольку любая программа на Ruby уже является определением класса. У методов могут быть обязательные или необязательные параметры. Методы разграничиваются фигурными скобками или ключевыми словами def и end.

Создание метода

править

Благодаря тому, что указание класса-носителя метода необязательно, на Ruby можно программировать в функциональном стиле, не заботясь о создании класса-«носителя» для каждой группы методов. Метод создаётся с помощью ключевых слов def … end.

def sum(a, b)
    return a + b
end

sum(10, 2)    #=> 12

Ruby по умолчанию возвратит из метода результат последнего выполненного выражения, поэтому в конце метода или в условных конструкциях слово return можно опускать. Поскольку методы могут быть переопределены в процессе выполнения программы, можно «на ходу» переписать метод так:

def sum(a, b)
    a + b
end

sum(10, 2)    #=> 12


Указание значений по умолчанию

править

У методов могут быть необязательные аргументы. Для этого им нужно присвоить значение, которое следует применять «по умолчанию»:

def sum(a, b = 5)
    a + b
end

sum(10, 2)    #=> 12
sum(10)       #=> 15

Методы с восклицательным и вопросительным знаком

править

В Ruby при создании методов можно применять простейшую пунктуацию. Два стандартных приёма применения такой пунктуации — восклицательный и вопросительный знак в конце метода. Методы с вопросительным знаком традиционно работают как предикаты, то есть возвращают true или false. Пример методов-предикатов, — методы массива.

Например, в Java подобные методы начинались бы со слова is: isVolatile(), isEnabled.

Обычно программист, чтобы проверить, пуст ли массив, посмотрит его длину:

arr = []
if arr.length == 0
    puts "empty"
else
    puts "not empty"
end

У массива в Ruby есть метод-предикат .empty?, возвращающий true если массив пуст.

arr = []
if arr.empty?
    puts "empty"
else
    puts "not empty"
end

Если вы реализуете программу, которой будут пользоваться другие, считается хорошим тоном реализовывать методы-предикаты.

Ещё одна их прелесть — сочетание с модификаторами выражения:

arr = [1, 2, 3]
p "Array has something" if arr.any?

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

string = "   Some string with spaces     "
string.strip!    #=> "Some string with spaces" — возвращает результат операции…
string           #=> "Some string with spaces" …и меняет состояние объекта-адресата

Методы присваивания

править

Другие особые варианты пунктуации — знак равенства и арифметические знаки.

Знак равенства в конце названия метода означает, что этот метод присваивает свойству объекта значение:

class Bottle
    def capacity
        @capacity
    end

    def capacity=(new_cap)
        @capacity = new_cap
    end
end

bottle = Bottle.new
bottle.capacity = 10     #=> 10, автоматически преобразуется в вызов метода capacity=

второй метод

class Bottle
attr_accessor :capacity, :contents
end

bottle = Bottle.new
bottle.inspect            #=> "#<Bottle:0x2b650d8>"
bottle.capacity = 0.5     #=> 0.5
bottle.contents = "milk"  #=> "milk"
bottle.inspect            #=> "#<Bottle:0x2b650d8 @capacity=0.5, @contents=\"milk\">"
bottle.capacity           #=> 0.5
bottle.contents           #=> "milk"

.......

Операторы

править

Операторы (умножение, деление, возведение в степень и так далее — вплоть до сравнения!) — тоже методы. Например:

class Broom
    def+(another)
        12 + another
    end
end

whisk = Broom.new
whisk + 10    #=> 22

Это применяется, например, во встроенном в Ruby объекте Time. При прибавлении к нему целого числа он возвращает новый объект Time с добавленным количеством секунд:

t = Time.now     #=> Sun Jun 11 20:29:51
t + 60           #=> Sun Jun 11 20:30:51 — на минуту позже

То же самое характерно для имеющегося в стандартной библиотеке класса Date, но, в отличие от Time, он считает дни вместо секунд.

require 'date'
d = Date.today    #=> Sun Jun 11
d + 1             #=> Mon Jun 12 — на день позже

«Поглощение» аргументов метода

править

Можно «свернуть» аргументы с помощью звёздочки — тогда метод получит массив в качестве аргумента:

def sum(*members)
    members[0] + members[1]
end

sum(10, 2) #=> 12

Поскольку теперь наш метод принимает неограниченное количество элементов, мы можем пользоваться ими как массивом и в теле функции:

def sum(*members)
    initial = 0
    members.collect{ | item | initial += item }
    initial
end

sum(10, 2)            #=> 12
sum(10, 2, 12, 34)    #=> 58

Можно разделить аргументы на обязательные и необязательные, просто пометив последний аргумент «звёздочкой». Если методу будут переданы только обязательные аргументы, в переменной «со звёздочкой» в теле метода будет пустой массив.

Звёздочкой полезно пользоваться и когда нужно передать методу аргументы, но не хочется указывать их по отдельности. Следуя тому же примеру:

array_to_sum = [10, 2, 12, 34]
sum(*array_to_sum)    #=> 58

Подробнее о замыканиях

править

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

Ruby позволяет создавать анонимные методы и передавать их функциям — такие анонимные методы называются замыканиями. Очень большое количество функций Ruby основано на использовании замыканий. Например, итераторы (такие как each и map). Замыкание — это фактически «функция в функции» — программист определяет операцию, которую необходимо выполнить, но непосредственно её выполнение осуществляет метод, которому замыкание передаётся.

Зачем они нужны

править

Замыкания позволяют избавиться от очень большого количества операций, которые для каждого программиста являются привычными, а именно:

  • поддержание индекса в цикле,
  • забота об итераторах как отдельных объектах,
  • закрытие ресурса после его использования,
  • забота о контексте, в котором выполняется операция.

Как создать замыкание

править

Замыкание передаётся методу через конструкцию do … end или фигурные скобки. Общепринятым является использовать фигурные скобки, если вызов замыкания умещается на одну строку программы. Для демонстрации работы замыкания мы будем использовать метод .map. Этот метод принимает замыкание и выполняет его строго заданное число раз.

При передаче замыкания методу, замыкание следует за скобками аргументов.

puts (1..3).map(){ "Вау!" }    # выводит Вау! три раза

Поскольку при отсутствии аргументов скобки необязательны, простейшая запись такова:

puts (1..3).map{ "Вау!" }    # выводит Вау! три раза

Важно помнить, что замыкание использует методы и переменные, указанные при его создании, то есть замыкание захватывает контекст, но переменные, определённые в замыкании, остаются для него локальными!

puts (1..3).map{ word = 'Вау!'; word } # выводит Вау! три раза, поскольку замыкание знает
                                       # переменную word, и она определена в нём

puts word                              # вызывает сообщение об ошибке —
                                       # вне замыкания об этой переменной ничего не известно

Необходимо заметить, что если переменная была определена ранее, то она может использоваться внутри замыкания:

word=""
puts (1..3).map{ word = 'Вау!'; word }
puts word                              # выведет Вау!

или

i=0
(1..3).map do |x|
  i = x
end
puts i              # выведет 3

Как уже упоминалось, если замыкание многострочное, целесообразней пользоваться формой с do … end:

(1..3).map do
    random_number = rand()
    puts "Вау — случайный номер!\n" + random_number.to_s
end

Замыкания принимают аргументы

править

Другое замечательное свойство замыканий — они, как и функции, могут принимать аргументы. В таком случае метод, которому передано замыкание, сам «решает», что это замыкание получит в качестве аргумента. Например, уже продемонстрированный метод .map ещё и передаёт замыканию аргумент, который можно захватить следующим образом:

puts (1..3).map do |i|
    i
end

В данном случае при каждом выполнении замыкания переменная i будет получать значение из диапазона 1..3 в каждом положении итератора, начиная с единицы.

Аргументы метода указываются после открывающей фигурной скобки или после слова do через запятую и ограничиваются двумя вертикальными чертами.

Свои методы с замыканиями

править

Ключевое слово yield в методе открывает раздвижные двери, впускающие аргумент[ы] в замыкание.

def twice
    yield "и раз"
    yield "и два"
end

twice { |words| puts "!!! " + words }    #=> !!! и раз
                                         #=> !!! и два

При этом строка будет передаваться замыканию в переменную words при каждом выполнении.

Если замыкание обязательно, следует пометить его как последний аргумент метода и в начале аргумента добавить амперсанд:

def twice(&closure)
    yield "и раз"
    yield "и два"
end

twice    #=> Ошибка LocalJumpError - отсутствует замыкание

Последнее утверждение не совсем верно. Даже совсем не верно. Указания переменной замыкания недостаточно для контроля наличия входного замыкания. Дело в том, что в случае, если замыкание не вызывается, то и ошибки не будет:

def func(a, &closure)
    return a if a
    yield "и раз"
    yield "и два"
end

func true     #=> true
func false    #=> LocalJumpError: no block given

Более того, вызов функции twice без указания замыкания также приведёт к ошибке. Таким образом, гораздо лучше вместо введения обязательного параметра задавать замыкание по-умолчанию:

def func(a, &closure)
    return a if a
    closure ||= lambda{ |words| puts "!!! " + words }
    closure.call("и раз")
    closure.call("и два")
end

func true     #=> true
func false    #=> !!! и раз
              #=> !!! и два

func(false){ |words| puts "??? " + words }    #=> ??? и раз
                                              #=> ??? и два

Здесь lambda — пустая функция, а closure.call — явный способ вызова замыкания на выполнение.

Замыкание можно также передать другому методу, просто указав его как последний аргумент с амперсандом:

def writing_to(file, &closure)
    File.open(file, 'w', &closure)
end

Наконец, на десерт, напишем свой inject.

class Array
  def inject2 ( buf )
    self.map do |e|
      buf = yield(buf,e)
    end
    buf
  end
end
 
p [1,2,3].inject2(10){|b,e| b + e} #=> 16
p [1,2,3].inject(10){|b,e| b + e}  #=> 16

Некоторые применения замыканий

править

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

Типичное применение замыкания — когда после выполнений некой операции нужно «вынести мусор»: закрыть открытый ресурс или отсоединиться от сети. Предположим, что мы пишем метод для интернет-системы. При этом мы хотим выполнить несколько операций. Но чтобы их выполнить, нужно подключить пользователя к Сети. После того, как операции завершились, надо его так же незаметно отключить.

connected{ download_email }

В данном случае мы пишем только замыкание с download_email, все заботы по открытию (а главное — закрытию) соединения возьмёт на себя метод connected:

def connected
    connect_to_internet
    result = yield
    disconnect
    result
end

В данном случае мы сохраняем то, что вернуло замыкание, в метод, закрываем соединение и возвращаем результат замыкания как свой собственный.

Чаще всего о методах, принимающих замыкания, можно говорить как о деепричастном обороте — например, «соединившись», «внутри_транзакции», «с файлом», «трижды».

Если воспользоваться встроенной проверкой исключений, то метод принимает такой вид:

def connected
    connect_to_internet
    begin
        result = yield
    ensure
        disconnect
    end
    result
end

Тогда, даже если метод вызовет ошибку, соединение всё равно будет закрыто.

Методы, которых не было

править

Экспериментально замечено, что во время сессии у студентов значительно повышается способность к изобретениям различного рода. Иногда удаётся направить эту энергию в нужное русло: некоторые студенты во время сдачи зачёта начинают придумывать свои методы. Естественно, что «придуманные методы» они реализовать не могут, но с этим замечательно справляются их преподаватели. Некоторым методам даже дают имена студентов, которые приложили своё незнание к их созданию. Многие из таких методов включают в последующие версии языка.

Ширяевский .size

править

Студент МЭТТ Ширяев Денис на одном из зачётов предложил использовать метод .size в качестве итератора. Он использовал его для подсчёта количества элементов массива, удовлетворяющих условию. По сути, он предложил укоротить связку .find_all{ … }.size. Вот как будет выглядеть программа подсчёта количества чётных элементов массива:

array = [1, 2, 3, 4, 5, 6]
array.size{ |i| (i % 2).zero? }    #=> 3

Чтобы заставить работать данную программу, необходимо перед использованием метода .size переопределить его, написав следующий код, который будет реализовывать эту функциональность:

class Array
    def size(&closure)
        closure ? inject(0){ |count, elem| (yield elem) ? count + 1 : count } : length
    end
end

Метод реализован только для массивов, но возможно его добавление к хешам или строкам.

Случайное число из диапазона

править

Студенты часто возмущаются: почему, чтобы получить случайное число от 3 до 6 нужно писать нечто невнятное вида:

3 + rand(4)

Откуда чего берётся? Почему нельзя написать проще? Например вот так:

(3..6).rand

Действительно, почему? Давайте добавим такую функциональность к классу Range:

class Range
    def rand
        first + Kernel.rand(last - first + (exclude_end? ? 0 : 1))
    end
end

Для проверки можно выполнить следующий код:

p Array.new(100){ (3..6).rand }.uniq.sort   #=> [3, 4, 5, 6]

Что и требовалось реализовать. Кстати, данная реализация имеет один изъян: для строковых диапазонов метод Range#rand будет выдавать ошибку. Решается проблема достаточно просто. Надо реализовать Array#rand (получение случайного элемента массива), а внутри Range#rand вызывать связку .to_a.rand. Теперь тоже самое, но на Ruby:

class Array
    def rand
        self[Kernel.rand(size)]
    end
end

class Range
    def rand
        to_a.rand
    end
end

Или еще проще (без изменения класса Array):

class Range
    def rand
        to_a.sample
    end
end

Для проверки выполним следующий код:

p Array.new(100){ ("a".."c").rand }.uniq.sort  #=> ["a", "b", "c"]

Странно, но, видимо, всё работает!

Способы расширения библиотеки методов

править

Как добавить метод к массиву/строке/венику?

править

Важно помнить, что в Ruby все типы являются объектами, даже сами классы. Каждый класс до конца выполнения программы остаётся открытым, а это значит, что в любой тип можно добавить собственные методы (или изменить поведение существующих). Каждый класс можно определять постепенно, в нескольких частях программы:

class Broom
    def sweep
    end
end

Broom.instance_methods    #=> […, "sweep", …]

class Broom
    def wash_lavatory_pan(lavatory_pan)
    end
end

Broom.instance_methods    #=> […, "sweep", …, "wash_lavatory_pan", …]

Метод .instance_methods возвращает массив, который содержит имена методов, которые можно вызвать.

Добавленные методы становятся доступны немедленно, в том числе для уже созданнных экземпляров типа. Стоит помнить, что методы в Ruby — на самом деле «сообщения», и у каждого метода есть «приёмник», то есть объект, которому сообщение отправлено. Метод по умолчанию ищет другие методы в экземпляре класса, поскольку приёмником для него является self.

Простейший пример — добавление метода классу String, выводящий только согласные буквы из строки:

class String
    def consonants
        cons = []
        self.scan(/[BCDFGHJKLMNPRSTVWXZbcdfghjklmnprstvwxz]/){ |m| cons << m }
        cons.uniq.join
    end
end

"Crazy brown fox jumps over a lazy dog".consonants    #=> "Crzbwnfxjmpsvrldg"

Операция расширения класса (добавление нового метода к существующему) по сути не отличается от создания нового класса.

У объектов в Ruby есть методы класса и методы экземпляра. В нашем примере consonants — это именно метод экземпляра. При создании нового класса или изменении существующего создать метод класса можно, начав его имя с имени класса или с self и точки:

class String
    def self.consonants_from(string)
        cons = []
        string.scan(/[BCDFGHJKLMNPRSTVWXZbcdfghjklmnprstvwxz]/){ |m| cons << m }
        cons.uniq.join
    end
end

String.consonants_from("Crazy fox jumps over a lazy dog")    #=> "Crzbwnfxjmpsvldg"

Одним из специфических свойств Ruby является то, что классы сами по себе — экземпляры класса Class, и с ними можно работать как с обычными объектами. Специальный синтаксис для доступа к методам класса в Ruby не нужен. Классы можно хранить в переменных, передавать методам и так далее.

В контексте класса self — это сам класс.

Проиллюстрируем это простым примером. Как мы знаем, у класса File есть метод open. Создадим метод у класса File, дающий нам доступ к временному файлу, создаваемому в момент выполнения кода. Это такой же метод, но открывающий только файлы из директории /tmp:

class File
    def self.temporary(&closure)
        # определим директорию, в которой в данный момент запущена программа
        # методы dirname и expand_path в данном случае — File.dirname и File.expand_path
        dirname = self.dirname(self.expand_path(__FILE__))
        base    = basename(__FILE__, '.rb')         #=> имя файла с программой без расширения .rb
        stamp   = "#{base}_#{Time.now.to_i}.tmp"    #=> системное время в секундах и расширение .tmp

        # File.join соединит фрагменты пути обратным слешем в Windows и прямым слешем на UNIX
        path = self.join(dirname, stamp)
        self.open(path, 'w', &closure)
    end
end

File.temporary { |f| f << "Some info" } #=> #<File:/Tests/(irb)_1151198720.tmp (closed)>
 

Для управления временными файлами в Ruby существует класс Tempfile. Помимо других достоинств он гарантирует, что созданные временные файлы по завершении программы будут удалены.

Если к классу надо добавить много методов сразу, то при описании класса можно выйти на уровень его объекта-класса. Это свойство в Ruby называется eigenclass (нем. eigen — свой, особый). Подозревая, что многие из читателей незнакомы с математическим понятием собственного значения/вектора/пространства, мы кратко и по-программистски назовём eigenclass айгенклассом. Аналогичные концепции в других языках, например в Smalltalk, от которого Ruby наследовал свою объектную идеологию, называются также метаклассами.

Добавим к классу File метод myself:

class File
    class << self
        def myself
        
        end
    end
end

Если нужно добавить метод только к конкретному экземпляру, нужно выйти на его айгенкласс:

string = "Crazy brown fox jumps over a lazy dog"
other_string = "Three black witches"

def string.vowels
    vowels = []
    scan(/[AEIOUYaeiuoy]/){ |m| vowels << m}
    vowels.uniq.join
end

string.vowels          #=> "ayoue"
other_string.vowels    #=> NoMethodError: undefined method `vowels' for …

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

Программист-разрушитель

править

Как ни странно, изредка программисту приходится взять на себя позицию разрушителя — удалить существующий метод или константу. Метод undef позволяет сделать это:

class Broom
    def sweep
        "Метём!"
    end
end

class Birch_broom < Broom

    def whip(back)
    end

    def wet_in_basin(basin)
    end

    undef sweep

end

broom       = Broom.new
birch_broom = Birch_broom.new

broom.sweep          #=> "Метём!"
birch_broom.sweep    #=> Ошибка NoMethodError — такого метода нет, хоть он и был унаследован

Уничтожение класса несколько сложнее, но тоже возможно:

Object.send(:remove_const, :Broom)

После этого Broom будет существовать только для объекта-экземпляра:

class Broom
end

whisk = Broom.new

Object.send(:remove_const, :Broom)

Broom          #=> Ошибка NameError: неизвестная константа Broom
whisk.class    #=> Broom, всё ещё существует для экземпляра

Это свойство Ruby крайне полезно, если нужно создать класс, наследующий от другого, но при этом имеющий другого родителя. Например:

# В чужой программе:
class Connection < Socket
    # много-много методов…
end

conn = Connection.new()


# В нашей программе:
Object.send(:remove_const, :Connection)

class Connection < EncryptedSocket
    # такие-же методы, как у Connection, но работающие с шифрованным соединением…
end

# В итоге чужая программа будет использовать созданный нами Connection

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

 

История из жизни

При разработке своего Rails-приложения мной применялся класс OrderedHash, который работает как стандартный хеш, но при этом имеет упорядоченные ключи. Это позволяет, к примеру, удобно сгруппировать новости по датам, сохраняя порядок дат.

В какой-то момент моя программа перестала работать. Почему? В Rails был, для внутренних нужд, добавлен другой класс OrderedHash, но при этом он не соответствовал моему (и даже не соответствовал обычному Hash — некоторых методов в нём просто не было! Благодаря remove_const мне удалось просто выгрузить их класс и заменить его своим. А тесты в комплекте чужой библиотеки позволили удостовериться, что я ничего не испортил и она с моим «внедрённым» классом функционирует нормально.

Julik 01:52, 25 июня 2006

Как написать свой итератор?

править

Как написать свой класс?

править

Писать класс не так уж и сложно. Простейший класс будет выглядеть так:

class NewClass
  def initialize(a,b,c)
    @a = a
    @b = b
    @c = c
  end
  
  def output
    puts "a = #{@a}"
    puts "b = #{@b}"
    puts "c = #{@c}"
  end
end
   
newclass = NewClass.new(10,20,30)
  
newclass.output
#=> a = 10
#=> b = 20
#=> c = 30

Наследовать или перемешать?

править

Как сделать свою библиотеку методов?

править