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 существует класс |
Если к классу надо добавить много методов сразу, то при описании класса можно выйти на уровень его обьекта-класса. Это свойство в 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-приложения мной применялся класс В какой-то момент моя программа перестала работать. Почему? В Rails был, для внутренних нужд, добавлен другой класс Julik 01:52, 25 июня 2006 |