Перейти к содержанию

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

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

[править]

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

[править]