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 |
[править] Как написать свой итератор?
[править] Как написать свой класс?
[править] Наследовать или перемешать?
[править] Как сделать свою библиотеку методов?
[править] Методика самопознания
Ruby очень динамичный язык программирования, а это значит, что с ходом выполнения программы состояние/структура объектов/классов может изменяться до неузнаваемости. Казалось бы, программисты на Ruby -- самые настоящие мазохисты. Возможно это и так, но не из-за того, что они программируют на Ruby, так как в него встроены достаточно развитые средства самопознания (унаследованные от языка Smalltalk).
[править] Что такое самопознание?
Если язык Ruby настолько динамичен, то как же определить текущее состояние объекта или класса? Ответ очевиден -- нам нужны методы, которые позволят узнать всю подноготную о текущем состоянии объекта или класса. Именно эти методы мы и будем называть методами самопознания.
Сразу после того, как мы узнали информацию о текущем состоянии, то у нас может возникнуть непреодолимое желание эту информацию использовать в своих целях. Именно поэтому мы рассмотрим не только методы самопознания, но и методы самомодификации. Выделять их в отдельный раздел особого смысла не имеет (так как их очень мало) и поэтому они будут рассмотрены попутно, как бы между делом.
|
Методами самомодификации мы будем называть такие, которые позволяют изменять объект или класс на основании информации о его текущем состоянии |
Каждый из методов самопознания отвечает на тот или иной вопрос о текущем состоянии. Именно поэтому все дальнейшие заголовки будут выглядеть как вопросы.
|
Совокупность методов самомодификации и самопознания называют методами метапрограммирования |
[править] Какому классу принадлежит объект?
Язык программирования Ruby является строго типизированным (как и большинство других языков), то есть никогда не существует неопределенности по поводу класса того или иного объекта. Долгое время Ruby считали не строго типизированным языком, но это заблуждение, которое возникло из динамической структуры языка: класс переменных определяется объектом на который ссылается эта переменная и вместе со сменой объекта может поменяться и класс переменной.
|
Авторы дают неверную интерпретацию понятия "строго типизированный" |
Исходя из вышеизложенного, существует необходимость узнать какой класс хранится в той или иной переменной (или возвращается каким либо методом). Для этой цели служит метод .class ("неожиданно", правда?), который возвращает класс, которому принадлежит данный объект.
"типичная строка".class #-> String 0xface.class #-> Fixnum 9876543210.class #-> Bignum 1234.class.class #-> Class Fixnum.class #-> Class
Обратите внимание, что метод .class возвращает объект класса Class (еще одна неожиданность). А почему же не строка? Дело в том, что от класса можно сразу же вызывать методы этого класса. Например, Вам нужно создать объект точно такого же класса, как и у переменной my_variable. Тогда Ваш код может выглядеть следующим образом:
my_variable = [1, 2, 3, 4] my_variable.class #-> Array my_variable.class.new #-> [] Array.new #-> []
Думаю, что никому не надо объяснять, какую мощь дает подобный механизм. Например, в своей деятельности мне не раз приходилось создавать массив классов, от каждого из которых вызывались одни и те же методы.
|
У метода .class есть синоним .type, но последние версии интерпретатора выдают предупреждение, что метод .type устарел и в дальнешем исчезнет из языка |
[править] От какого класса унаследован этот класс?
Другим инструментом исследования классовой иерархии является метод .superclass, который предназначен для получения класса родителя. Заметим, что метод .superclass можно вызывать только для объекта класса Class, который, в свою очередь, можно получить посредством метода .class. Какое-то сложное определение получилось, но давайте рассмотрим работу метода .superclass на примере:
Integer.superclass #-> Numeric 123.class.superclass #-> Integer 123.class.superclass.superclass #-> Numeric Numeric.superclass #-> Object Object.superclass #-> nil
Из примера видно, что посредством последовательного вызова метода .superclass можно узнать обо всей иерархии наследования какого либо класса.
[править] Какие классы/модули использовались этим классом?
На первый взгляд может показать странным такой вопрос, так как мы уже знаем метод superclass, что дает нам возможность получить достоверную информацию о иерархии родительских классов. Дело в том, что в языке Ruby отсутствует множественное наследование, которое компенсируется механизмом примесей. Вот для того, чтобы получить массив всех классов/модулей, которые относятся к данному классу и существует метод .ancestors. Посмотрим его в деле:
Fixnum.ancestors #-> [Fixnum, Integer, Precision, Numeric, Comparable, Object, Kernel]
|
В соответствии с негласным правилом, методы, имена которых заканчиваются на s возвращают массив. Это правило получило свое дальнейшее развитие в Ruby on Rails и его следует придерживаться всем разработчикам на Ruby |
Обратите внимание, что текущий класс располагается нулевым элементом в массиве, а все остальные классы расположены в порядке наследования/примешивания.
[править] Как получить список примесей класса?
Поработав с методом .ancestors может возникнуть резонный вопрос, а что если нужно получить только список примесей без родительских классов? В принципе, для этого надо знать только то, что все примеси имеют класс Module. Узнать это можно, если выполнить простенькую программу:
[Fixnum, Integer, Precision, Numeric, Comparable, Object, Kernel]. map{ |c| c.class } #-> [Class, Class, Module, Class, Module, Class, Module]
Поэтому, для того, чтобы получить список примесей класса, можно воспользоваться следующей программой:
Fixnum.ancestors.select{ |c| c.class == Module } #-> [Precision, Comparable, Kernel]
Но зачем писать больше, если для этого есть специальный метод .included_modules, который возвращает список подключенных примесей.
Fixnum.included_modules #-> [Precision, Comparable, Kernel]
Ну, а если результат одинаковый, то для решения этой задачи лучше вызвать один метод .included_modules вместо двух (ancestors и .select).
[править] Как получить список всех родительских классов?
Хочу сразу Вас огорчить, что для решения этой задачи специального метода не существует, а это значит, что задачу будем решать через уже известные.
my_variable = 123 class_array = my_variable.class.ancestors - my_variable.class.included_modules #-> [Fixnum, Integer, Numeric, Object] class_array.join(" < ") #-> "Fixnum < Integer < Numeric < Object"
Последняя строка примера мне особенно нравится. В ней наглядно представлена вся иерархия наследования для класса Fixnum. Очень странно, что такой вопрос довольно часто возникает, а специального метода для его решения -- нет. Непорядок!
class Class def hierarchy ancestors - included_modules end end Fixnum.hierarchy.join(" < ") #-> "Fixnum < Integer < Numeric < Object"
Для поиска списка всех родительских классов мы создали метод .hierarchy в классе Class. Родительские классы возвращаются в виде массива объектов класса Class. Вот теперь порядок!
|
Следует помнить, что подключенные примеси и используемые классы -- это понятия, которые действительны только для классов (то есть объектов класса Class) |
[править] Является ли данный объект экземпляром этого класса?
Вопрос дурацкий, но тем не менее стоит того, чтобы его осветили. Почему дурацкий? Да потому, что мы запросто можем узнать класс объекта при помощи метода .class и сравнить два класса при помощи методов == и !=.
Fixnum == 5.class #-> true [].class != Array #-> false String == Array #-> false
Для подобной же цели придуман специальный метод .instance_of?, который позволяет заменить связку методов .class и ==.
5.instance_of?( Fixnum ) #-> true 5.instance_of?( Integer ) #-> false [].instance_of?( Array ) #-> true String.instance_of?( Array ) #-> false String.instance_of?( Class ) #-> true
|
Метод .instance_of? можно перевести на русский, как вопрос "является ли экземпляром класса?" |
Обратите внимание, что число пять хоть и является целым, но метод .instance_of? не признает его экземляром класса Integer. В принципе он прав: число пять является экземпляром класса Fixnum, а не Integer. Это означает, что метод .instance_of? излишне строг.
И что же нам делать, если эта строгость нам ни к чему? Для этого можно воспользоваться методами .class, .ancestors и .include?, которые будут учитывать не только текущий класс, но и его родительские классы с примесями.
my_number = 5 my_number.class.ancestors.include?( Fixnum ) #-> true my_number.class.ancestors.include?( Integer ) #-> true my_number.class.ancestors.include?( Enumerable ) #-> false my_number.class.ancestors.include?( Comparable ) #-> true
Подобный код замечательно решает поставленную нами задачу, но выглядит громоздким. Немудрено, что для подобной задачи применяется специальный метод .is_a? (и его менее популярный псевдоним .kind_of?).
my_number = 5 my_number.is_a?( Fixnum ) #-> true my_number.is_a?( Integer ) #-> true my_number.is_a?( Enumerable ) #-> false my_number.is_a?( Comparable ) #-> true
Результаты примера следует интерпретировать так: число пять является целым (класс Integer), но не слишком большим (класс Fixnum); при этом, элементы этого типа можно сравнивать между собой (примесь Comparable), но перечислимым множеством они не являются (примесь Enumerable).
|
[править] А как сравнивать классы между собой?
Сравнением классов мы занимались и ранее, но использовали только методы != и ==. Помимо них, можно использовать и все остальные: >, <, >=, <= и даже <=>.
|
Подобное разнообразие методов сравнения связано с тем, что в класс Class включили примесь Comparable, то есть сделали объекты класса Class сравнимыми между собой |
Для того, чтобы разобраться в работе методов сравнения, рассмотрим несколько примеров, а потом подробно разберем их результаты.
Fixnum < Numeric #-> true Object > Integer #-> true Float < Integer #-> nil IO <= File #-> false
Метод "меньше" воспринимается Ruby, как вопрос "является ли потомком?", то есть первый пример можно расценить как вопрос вида: "класс Fixnum является ли потомком класса Numeric?" Если мы взглянем на иерархию наследования чисел, то увидим, что утвердительный ответ (true) на этот вопрос вполне оправдан.
Следуя той же логике, метод "больше" воспринимается как вопрос "является ли родителем?", то есть второй пример можно расценить как вопрос вида: "класс Object является ли родителем класса Integer?" Так как все классы унаследованы от класса Object (кроме самого Object), то утвердительный ответ (true) не должен нас сильно удивить.
Третий пример нам демонстрирует одну интересную особенность методов сравнения: если классы не состоят в отношениях "родитель-потомок", то они являются несравнимыми и поэтому метод сравнения отреагировал на эту ситуацию возвратом nil.
|
Напомним, что в логическом контексте объект nil равносилен объекту false (то есть они являются взаимозаменяемыми) |
Последний пример демонстрирует работу метода "меньше или равно". Он соединяет в себе методы "меньше" и "равно", то есть воспринимается как вопрос вида: "является ли класс IO классом File или его потомком?" Исходя из того, что класс File является потомком класса IO, а не наоборот, ответ на этот вопрос отрицательный (false).
[править] Какие константы доступны?
Чтобы узнать какие константы объявлены в том или ином классе/модуле необходимо вызвать метод .constants (еще одна "неожиданность") от этого класса/модуля.
class MyClass MY_CONST = "все равно какое значение" end MyClass.constants #-> ["MY_CONST"] Math.constants #-> ["E", "PI"] Math::E #-> 2.71828182845905 Math::PI #-> 3.14159265358979 MyClass::MY_CONST #-> "все равно какое значение"
[править] Эта константа объявлена?
С высоты полученных ранее знаний, мы можем решить эту задачу следующим образом:
Math.constants.include?( "Pi" ) #-> false Math.constants.include?( "PI" ) #-> true
Для решения задачи использовалась комбинация методов .constants и .include?, что не совсем рационально, так как метод .constants возвращает массив, содержащий список всех констант модуля или класса. В нашем случае это не совсем критично, но существуют классы (вроде Object), где количество констант может составлять десятки, а то и сотни. Поэтому, правильней будет использовать специальный метод .const_defined?, который сделает то же самое, но без промежуточного массива (чем сэкономит нам массу ценнейших микросекунд и байт памяти).
Math.const_defined?( "Pi" ) #-> false Math.const_defined?( "PI" ) #-> true
Этот же метод хорошо использовать для проверки доступности того или иного класса, так как практически все классы представляют из себя константы класса Object.
|
Существует способ создать класс, но не присваивать ему имя. Только этот способ мы пока рассматривать не будем |
Object.const_defined?("Math") #-> true Object.const_defined?("Complex") #-> false
Из примера следует, что модуль Math с математическими методами доступен, а вот классу комплексных чисел повезло меньше.
[править] Как по имени константы получить ее значение?
Давайте еще раз посмотрим на пример получения доступных констант:
Math.constants #-> ["E", "PI"]
Мы лишь узнали имена констант, но их значения нам пока не доступны. Для того, чтобы получить значения константы существует метод .const_get (который также следует вызывать от конкретного класса или модуля). Для примера, давайте заменим имена констант из предыдущего примера на их значения:
Math.constants.map{ |const_name| Math.const_get( const_name ) }#-> [2.71828182845905, 3.14159265358979] Math.const_get( "PI" ) #-> 3.14159265358979 Math::PI #-> 3.14159265358979
|
Обратите внимание, что внутри итератора .map нам приходится указывать конкретный модуль (Math), чтобы метод .const_get нормально работал. Иначе возникнет ошибка вида NoMethodError |
[править] Какие классы доступны для использования?
Так как имена всех классов являются константами, то сведем задачу в предыдущей -- найдем все константы класса Object (он является базовым для всех остальных классов), которые являются объектами класса Class:
classes = Object.constants.select{ |const_name| Object.const_get( const_name ).is_a? Class }.sort #-> ["ArgumentError", "Array", ..., "ZeroDivisionError"] classes.size #-> 79
Последняя сортировка (метод .sort) нужна для красоты, так как в исходном массиве (который вернул итератор .select) имена констант не упорядочены.
|
Все ошибки имеют свой собственный класс. Сделано это для того, чтобы можно было обработку ошибок настраивать на конкретный класс ошибок. Имена таких классов всегда заканчиваются на слово Error |
Все бы хорошо, но почти восемьдесят классов -- это перебор. Надо бы удалить классы, которые являются классами ошибок, так как они не очень нам интересны. Для этого надо избавиться от классов, имена которых заканчиваются на слово Error. Чтобы это осуществить, необходимо добавить к предыдущему примеру следующий код:
without_error = classes.select{ |class_name| !class_name[/Error$/] } #-> ["Array", "BasicSocket", ..., "UnboundMethod"] without_error.size #-> 54
Те читатели, которым интересны только классы ошибок, могут использовать следующий код:
classes - without_error #-> ["ArgumentError", ..., "ZeroDivisionError"]
Упорядоченность результата в последнем примере унаследована от упорядоченности массива classes.
[править] Как изменить значение константы?
Вы сами хоть поняли, что спросили? Константа потому и константа, что ее значение неизменно. Ситуаций, когда необходимо изменить значение константы просто не должно возникать!
|
Любые попытки поменять значение константы будут вызывать бурную реакцию со стороны интерпретатора. Он будет выдавать предупреждение вида: "значение этой константе уже присвоено" |
Тем не менее, существует способ создать новую константу. Сделать это можно посредством метода присвоения.
Pi = 3.14 Pi #-> 3.14 Math::Pi = 3.1415 Math::Pi #-> 3.1415
|
Создавать новую константу можно не только для корневого класса (Object), но и для любого другого модуля или класса (имя которого необходимо указать перед вызовом метода) |
Все это замечательно, но что делать если задано только имя константы в виде строки? Для этих целей используется метод .const_set, который нужно вызывать от класса, в который следует добавить новую константу. Давайте перепишем предыдущий пример с тем предположением, что имя константы нам задано в виде строки.
Object.const_set("Pi",3.14) Pi #-> 3.14 Object.const_get("Pi") #-> 3.14 Math.const_set("Pi",3.1415) Math::Pi #-> 3.1415 Math.const_get("Pi") #-> 3.1415
|
Напоминаем, что для получения значения константы, заданной только своим именем, осуществляется методом .const_get |
Еще немножко усложним задание. Пусть теперь в виде строки задано не только имя константы, но и имя класса. Тогда решение может выглядеть следующим образом:
Object.const_get("Math").const_set("Pi",3.1415) Object.const_get("Math").const_get("Pi") #-> 3.1415 Object.const_get("Math").const_get("PI") #-> 3.14159265358979
|
Все это время мы создавали константу Pi в модуле Math. Имейте в виду, что в реальности это не требуется, так как в этом самом модуле уже задана константа PI, которая имеет достаточно точное значение числа Пи |
Получается достаточно длинное выражение, но иногда у программиста просто нет возможности его сократить.
[править] Какие переменные доступны?
Переменные бывают следующих видов: экземпляра, класса, локальные и глобальные. Первые два вида переменных еще называют атрибутами и они имеют очень важное значение для самопознания. Тем не менее, мы сначала рассмотрим методы самопознания для последних двух видов переменных, так как работа с ними наиболее простая и незамысловатая.
[править] Какие глобальные переменные доступны?
Для того, чтобы получить список глобальных переменных надо вызвать метод global_variables (который следует вызывать без указания класса, так как он является закрытым).
global_variables #-> ["$-v", "$FILENAME", ..., "$KCODE"] global_variables.include?( "$a" ) #-> false $a = 1 global_variables.include?( "$a" ) #-> true
Метод .include? применен для того, чтобы читателю не пришлось просматривать весь список глобальных переменных в поисках $a.
|
Имена глобальных переменных всегда имеют префикс $. Переменные называются глобальными потому, что их область видимости распространяется на всю программу, а не только на какой-то конкретный класс, объект или метод |
[править] Какие локальные переменные доступны?
Список локальных переменных доступен посредством вызова метода local_variables (который также следует вызывать без указания класса, так как он тоже является закрытым).
local_variables #-> [] a = 1 local_variables #-> ["a"]
|
Имена всех локальных переменных начинаются со строчной latinskoi' буквы или знака _. Локальными они называются потому, что их область видимости распространяется только на конкретный блок или метод |
Почему глобальные и локальные переменные считаются не интересными с точки зрения самопознания? Дело в том, что значения этих переменных лишь косвенным образом влияют на текущее состояние классов или объектов. Именно поэтому, среди методов самопознания отсутствуют те, которые позволили бы получить или изменить текущее состояние глобальной или локальной переменной.
[править] Какую информацию можно получить о переменных экземпляра?
Теперь перейдем к более полезным и востребованным видам переменных: экземпляра (объекта) и класса. Начнем по-порядку, с переменных экземпляра.
|
Метод получения массива имен переменных экземпляра называется .instance_variables (префикс instance_ практически всегда указывает на принадлежность к экземпляру).
class Sample def initialize @variable_1, @variable_2 = 1, "а можно не только число" end end my_object = Sample.new my_object.instance_variables #-> ["@variable_1", "@variable_2"]
В данном примере мы придумали класс Sample и создали конструктор этого класса .initialize, который вызвали посредством обращения Sample.new. Конструктор вернул нам экземпляр класса Sample, от которого мы и вызвали метод .instance_variables.
Теперь мы можем получить значение любой из этих переменных посредством вызова метода .instance_variable_get:
my_object.instance_variable_get( "@variable_1" ) #-> 1 my_object.instance_variable_get( "@variable_2" ) #-> "а можно не только число"
У переменных экземпляра можно не только узнать значение, но и поменять его. Осуществить это можно посредством метода .instance_variable_set
my_object.instance_variable_set( "@variable_1", 2 ) my_object.instance_variable_get( "@variable_1" ) #-> 2
Помимо рассмотренных методов получения/изменения, есть еще и метод удаления переменной экземпляра .remove_instance_variable, то есть можно в любой момент времени удалить переменную экземпляра. Давайте рассмотрим, что из этого получится:
class Sample def initialize @variable_1, @variable_2 = 1, "а можно и число" end def remove( name ) remove_instance_variable( name ) end end my_object = Sample.new my_object.instance_variables #-> ["@variable_1", "@variable_2"] my_object.remove("@variable_1") my_object.instance_variables #-> ["@variable_2"]
Создавать дополнительный метод .remove в классе Sample пришлось потому, что метод .remove_instance_variable является закрытым, то есть он может быть использован только внутри метода экземпляра. Если создание дополнительного метода не требуется, то можно использовать метод .instance_eval, который выполняет любой программный код к контексте экземпляра. Перепишем наш пример с использованием этого метода.
|
Блок метода .class_eval может иметь параметр -- текущий экземпляр (он же self), в контексте которого выполняется блок |
class Sample def initialize @variable_1, @variable_2 = 1, "а можно и число" end end my_object = Sample.new my_object.instance_variables #-> ["@variable_1", "@variable_2"] my_object.instance_eval{ remove_instance_variable( "@variable_1" ) } my_object.instance_variables #-> ["@variable_2"] my_object.instance_eval{ remove_instance_variable( "@variable_2" ) } my_object.instance_variables #-> []
Кстати, метод .instance_eval можно использовать для получения значения переменной экземпляра. Например вот так:
Sample.new.instance_eval{ @variable_1 } #-> 1
[править] Какую информацию можно получить о переменных класса?
Работа с переменными класса осуществляется практически также, как и с переменными экземпляра. С той лишь разницей, что в названии методов вместо слова instance_ надо писать class_. На этом можно было бы и закончить, но давайте все таки рассмотрим примеры применения этих методов.
|
В соответствии с вышеизложенным правилом, заменяем в названии метода .instance_variable слово instance_ на class_ и получаем название метода, который получает массив имен переменных класса.
class Sample @@variable_1, @@variable_2 = 1, "а можно и число" end Sample.class_variables #-> ["@@variable_2", "@@variable_1"]
Как и прежде, мы создали класс Sample, но на этот раз не стали реализовывать конструктор .initialize, так как для работы с классом совсем не обязательно создавать его экземпляр (методом .new).
Пришло время рассмотреть методы получения и изменения значения переменной класса. В примере мы изменим значение переменной класса (методом class_variable_set) и сразу же получим ее новое значение (методом class_variable_get). Основная проблема заключается в том, что оба метода являются закрытыми. Поэтому из вызов возможен только в контексте метода класса. Для решения этой проблемы мы создадим метод Sample.change, который будет менять переменную и возвращать ее новое значение в качестве своего результата.
class Sample def Sample.change( name, value ) class_variable_set( name, value ) class_variable_get( name ) end end Sample.change( "@@variable_1", 2 ) #-> 2
Честно говоря, идея создавать метод класса для того, чтобы изменить переменную класса -- не самая лучшая идея. Проблема заключается в том, что мы порождаем новую сущность, которая может нам и не понадобиться. Как раз для того, чтобы выполнить нужные нам закрытые методы без создания еще одного и существует метод .class_eval. Он позволяет выполнять произвольный программный код в контексте класса, а в качестве параметра принимает блок кода, который необходимо выполнить в нужном контексте. Перепишем наш пример с применением метода .class_eval:
Sample.class_eval{ class_variable_set( "@@variable_1", 2 ) class_variable_get( "@@variable_1" ) } #-> 2
Ну и напоследок рассмотрим увлекательный процесс удаления переменных класса. Делается это при помощи метода remove_class_variable и при помощи уже известного нам метода .class_eval (так как метод remove_class_variable является закрытым).
class Sample @@variable_1, @@variable_2 = 1, "а можно и число" end Sample.class_variables #-> ["@@variable_1", "@@variable_2"] Sample.class_eval{ remove_class_variable( "@@variable_1" ) } Sample.class_variables #-> ["@@variable_1"] Sample.class_eval{ remove_class_variable( "@@variable_2" ) } Sample.class_variables #-> []
|
Получить значение переменной класса без помощи метода class_variable_get (только при помощи .class_eval) у меня не получилось. Возможно, что это получится у вас |