RDRubyTutorial: Метапрограммный Ruby
октября 19, 2011 | Published in Ruby, Основы | 25 Comments
Метапрограммный Ruby
Эта статья-учебник даст вам самые основные знания и понимания программирования на языке Ruby. В ней, в отличие от RubyDev Ruby Tutorial (RDRT) не будет рассматриваться работа с какими-нибудь конкретными объектами, например строками или целыми числами, или с массивами, или с чем-либо еще. Мы будем рассматривать синтаксис и основные концепции языка и не будем изучать методы и приемы работы с определенными объектами — все это описано в официальной документации (учите английский).
Автор (то есть я) предполагает, что вы в общих чертах знаете, что такое Ruby, читали блог RubyDev или статью о Ruby в Wikipedia, по этому я не намерен повторяться и рассказывать о происхождении языка, истории, его авторе и прочих практически бесполезных вещах.
Данный учебник будет состоять из нескольких глав, которые будут пронумерованы.
1. Объекты
Ruby — объектно-ориентированный язык, а значит все, с чем мы работаем — объекты. Объекты принадлежат определенному классу (типу), например вы, мой дорогой читатель, которого, предположим зовут Александр Сергеевич принадлежите классу людей, или группе людей, или ваш тип, как объекта — человек.
Александр Сергеевич, кто вы? — Я — Человек!
Объект — это набор свойств и методов. Александр Сергеевич, вы например, имеете свойства: имя, фамилия, отчество, рост, вес, цвет волос и так далее, но также вы имеете методы, то есть вы можете выполнять некоторую работу (выполнять некоторые функции), например, сидеть, стоять, разговаривать и отвечать комментариями на мои статьи.
Для того, чтобы появился новый объект вашего типа, Александр Сергеевич, вам следует совокупиться с некоторой человеческой особью женского пола и через 9 месяцев будет получен новый объект класса человек. В Ruby процесс создание новых объектов более быстрый и менее ресурсоемкий. Чуть позже мы разберемся с ним, а сейчас поговорим о классах.
2. Классы
Класс это то, что порождает и описывает некоторую коллекцию объектов. Например, вы, Александр Сергеевич и еще 7.5 миллиардов людей принадлежите классу Человек потому, что у вас всех имеется набор общих свойств и методов. То есть, хотя вы отличаетесь внешне от гаитянина и еще больше отличаетесь от гаитянки, но вы гораздо больше похожи на них, чем, например, на плавленный сырок. В реальном мире классы только описывают и объединяют объекты, но не плодят их. Нет какого-то класса Человек, который бы создавал всех людей, и нет класса Плавленный Сырок, который бы создавал все плавленные сырки. В Ruby и в других языках мы вынуждены использовать классы для создания объектов, хотя в других языках имеется иная парадигма программирования — Прототипы, где один объект может порождать другой, что очень похоже на то, как размножаются люди, животные и некоторые растения, но не похоже на то, как на свет появляются плавленные сырки.
3. Основные (примитивные) типы данных
Каждый язык программирования оперирует определенными данными. Эти данные имеют определенный тип, например, коллекции и числа. В Ruby таких типов очень много и вы можете создать еще больше собственных, однако каждый тип данных (класс) использует более примитивные типы, например, чтобы молвить слово вам необходимо знать буквы, а чтобы разговаривать целыми предложениями вам необходимо знать слова. Основными типами в Ruby являются следующие:
- Строки — произвольного размера коллекции символов.
- Целые числа — обыкновенные целые числа отрицательные или положительные.
- Дробные числа (числа с плавающей точкой)
- Массивы — произвольного размера коллекции произвольных элементов для доступа к которым используются целочисленные ключи. Это как АТС, где для звонка Васе, Пете и Жоре необходимо знать их номера телефонов.
- Хэши (ассоциативные массивы) — это, грубо говоря, массивы в которых для доступа к элементам могут быть использованы ключи любого типа.
- Символы — это, если совсем просто, числа набранные прописью, например: двадцать семь.
Эти типы называются основными потому, что они не используют других типов для своего представления и могут быть использованы для представления других типов, например, имя человека может быть представлено как строка, возраст как целое число и число в плавающей точкой если интересует полный возраст, а не количество полных лет, круг друзей в виде массива элементами которого являются также объекты типа Человек.
Объекты основных типов данных также обладают свойствами и методами, как и любой другой объект в Ruby. Например, строки имеют свойство — длину, и имеют методы позволяющие притворять строку в символ.
4. Практика работы с примитивными объектами
Пример стоки (класс String):
"RubyDev.ru"
На самом деле «RubyDev» является литералом, то есть самими данными не являющимися сами по себе объектом, которые однако конвертируются в объект при необходимости, например, попытке вызова для литерала метода.
Пример простого числа и числа с плавающей точкой (классы Fixnum и Float):
2011 36.6
Пример символов (класс Symbol):
:rubydev :rubydev_ru
Пример массивов (класс Array):
[1,2,100,"RubyDev.ru", :rubydev]
Пример ассоциативных массивов (класс Hash):
{"key" => "value", 1 => "Hello!", :blog => "rubydev.ru", "hash" => {1 => 2, "on" => :ON}}
Помимо создания основных объектов из литералов вы можете использовать менее популярный способ — создания основных объектов из их классов, например:
Hash.new String.new
и т.д.
Пример использования методов для объектов:
"RubyDev".size #=> 7 "Ruby" + "Dev" #=> "RubyDev" #+ - это также метод: "Ruby".+("Dev") #=> "RubyDev" [1,2,3] << 100 #=> [1,2,3,100]
5. Методы и приемники
Методы — это совсем просто говоря куски кода, которые выполняют определенное действие и имеют имя, а также принадлежат определенному объекту. Приемник — это объект, который принимает вызов метода.
«RubyDev».size#=>7
«RubyDev» — объект — приемник
size — метод
7 — возвращаемое значение
1 + 2 #=> 3
1 — приемник
+ — метод
2 — аргумент метода
3 — возвращаемое значение
Для того, чтобы приемник мог ответить на вызов в нем должен быть определен вызываемый метод, иначе произойдет ошибка NoMethodError.
Часто можно увидеть вызов метода без приемника, например:
puts "Hello!" #Hello!
В таком случае интерпретатор Ruby понимает наш код как такой, что вызывает метод для приемника self. О том, что такое self мы поговорим позже.
6. Практика работы с классами и методами
Для того, чтобы создать класс необходимо воспользоваться специальным оператором class. Типичное определение класса выглядит так:
class Human end
- так выглядит совсем простой класс, который называется Human. Этот класс так же как и любой другой может плодить объекты типа Human:
h = Human.new #=> #<Human:0x8c750d0>
h — переменная, которой мы ссылаемся на объект в оперативной памяти. h не является самим объектом, но только ссылкой на него, при необходимости мы можем сослаться переменной h на любой другой объект. Тем не менее, мы будем говорить «объект h» для простоты.
Human — наш класс
.new — метод класса Human. В Ruby классы — это тоже объекты, объекты класса Class:
Human.class #=> Class
Метод .new наследуется классом Human от класса Object:
Human.superclass #=> Object
Таким образом мы можем использовать метод .new для приемника — класса Human при этом не определяя этого метода в самом классе.
Что такое наследование вы узнаете немного позже.
Если для объекта h вызвать метод #class, то нам будет возвращен класс Human.
h.class #=> Human
В зависимости от того принадлежит метод в обычном объекте или в классе, мы называем методы методами класса и методами объекта. Для того, чтобы различать методы класса Human и методы объектов класса Human мы будем перед методами классов ставить точку, например: .class_method, а перед именами методов экземпляров классов ставить шарп, например: #instance_method.
Пользы от нашего класса Human очень мало ведь он не определяет методов экземпляров и не обладает собственными методами. Чуть не забыл! Объект, экземпляр и копия класса (instance) — это все определения одного и того же явления. Для того, чтобы объявить метод используется зарезервированное слово def, например:
class Human def my_method "returned value" end end
Здесь мы в контексте класса объявили метод с именем #my_method, который является методов экземпляра класса Human, но не методом самого класса. Если мы вновь создадим объект класса Human, то мы сможем воспользоваться этим методом:
h = Human.new #=> #<Human:0x8c3c244> h.my_method #=> "returned value"
В Ruby все возвращает значение, например метод h#my_method возвращает строку «returned value», а метод Human.new возвращает экземпляр класса Human. Возвращаемое значение, как правило является значением получаемым в последнем выражении, поскольку в данном случае последним выражением является строка «returned value», то эта же строка будет возвращена после выполнения метода. Вы можете явно указывать возвращаемое значение используя return, он полезен, тогда, когда сразу сложно определить что будет возвращено, например при нахождении в методе условий:
def what_is_bigger(a,b) if a > b return a elsif a < b return b else return "#{a} equal #{b}" end end #=> nil what_is_bigger(100,100) #=> "100 equal 100" what_is_bigger(1,2) #=> 2 what_is_bigger(2,1) #=> 2
Метод what_is_bigger принимает два аргумента: a и b, при вызове метода и передаче ему значений этих двух аргументов, в контексте метода создаются локальные переменные с соответствующими аргументам именами: a и b, которые мы используем для доступа к значениям переданным в метод. Локальные переменные — это переменные областью видимости которых является исключительно тот блок кода в котором они определены, либо тело метода, условного оператора или цикла.
Вы уже знаете как определять метод экземпляра, но не знаете как определять метод самого класса. Метод класса объявляется аналогично методу экземпляра класса с той лишь, разницей, что перед именем метода мы используем self отделенное от имени метода точкой, пример:
class Human def my_method "instance method" end def self.my_method "class method" end end Human.new.my_method #=> "instance method" Human.my_method #=> "class method"
Слова «мы используем self отделенное от имени метода точкой» сказанные выше звучат глупо и не передают суть, но пока вам следует с этим смириться. Мы рассмотрим что такое self и почему так определяются методы классов позже.
7. Переменные
Ранее в этом учебнике мы уже работали с переменными, но только с локальными. В Ruby имеется 4 типа переменных + константы.
- Локальные переменные — это переменные имена которых начинаются с прописной латинской литеры или символа подчеркивания. Локальные переменные доступны только в той области видимости, где они определены. Пример:
class Human def self.set_var(val) var = val end def self.print_var puts var end end Human.set_var "hello" Human.print_var #=> undefined local variable or method `var'
- Глобальные переменные — это переменные, имена которых начинаются со знака доллара «$» и которые доступны в любом месте кода независимо от того, где они определены. Пример:
class Human def self.set_var(val) $var = val end def self.print_var puts $var end end Human.set_var "hello" Human.print_var #hello
- Переменные класса — это переменные имена которых начинаются с двух знаков «собака» — «@@» и которые доступны везде в контексте класса и его объектов. Пример:
class Human def self.set_var(val) @@var = val end def self.print_var puts @@var end def set_var(val) @@var = val end def print_var puts @@var end end Human.set_var "hello" Human.print_var #hello h1 = Human.new h2 = Human.new h1.print_var #hello h2.print_var #hello h1.set_var "bye!" h1.print_var #bye h2.print_var #bye
Использование переменных класса — отличный способ реализации взаимодействия между однотипными объектами.
- Переменные объекта (экземпляра класса) — это переменные областью видимости которых является объект. Имена этих переменных начинаются с одного символа «собаки» — «@». Пример:
class Human def name=(name) @name = name end def name @name end end h1 = Human.new h1.name = "Vladimir" h2= Human.new h2.name = "Alexander" h1.name #=> Vladimir h2.name #=> Alexander
- Константы — это такие ссылки (переменные), которые предназначены хранить неизменяемые значения. В отличие от некоторых других языков, в константах можно переопределять значение константы. Имена констант начинаются с заглавных литер. Имена классов и модулей являются константами. Пример:
class Human CONST = "I'm a constant." CONST = "New Value" def get_const puts CONST end end #=>warning: already initialized constant CONST h1 = Human.new h1.get_const #=> New Value
Переопределить константу в контексте какого-нибудь метода нельзя, это вызовет ошибку: SyntaxError: dynamic constant assignment.
8. Практика работы с классами и объектами
Напомню как выглядит создание объекта.
class MyClass def hello puts 'Hello!' end end mc = MyClass.new
Определить тип объекта (его класс) можно используя метод #class:
mc.class #=> MyClass
А теперь поговорим о конструкторах в Ruby. Метод — конструктор, это метод, который автоматически выполняется при создании нового объекта. В Ruby метод конструктор носит имя #initialize. Конструкторы используются для установки начального состояния объекта. Пример использования конструктора:
class User def initialize(name) @name = name end def name @name end end user = User.new "Vladimir" user.name #=> "Vadimir"
Настало время познакомиться с аксессорам (access — доступ). Аксессоры — это методы доступа к переменным. Без аксессоров мы не сможем получать данные о состоянии объекта и изменять его состояние. Аксессоры бывают двух типов: get и set и называются соответственно getter и setter аксессорами. set-аксессоры используются для установки значения, а get-аксессоры используются для получения значений переменных. Пример методов-аксессоров:
class User def initialize(name) @name = name end def name #getter @name end def name=(uname) #setter @name = uname end def set_name(uname) #еще один setter @name = uname end end
Теперь представьте, что у вас таких переменных несколько десятков или сотня. Объявляя аксессоры в описанный выше способ станет большой тратой времени и загрязнит код. Наша статья называется «Метапрограммный Ruby» и вот только сейчас мы начинаем приближаться к изучению самого метапрограммирования.
Для того, чтобы упростить процесс создания аксессоров в каждом классе имеются специальные методы-макросы (так называют методы, которые выполняются в контексте класса): .attr_accessor, .attr_writer, .attr_reader.
.attr_reader используется для создания get-аксессоров.
.attr_writer используется для создания set-аксессоров.
.attr_accessor комбинирует в себе .attr_reader и .attr_writer.
Пример использования:
class User attr_accessor :name, :lname attr_reader :email def initialize(name, lname, email, pass) @name = name @lname = lname @email = email @password = pass end end user = User.new("Vladimir", "Melnik", "egotraumatic@some_mail.com", 12345) user.name #=> "Vadimir" user.lname #=> "Melnik" user.email #=> "egotraumatic@some_mail.com" user.name = "Thomas" user.name #=> "Thomas"
Мы передаем в attr_-максоры имена переменных в виде символов, а затем эти макросы прям во время выполнения программы создают методы-аксессоры. Это и есть самое настоящее метапрограммирование. Как видите польза от метапрограммирования в Ruby уже в этом одном примере велика.
Cуществует также макрос .attr, который позволяет заменить attr_accessor для одного атрибута, при этом создание get-аксессора опционально, он будет создан только если второй аргумент — true. Пример:
class MyClass attr :hello attr :bye, true def initialize @hello = "hello" @bye = "bye" end end MyClass.new.hello #=> hello MyClass.new.bye #=> bye MyClass.new.bye = "Good bye!" MyClass.new.hello = "Hi!" #=> NoMethodError
9. Наследование и поиск приемника метода в иерархии наследования
Чуть выше мы уже немного затрагивали темы наследования, а сейчас мы поговорим о наследовании более подробно. Наследование — это отношение между классами, когда один класс наследует свой код другому классу, это похоже на то, как от обезьян человек унаследовал что-то около 99% генов.
Один класс наследуется от другого то есть как бы копирует его, после чего добавляет к унаследованному функционалу новый или расширяет его. Пример:
class Human attr_accessor :name, :lname def initialize(name, lname) @name = name @lname = lname end end class Soldier < Human def pif_paf puts "Pif! Paf!" end end soldier = Soldier.new("Ivan", "Zaytzev") soldier.name #=> Ivan soldier.pif_paf #Pif! Paf!
В примере выше класс Soldier наследуется от класса Human.
Наследование — один из основных инструментов объектно-ориентированного программирования, которое позволяет писать более чистый и относительно легкоподдерживаемый код. Например, мы можем создать класс Record, который будет определять методы и свойства каких-нибудь абстрактных записей. Далее мы можем унаследоваться от Record классами MySQLRecord, PgSQLRecord и даже FileRecord, которые уже будут представлять собой некоторое подобие ORM и предоставлять доступ к записям соответственно из баз данных MySQL, PostgreSQL и обыкновенных файлов с определенным форматированием текста, например CSV, YAML, JSON, но тогда уже будет лучше унаследовать классы CSVRecord, YAMLRecord и JSONRecord от класса FileRecord. Для классов MySQLRecord и PgSQLRecord можно также ввести промежуточный класс SQLRecord. Таким образом общий код для нескольких классов будет находиться в их родительском классе и при обнаружении каких-нибудь неполадок вам необходимо будет внести исправления только в одном месте — родительском классе.
Теперь поговорим о поиске приемника в иерархии вызова. Видите ли, объекты не обладают методами, методы объектов определены внутри их классов. Объекты являются только набором свойств и «ссылкой» на собственный класс.
Когда вы вызываете метод для определенного объекта, он перенаправляет вызов к своему классу и тот выполняет метод в контексте объекта, то есть с использованием набора переменных и их значений, которым, по сути, и является объект.
Если метод не обнаружен в классе которому принадлежит объект, то поиск метода производится в родительском классе и подмешиваемых модулях. Поиск производится вплоть до самого базового класса — BasicObject. Если метод не найден, то будет возвращена ошибка NoMethodError.
10. Singleton класс и self
Не путайте Singleton класс с паттерном Singleton — это совсем разные вещи. Согласен с мнением многих людей в том, что это не самое удачное и правильное название. Кроме singleton class у того, о чем мы сейчас будем говорить имеются и другие — не официальные названия, например eigen-class, meta-class, ghost-class и еще несколько которые я или забыл или не знал.
Singleton класс — это особый, безымянный (анонимный), скрытый класс, который встраивается между объектом и классом объекта, наследуется от класса объекта. Singleton предназначен для хранения уникальных для объекта методов, которые из-за их хранения в singleton классе называют singleton-методами.
Здесь раскрывается тайное различие между объектами и классами. Дело в том, что обыкновенные объекты не могут хранить методы, а классы могут, но, правда, только методы собственных объектов, но не собственные методы. Собственные методы классов, как и методы обыкновенных объектов хранятся в классах от которых класс наследуется или в singleton классе класса. Сложно да?
Суть такова:
- Классы — это объекты базового класса Class.
- Классы — это объекты, которые могут порождать объекты, и которые могут иметь имя — константу.
- Классы — это объекты, которые могут хранить методы своих объектов.
- Классы, как и обыкновенные объекты не могут хранить собственные методы. Методы классов также хранятся в их Singleton классах.
Singleton классы невидимы невооруженным глазом, однако у каждого объекта имеется методы .singleton_class и .singleton_methods для доступа соответственно к собственному Singleton классу и получения списка собственных методов, то есть методов собственного singleton класса. Пример:
class Human attr_accessor :name, :lname def initialize(name, lname) @name = name @lname = lname end end human = Human.new('Ivan', 'RubyDev') puts human.class #=> Human puts human.singleton_class #=> #<Class:#<Human:0x99edf78>> puts human.singleton_methods.size #=> 0 def human.hello puts "Hello, I'm #{@name}." end human.hello #=> "Hello, I'm Ivan." human.singleton_methods #=> [:hello]
Настало время поговорить о self. self — это указатель внутри объекта на самого себя. Когда мы объявляем метод класса используя указатель self в качестве приемника, мы как бы говорим классу — «Объяви этот метод для себя!», но поскольку класс, как любой другой объект не может хранить собственные методы, то он сохраняет их в собственном singleton классе. Пример:
class MyClass def self.my_method puts "I'm #{self}#my_method" end end MyClass.my_method #I'm MyClass#my_method MyClass.singleton_class #=> #<Class:MyClass> MyClass.singleton_methods #=> my_method
Теперь все с singleton (eigen, ghost, …) классами стало понятнее и мы еще и познакомились с указателем self.
Кроме использования self-приемника существует альтернативный способ обращения к контексту singleton-класса. Пример:
class MyClass class << self def hello puts 'Hello!' end end end MyClass.hello # Hello! mc = MyClass.new class << mc def bye puts "Bye!" end end mc.bye # Bye! mc2 = MyClass.new mc2.bye #NoMethodError
11. Базовые классы
Базовые классы — это самые основные классы, от которых происходят все остальные классы и модули. К базовым классам относятся следующие: BasicClass, Object, Class, Module.
class MyClass end MyClass.class #=> Class MyClass.superclass #=> Object Class.class #=> Class Class.superclass #=> Module Module.class #=> Class Module.superclass #=> Object Object.class #=> Class Object.superclass #=> BasicObject
Что!? Клас Class — экземпляр самого себя? некоторые люди называют это парадоксом курици и яйца. Мы не будем разбираться с этим вопросом, все, что необходимо знать, это:
- Каждый класс — экземпляр класса Class.
- Каждый класс, если явно не наследуется от определенного класса, наследуется от класса Object.
- Самый низкоуровневый класс — это BasicObject. Он на столько низкоуровневый, что даже не наследуется от какого-либо класса.
12. Модули
Модули — это, грубо говоря, классы с ограниченной функциональностью, они не могут наследоваться и наследовать, но могут подмешивать и подмешиваться. Еще одно неформальное определение модулей — это «контейнер содержащий в себе код». Модули объявляются при помощи оператора module и являются экземплярами класса Module:
module MyMod end puts MyMod.class
Модули используются для двух целей: объединения кода в одно целое и для реализации множественного наследования. В Ruby отсутствует возможность множественное наследование, как и в многих других языках программирования, но отсутствие множественного наследование с лихвой компенсируется наличием механизма подмешивания. Пример:
module MyMod def self_name puts self end def self.self_name puts self end end MyMod.self_name #=> MyMod MyMod::self_name #=> MyMod class MyClass include MyMod end MyClass.new.self_name #=> #<MyClass:0x90acd38> MyClass.self_name #=> NoMethodError
Метод объявленный в модуле с приемником self является методом модуля (методом, что хранится в singleton классе модуля). Метод .self_name можно вызвать двумя способами:
MyMod.self_name #традиционный способ "приемник.метод". MyMod::self_name #синтаксис обращения к содержимому модуля.
Второй метод #self_name что объявлен без приемника используется исключительно для подмешивания. Существует два режима подмешивания:
- include — режим, когда методы из модуля подмешиваются как методы экземпляра класса.
- extend — режим, когда методы из модуля подмешиваются как методы класса.
В примере выше мы использовали метод .include для подмешивания методов модуля как методов экземпляра класса MyClass. Ниже приведен пример с использованием метода .extend:
class MyClass extend MyMod end MyClass.self_name #=> MyClass MyClass.new.self_name #=> NoMethodError
У модулей имеется два крайне полезных метода: .extended и .included, эти методы автоматически выполняются при соответствующем методе подмешивания.
Часто нам необходимо подмешать в класс одновременно и методы экземпляра класса и методы класса. Для этого те или иные методы выносятся в отдельный модуль, который подмешивается внутри автоматически запускаемого метода .extended или .included. Пример:
module MyMod def self.included(klass) klass.extend ClassMethods end def self_name puts self end module ClassMethods def self_name2 puts self end end end class MyClass include MyMod end MyClass.self_name2 #=> MyClass MyClass.new.self_name #=> #<MyClass:0x895a8dc>
С модулями разобрались.
13. Метапрограммировани: eval — методы
В Ruby существует целых четыре eval-метода:
- eval — собственно традиционный eval.
- class_eval — eval, который выполняет код в контексте класса.
- instance_eval — eval, который выполняет код в контексте экземпляра класса.
- module_eval — eval, который выполняет код в контексте модуля.
Традиционно eval — это функция или метод, который принимает в качества аргумента строку, которую интерпретирует и выполняет как код. В Ruby, eval-методы могут принимать в качестве аргумента блок кода и выполнять его в определенном контексте.
Использование метода .eval считается плохим стилем поскольку использование .eval может нарушить безопасность приложения. Вместо .eval рекомендуется использовать другие, описанные выше eval-методы. Вообще рекомендуется, как и всему в этой жизни, знать меру с использованием eval-методов, поскольку они несколько запутывают код.
Примеры использования eval — методов:
# eval eval("puts 'Welcome to RubyDev!'") #Welcome to RubyDev! # class_eval class MyClass end MyClass.class_eval(<<-DEFMETHOD) def self.my_method puts "I'm #{self} method." end DEFMETHOD MyClass.my_method class SomeClass end SomeClass.class_eval do def self.hello puts 'Hello!' end end SomeClass.hello #Hello! # instance_eval myc = MyClass.new myc2 = MyClass.new myc.instance_eval(<<-DEFMETHOD) def my_id self.object_id end DEFMETHOD myc.my_id # 72703890 myc2.my_id #NoMethodError myc2.instance_eval "def my_class_name;puts self.class;end" myc2.my_class_name # MyClass # module_eval module MyModule def self.hello puts "Hello!" end end MyModule.module_eval("hello") #Hello!
14. Открытые классы и модули
В Ruby классы являются открытыми. Это означает что уже определенный класс вы можете в любом месте кода открыть и добавить в него новый функционал или дополнить имеющийся. Многие сторонники Java и PHP, говорят, что это недостаток, ибо мы не можем доверять коду, поскольку кто-то может сделать что-то подобное:
class Fixnum def +(n) self ** n end end puts 3 + 10 # 59049
И программист будет рыдать горькими слезами пытаясь найти то место, где метод #+ был переопределен. На самом деле такое переопределение метода легко обнаружить, кроме того, мы и не должны доверять коду, здесь я соглашусь с Девидом Томасом (Dave Thomas), который говорил в одном из своих докладов, что мы должны доверять программистам, а не коду ибо программисты пишут код, а не код пишется самостоятельно. Мое личное мнение заключается в том, что я считаю данную возможность потрясающей! Вы можете взять предопределенный класс и в зависимости от состояния системы изменить его. Пример:
class MyClass end mc = MyClass.new class MyClass def hello puts "Hello!" end end mc2 = MyClass.new mc.hello #Hello! mc2.hello #Hello! def MyClass.bye puts "bye!" end mc3 = MyClass.new mc3.hello #Hello! MyClass.bye #bye!
Модули в Ruby можно аналогичным образом дополнять новыми методами, константами, другими модулями и классами непосредственно во время исполнения программы.
15. Работа с переменными из внешнего мира
Мы можем воспользоваться eval-методами для объявления некоторой переменной в некотором контекст, например:
class MyClass def self.hello puts @hello end end MyClass.class_eval("@hello =\"Hello!\"") MyClass.hello # Hello
Однако для таких целей в Ruby имеются специальные, более удобные методы. Код выше с .class_eval мы можем легко заменить следующим кодом с использование instance_variable_set:
MyClass.instance_variable_set("@hello", "Hello!") MyClass.hello # Hello
Кроме метода instance_variable_set которым обладают все объекты и классы и обычные объекты, имеется аналогичный get-аксессор к переменным, который позволяет получать их значения:
class MyClass def self.hello puts @hello end end MyClass.instance_variable_set("@hello", "Hello!") puts MyClass.instance_variable_get("@hello") # Hello!
Существуют также аналогичные методы для объявления переменных класса: .class_variable_set и .class_variable_get, которые работают по аналогии с описанными. Кроме всего этого, следует знать методы .class_variables и instance_variables, которые предоставляют массивы соответственно переменных класса текущего класса и переменных экземпляра класса текущего класса или простого объекта.
16. Контекст и контекстные блоки, глобальная область видимости
Контекст исполнения и область видимости переменных - это две очень важные вещи на которые стоит обратить особое внимание. Мы уже говорили о ссылке self, которая всегда ссылается на текущий объект и научились ее с пользой использовать, однако мы еще совсем мало знаем о контексте исполнения и области видимости переменных. Вот вам простая задачка:
class MyClass @@var = "class var" @var = "instance var" def self.hello @hello = "Hello!" puts @hello end end puts MyClass.class_variables # @@var puts MyClass.instance_variables # @var
Мы знаем, что имя переменной экземпляра класса (объекта) начинается с одного знака собачки "@", а переменные класса с двух "@@". Вопрос: Что это за переменная в методе MyClass.hello?
- Если не знаете - не нужно угадывать, давайте воспользуемся методами .class_variables и .instance_variables:
puts MyClass.class_variables # @@var puts MyClass.instance_variables # @var
А теперь проверим какими переменными обладает экземпляр класса MyClass:
puts MyClass.new.instance_variables # ничего, пустой массив
Куда же пропала переменная @hello и переменная @var? Первой вообще не видно, хотя метод .hello исправно печатает приветствие, а второй не видно в переменных экземпляра класса MyClass.
Все дело в том, что, как уже говорилось, классы - это также объекты. Благодаря этому классы имеют собственные переменные экземпляров классом и на самом деле переменные @var и @hello принадлежат не экземпляру класса MyClass, а самому объекту - классу MyClass. В чем же тогда разница между переменными класса и переменными объекта - класса? - Вопрос достаточно интересен!
Переменные экземпляра класса и переменные класса отличаются тем, что областью видимости переменных класса является класс и все унаследовавшие данный класс классы получают доступ к значениям этих переменных и могут переопределять их! Переменные экземпляра класса принадлежат только самому объекту и не могут быть унаследованы и изменены из внешнего мира без специальных методов - аксессоров. Традиционный пример:
class Animal @@voice = "---" def self.voice puts @@voice end end class Cat < Animal @@voice = "Meaw!" def self.meow puts @@voice end end class Dog < Animal @@voice = "Woof!" def self.woof puts @@voice end end Cat.meow #Woof! Dog.woof #Woof! Animal.voice #Woof! Cat.meow #Woof!
Почему так происходит? Дело в том, что классы инициализируются так же как и обыкновенные объекты и инициируются в порядке их объявления. Таким образом при инициализации класс Dog переопределил переменную @@voice, которая для всех трех классов является общей ведь переменная класса хранится в том классе, цепочки наследования классов, в котором она была впервые объявлена, то есть, при обращении к переменной @@voice или изменении ее значения мы имеем дело не с переменными классов Cat и Dog, а с переменной класса класса Animal.
Теперь перепишем этот же код с использованием переменных экземпляра класса:
class Animal @voice = "---" def self.voice puts @voice end end class Cat < Animal @voice = "Meaw!" def self.meow puts @voice end end class Dog < Animal @voice = "Woof!" def self.woof puts @voice end end Cat.meow #Meaw! Dog.woof #Woof! Animal.voice #--- Cat.meow #Meow!
Теперь вы действительно должны понимать разницу между переменными класса и экземпляра класса, и понимать разницу между переменными экземпляра класса класса и переменными экземпляра класса его объектов. Кстати, экземпляры класса не имеют доступа к переменным экземпляра класса своего класса, но имеют доступ к переменным класса своего класса и его родительских классов:
class MyClass @@myclass = "string value" def get_var @variable end end class RubyDev < MyClass @@rubydev ="http://rubydev.ru" @variable = "some value" def rubydev @@rubydev end def myclass @@myclass end end obj = RubyDev.new puts obj.instance_variables #у объекта собственных переменых нет obj.get_var #=> nil puts obj.rubydev #=> "http://rubydev.ru" puts obj.myclass #=> "string value"
Теперь вы знаете, что в примере выше у объекта не было видно в выводе метода #instance_variables переменной @var, потому, что она принадлежала классу, а не объекту. Однако, почему совсем нигде не видно переменной @hello? - Потому, что она объявлена в контексте метода .hello, а значит ее инициализация произойдет только при вызове этого метода. Не смотря на контекст переменная будет принадлежать объекту, а не методу, ведь она обозначена как переменная экземпляра класса, а не как локальная переменная.
Сейчас я расскажу о контекстных блоках. В Ruby имеется синтаксические элементы - блоки кода, которые также имеют, начиная с версии Ruby 1.9, собственную область видимости, но это не то, о чем я сейчас хочу рассказать, прошу не путать.
В Ruby существуют некоторые структуры, которые обладают собственными областями видимости, их код я и называю контекстным блоком так как он выделяется из общего кода и его код выполняется в собственной области видимости. Тело класса - контекстный блок, ведь он выделяется при помощи ключевых слов class ... end и по принятому стилистическому соглашению, перед строками кода из определения класса следует ставить два пробела. То же верно и для модулей и для методов и для блоков кода. То есть, контекстные блоки - это области видимости, которые мы явно видим в коде. Примеры:
#Область видимости mail module MyMod #область видимости MyMod class MyClass #область видимости MyClass def my_method #область видимости my_method end #область видимости MyClass end #область видимости MyMod end #область видимости main p = proc do #область видимости блока кода end
Каждая вложенная область видимости имеет доступ к переменным, константам и методам той области в которую она вложена. Однако, область видимости не имеет доступа к содержимому внутренних областей видимости. Пример:
class MyClass def initialize #метод выполняется автоматически при создании объекта и инициирует его переменные @hello = "Hello" end def hello #внутри контекстного блока метода мы имеем достп к переменной объекта puts @hello end def hello_hello #... и даже к другим методам hello end end MyClass.new.hello # Hello MyClass.new.hello_hello # Hello
Существуют также структуры языка, которые не определяют собственной области видимости, например циклы и условные операторы:
a = 1 while a < 5 puts a a = a + 1 b = a end puts b # 5 if b = 5 c = 100 end puts c # 100
Переменные объявленные в них доступны и вмещающей их области видимости.
Теперь поговорим о глобальной области видимости. Если мы запустим Ruby REPL - IRB и введем просто self, То мы увидим нечто загадочное:
$ irb
ruby-1.9.2-p180 :001 > self #=> main
Что такое main? Начиная изучать Ruby я и сам несколько удивился такому результату. Выходит, что глобальная область видимости - это тоже некий объект, ведь в нем мы можем использовать self и self возвращает нам глобальную область видимости. Стоит использовать всего один известный нам метод, как магическое поведение self растает:
$ irb
ruby-1.9.2-p180 :001 > self #=> main
ruby-1.9.2-p180 :002 > self.class #=> Object
Оказывается, self - это экземпляр класса Object!
В Ruby 1.8.7 нам это было бы легче вычислить:
$ irb
no such file to load -- awesome_print
ruby-1.8.7-p334 :001 > self
=> #<Object:0xb7793954 @prompt={:PROMPT_I=>"ruby-1.8.7-p334 :%03n > ", :PROMPT_N=>"ruby-1.8.7-p334 :%03n?> ", :PROMPT_S=>"ruby-1.8.7-p334 :%03n%l> ", :PROMPT_C=>"ruby-1.8.7-p334 :%03n > ", :AUTO_INDENT=>true, :RETURN=>" => %s \n"}>
ruby-1.8.7-p334 :002 > self.class
=> Object
17. Объявление методов при помощи .define_method и обработка NoMethodError при помощи #method_missing
Часто бывает необходимо объявить методы непосредственно во время выполнения программы. Специально для такой необходимости существует метод .define_method. Пример использования:
class RubyDev define_method(:hello) do puts "Hello!" end class << self define_method(:bye) do puts "Bye!" end end end RubyDev.new.hello # Hello! RubyDev.bye # Bye!
.define_method принимает имя метода в виде символа и блок кода, который следует выполнить при обращении к объявляемому методу. .define_method, естественно, работает медленнее чем объявление def и выглядит не так красиво, по этому .define_method следует использовать только там, где стандартный синтаксис определения методов не подходит.
При вызове каждого метода, поиск приемника происходит по всей цепочке наследования и подмешивания и если метод нигде не найден, то возвращается ошибка NoMethodError. Именно обработкой этой ошибки занимается метод #method_missing. #method_missing является методом - хуком, то есть таким методом, который вызывается автоматически по какому-то событию, в данном случае это событие - появление ошибки NoMethodError (точнее отсутствие вызываемого метода, ошибку возвращает сам #method_missing). #method_missing принимает в качестве аргумента имя вызываемого метода, его параметры и блок кода, который мог передаваться методу. Пример:
class RubyDev def method_missing(name, *args, &block) if name == :hello puts "Hello, #{args[0]}!" block.call else super end end end RubyDev.new.hello("Vasya") { puts "Bye!" } #Hello, Vasya! #Bye! RubyDev.new.ggg #=> NoMethodError
Когда мы объявляем метод #method_missing, то мы на самом деле переписываем уже объявленный метод, который возвращает ошибку NoMethodError при отстутствии вызываемого метода, по этому вам следует вызывать старую версию метода используя метод super, таким образом мы не просто переобъявляем метод, мы усовершенствуем уже имеющийся. Если мы пропустим вызов super, то при вызове отсутствующих методов, которые мы не обрабатываем в #method_missing не будут возникать ошибки NoMethodError, интерпретатор будет просто игнорировать вызов несуществующих методов, что совсем не верно, но иногда может пригодиться.
Часто .define_method используют внутри #method_missing. Это необходимо для кэширования результата выполнения method_missing так как если обработку отсутствия метода не кэшировать, то при каждом запросе будет происходить поиск метода, а затем обработка ошибки в method_missing. Используя .define_method мы создаем метод в момент первого срабатывания method_missing и затем, при каждом вызове этого же метода будет выполняться уже определенный нами метод, а не метод method_missing, что будет более производительно.
Пример:
require 'benchmark' class RubyDev def method_missing(name, *args, &block) if name == :hello "Hello, #{args[0]}!" yield elsif name == :hi self.class.class_eval do define_method(:hi) do |args| "Hi, #{args[0]}!" yield end end self.hi(args) else super end end end Benchmark.bm do |b| b.report("method_missing"){1000000.times { RubyDev.new.hello("Vasya") { "Bye!" }}} b.report("method_missing with define_method"){1000000.times { RubyDev.new.hi("Vasya") { "Bye!" }}} end # user system total real #method_missing 3.600000 0.010000 3.610000 ( 6.503967) #method_missing #with define_method 2.340000 0.010000 2.350000 ( 4.345760) #Производительность выше на 33.2% # user system total real #method_missing 3.520000 0.020000 3.540000 ( 7.421288) #method_missing #with define_method 2.220000 0.000000 2.220000 ( 4.114503) #Производительность выше на 44.6%
Как видите, разница в производительности при использовании .define_method составляет 30 - 45%. Если бы мы использовали в #method_missing регулярные выражения для проверки соответствия имени несуществующего метода определенному выражению, как то часто и происходит, то выигрыш от использования .define_method был бы больше.
18. Блоки кода и процедурные объекты
Очень важной и мощной частью языка Ruby являются блоки кода. Мы уже использовали их раньше, но не разбирались с тем, что это такое.
Блоки кода - это просто куски кода, которые заключены в фигурные скобки { ... } или в do ... end. Фигурные скобки следует использовать для однострочных блоков кода, а do ... end для многострочных.
Блоки кода являются аргументами для вызова методов, но не являются объектами и потому не могут быть использованы как-нибудь иначе. Пример:
3.times { |n| puts n } #1 #2 #3
Вот, приблизительно так выглядит метод #times:
class Fixnum def times t = 1 while t <= self yield(t) t += 1 end end end
yield - это зарезервированное слово, оно выполняет переданный в метод блок кода при этом может передавать в него аргументы, в нашем случае в качестве аргумента блока n передается значение t. Еще пример:
[:a, :b, :c].each { |s| puts s.to_s.upcase.to_sym } #A #B #C
Методы - итераторы в Ruby служат отличной заменой циклам когда работа происходит с некоторой коллекцией элементов.
Блоки позволяют в очень простой способ разрабатывать DSL (Domain Specific Language - язык предметной области), например сложно представить как бы выглядел RSpec если бы в Ruby не было бы блоков кода.
yield позволяет использовать только один блок кода, но, что если нам необходимо 2 блока? Мы можем обозначить аргументы - блоки в списке аргументов метода поставив перед именами аргументов знак &. Давайте перепишем пример с методом #times без использования yield:
class Fixnum def times(&block) t = 1 while t <= self block.call(t) t += 1 end end end 3.times {|n| puts n }
19. Поправки
1. Часто singleton класс называют metaclass'ом, eigenclass'ом или ghostclass'ом, но лично мое мнение заключается в том, что некоторые альтернативные названия являются неправильными, потому, что:
Metaclass'ом в ООП принято называть класс, объекты которого являются классами. То есть, фактически метаклассом можно назвать только класс Class.
2. Не все объекты могут иметь singleton класс, точнее уникальные методы (singleton методы). Например, числа и символы. Это связано, с тем, что строки "Hello" и "Hello" - два разных объекта:
"Hello".object_id #=> 80417570 "Hello".object_id #=> 80417540
А числа, например: 7 и 7, - один, ровно как и символы:
7.object_id #=> 15 7.object_id #=> 15 :RubyDev.object_id #=> 123818 :RubyDev.object_id #=> 123818
20. Рекомендации
Рекомендую вновь прибывшим читателям RubyDev обратить свое внимание на RubyDev Ruby Tutorial, там подробно описана работа с методами, классами, примитивными типами данных.
Для более практического закрепления знаний о метапрограммировании в Ruby рекомендую посмотреть на разработку Андрея Зиненко - и попробовать сделать какую-нибудь свою библиотеку с использованием техник метапрограммирования.
октября 19, 2011 at 21:26 (#)
Спасибо, но Зиненко < — здесь букво О в конце =)
И разбей статью на кусков 5-6 отдельных, читать невозможно.
октября 19, 2011 at 23:08 (#)
Сейчас исправлю фамилию.
Andrew, я просто люблю большие, основательные статьи =)
октября 20, 2011 at 07:28 (#)
ужос))я пока еще не дорос до этого.
октября 20, 2011 at 13:58 (#)
s/оффициальной/официальной
И к чему тут про синглетон? Паттернам можно целую книгу посвятить :-)
октября 20, 2011 at 17:01 (#)
weiss,
1. спасибо, ошибки исправлю
2. видно вы не читали статью если говорите о паттерне Singleton. В Ruby имеется концепция специальных скрытых классов, которые хранят методы объектов так как объекты не могут хранить собственных методов. Поскольку каждому объекту приналежит уникальный Singleton класс, т.е. Singleton класс имеет только один экземпляр, то и назвали его так же как и известный паттерн GOF.
октября 20, 2011 at 21:25 (#)
«Аксессоры бывают двух типов: set и set»
октября 20, 2011 at 21:29 (#)
«Мы передаем в attr_-максоры»
октября 20, 2011 at 22:44 (#)
Спасибо за шикарную статью :)
октября 21, 2011 at 15:10 (#)
Автор, хотелось бы увиеть на сайте хороший туториал по работе с Date, DateTime =)
октября 21, 2011 at 17:49 (#)
Пожалуйста перестань бомбить полные статьи в RSS, только заголовки плиз…
октября 22, 2011 at 00:30 (#)
i_told_ua, проводил пару месяцев назад голосование на RubyDev по поводу того, какие версии статей пускать в RSS, и большинство проголосовало за полную версию.
октября 22, 2011 at 19:12 (#)
Jarchikl, спасибо за сообщение об ошибках, чуть позже поправлю.
октября 23, 2011 at 19:15 (#)
Хорошая статья для начинающих, но название не соответствует реальному наполнению. Опечатки имеются, но обращать на них внимание в комментах, думаю, не стоит — они очевидны.
ноября 2, 2011 at 11:02 (#)
Вроде все опечатки исправил. Спасибо за сообщения об ошибках — они делают RubyDev лучше.
ноября 22, 2011 at 08:38 (#)
Я начинающий, несколько дней назад появилась необходимость быстрого освоения RoR. После просмотра Вашей статьи некоторые моменты прояснились, спасибо. Но «С нуля» — слишком опрометчиво :) — после прочтения, я как не понимал «а как же на этом пишут (и зачем)» так и не понимаю.
Ушёл грызть Ruby Tutorial.
ноября 23, 2011 at 14:12 (#)
Большое спасибо за ресурс и за статью, нашел здесь много полезного.
Нашел некоторые опечатки
13. Метапрограммировани: eval – методы
строка 31 листинга
непонятно происхождение объекта myc и его метода my_id
тоже самое насчёт метода объекта myc2
16. Контекст и контекстные блоки, глобальная область видимости
строка 26 листинга
будет выведено «string value»
ноября 23, 2011 at 18:48 (#)
Edward, спасибо сбольшое, поправил.
мая 28, 2012 at 20:48 (#)
спасибо за статью, наконец то разобралься с
.attr_reader
.attr_writer
.attr_accessor
мая 28, 2012 at 21:04 (#)
sahi, пожалуйста =)
июля 14, 2012 at 10:39 (#)
Дочитал до категоричного заявления под заголовком Классы — «Нет какого-то класса Человек, который бы создавал всех людей», и рука потянулась к перу. Возможно, его и нет, но его легко создать.
При создании экземпляра класса получаются объекты отдельной персоны человечества, которые помещаются в атрибут класса — массив. Можно сказать, что объект — это экземпляр класса, и сам класс тоже объект.
августа 3, 2012 at 04:13 (#)
Спасибо за труды. Отличная статья для начинающих. И главное сразу все, и в понятной форме. Практически справочник.
августа 31, 2012 at 12:44 (#)
class << self
можно как — то подробней обьяснить этот кусок кода (п10.)
августа 31, 2012 at 12:45 (#)
Отличная статья!
Спасибо!
сентября 12, 2012 at 20:32 (#)
при этом создание get-аксессора опционально, он будет создан только если второй аргумент — true
Думаю, Вы хотели сказать: «…создание set-аксессора опционально…»
сентября 12, 2012 at 20:49 (#)
п 11. «BasicClass» Скорее всего речь идет о BasicObject