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
Внимание! Данный листинг не будет работать на Ruby версии 1.9.2 |
В данном случае при каждом выполнении замыкания переменная 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 существует класс |
Если к классу надо добавить много методов сразу, то при описании класса можно выйти на уровень его объекта-класса. Это свойство в 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-приложения мной применялся класс В какой-то момент моя программа перестала работать. Почему? В Rails был, для внутренних нужд, добавлен другой класс 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