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 если массив пуст, и метод-предикат .any?, возвращающий 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                              # вызывает сообщение об ошибке —
                                       # вне замыкания об этой переменной ничего не известно

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

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

[править] Замыкания принимают аргументы

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

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

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

Аргументы метода указываются после открывающей фигурной скобки или после слова 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

[править] Некоторые применения замыканий

Замыкания — одна из главных особенностей 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

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

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    #=> "Crzbwnfxjmpsvldg"

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

У обьектов в 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 = 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 айгенклассом. Аналогичные концепции в других языках, например в Samlltalk, от которого Ruby наследовал свою объкетную идеологию, называются также метаклассами.

Добавим к классу File метод myself, который даёт быстрый доступ к текущему файлу с исходным кодом:

class Manager
    class << self
        def create
        …
        end
 
        def manage
        …
        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, :Сonnection)
 
class Connection < EncryptedSocket
    # такие-же методы, как у connection, но работающие с шифрованным соединением…
end
 
# В итоге чужая программа будет использовать созданный нами Connection

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

Информация

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

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

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

Julik 01:52, 25 июня 2006

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

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

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

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