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 рекомендую посмотреть на разработку Андрея Зиненко -   и попробовать сделать какую-нибудь свою библиотеку с использованием техник метапрограммирования.

Tags: , ,

Responses

  1. Andrew says:

    октября 19, 2011 at 21:26 (#)

    Спасибо, но Зиненко < — здесь букво О в конце =)

    И разбей статью на кусков 5-6 отдельных, читать невозможно.

  2. admin says:

    октября 19, 2011 at 23:08 (#)

    Сейчас исправлю фамилию.
    Andrew, я просто люблю большие, основательные статьи =)

  3. tankard says:

    октября 20, 2011 at 07:28 (#)

    ужос))я пока еще не дорос до этого.

  4. weiss says:

    октября 20, 2011 at 13:58 (#)

    s/оффициальной/официальной
    И к чему тут про синглетон? Паттернам можно целую книгу посвятить :-)

  5. admin says:

    октября 20, 2011 at 17:01 (#)

    weiss,
    1. спасибо, ошибки исправлю
    2. видно вы не читали статью если говорите о паттерне Singleton. В Ruby имеется концепция специальных скрытых классов, которые хранят методы объектов так как объекты не могут хранить собственных методов. Поскольку каждому объекту приналежит уникальный Singleton класс, т.е. Singleton класс имеет только один экземпляр, то и назвали его так же как и известный паттерн GOF.

  6. Jarchikl says:

    октября 20, 2011 at 21:25 (#)

    «Аксессоры бывают двух типов: set и set»

  7. Jarchikl says:

    октября 20, 2011 at 21:29 (#)

    «Мы передаем в attr_-максоры»

  8. says:

    октября 20, 2011 at 22:44 (#)

    Спасибо за шикарную статью :)

  9. Pavel says:

    октября 21, 2011 at 15:10 (#)

    Автор, хотелось бы увиеть на сайте хороший туториал по работе с Date, DateTime =)

  10. says:

    октября 21, 2011 at 17:49 (#)

    Пожалуйста перестань бомбить полные статьи в RSS, только заголовки плиз…

  11. admin says:

    октября 22, 2011 at 00:30 (#)

    i_told_ua, проводил пару месяцев назад голосование на RubyDev по поводу того, какие версии статей пускать в RSS, и большинство проголосовало за полную версию.

  12. admin says:

    октября 22, 2011 at 19:12 (#)

    Jarchikl, спасибо за сообщение об ошибках, чуть позже поправлю.

  13. Victor says:

    октября 23, 2011 at 19:15 (#)

    Хорошая статья для начинающих, но название не соответствует реальному наполнению. Опечатки имеются, но обращать на них внимание в комментах, думаю, не стоит — они очевидны.

  14. admin says:

    ноября 2, 2011 at 11:02 (#)

    Вроде все опечатки исправил. Спасибо за сообщения об ошибках — они делают RubyDev лучше.

  15. Алексей Абрамов says:

    ноября 22, 2011 at 08:38 (#)

    Я начинающий, несколько дней назад появилась необходимость быстрого освоения RoR. После просмотра Вашей статьи некоторые моменты прояснились, спасибо. Но «С нуля» — слишком опрометчиво :) — после прочтения, я как не понимал «а как же на этом пишут (и зачем)» так и не понимаю.
    Ушёл грызть Ruby Tutorial.

  16. Edward says:

    ноября 23, 2011 at 14:12 (#)

    Большое спасибо за ресурс и за статью, нашел здесь много полезного.
    Нашел некоторые опечатки
    13. Метапрограммировани: eval – методы
    строка 31 листинга

    myc.my_id # 72703890

    непонятно происхождение объекта myc и его метода my_id
    тоже самое насчёт метода объекта myc2

    16. Контекст и контекстные блоки, глобальная область видимости
    строка 26 листинга

    puts obj.myclass #=> "some value"

    будет выведено «string value»

  17. admin says:

    ноября 23, 2011 at 18:48 (#)

    Edward, спасибо сбольшое, поправил.

  18. sahi says:

    мая 28, 2012 at 20:48 (#)

    спасибо за статью, наконец то разобралься с
    .attr_reader
    .attr_writer
    .attr_accessor

  19. admin says:

    мая 28, 2012 at 21:04 (#)

    sahi, пожалуйста =)

  20. Publius says:

    июля 14, 2012 at 10:39 (#)

    Дочитал до категоричного заявления под заголовком Классы — «Нет какого-то класса Человек, который бы создавал всех людей», и рука потянулась к перу. Возможно, его и нет, но его легко создать.

    #!/usr/bin/ruby -w
    # coding: utf-8
    
    # Класс Humanity
    # Для описания всего человечества
    
    class Humanity
      @@humanity = []
      def initialize name
        @name = name
        add self
      end
      def self.showall
        out = @@humanity.inspect
        out.sub!(/\[/, '')
        out.sub!(/\]/, '')
        out = out.split ', '
        puts out
      end
    private
      def add person
        @@humanity.push person
      end
    end
    
    Humanity.new 'Ada Lovelace'
    Humanity.new 'Ivan Ivanovich'
    Humanity.new 'Petr Petrovich'
    Humanity.new 'Sidor Sidorovich'
    
    Humanity.showall
    

    При создании экземпляра класса получаются объекты отдельной персоны человечества, которые помещаются в атрибут класса — массив. Можно сказать, что объект — это экземпляр класса, и сам класс тоже объект.

  21. berolak says:

    августа 3, 2012 at 04:13 (#)

    Спасибо за труды. Отличная статья для начинающих. И главное сразу все, и в понятной форме. Практически справочник.

  22. Alex says:

    августа 31, 2012 at 12:44 (#)

    class << self
    можно как — то подробней обьяснить этот кусок кода (п10.)

  23. Alex says:

    августа 31, 2012 at 12:45 (#)

    Отличная статья!
    Спасибо!

  24. Wiseland says:

    сентября 12, 2012 at 20:32 (#)

    при этом создание get-аксессора опционально, он будет создан только если второй аргумент — true

    Думаю, Вы хотели сказать: «…создание set-аксессора опционально…»

  25. Wiseland says:

    сентября 12, 2012 at 20:49 (#)

    п 11. «BasicClass» Скорее всего речь идет о BasicObject

Leave a Response

Для подсветки кода используйте BB - коды: [language]...[/language], где language может быть: ruby, javascript, css, html.