Я и другие объекты
В моей предыдущей статье о путаницах мы видели, что методы уровня класса определены следующим образом:
class MyClass def self.hello "hello" end end MyClass.hello # => "hello"
Разработчики из других языков могут испытывать искушение предположить, что def self.method_name
является своего рода специальным синтаксисом для создания методов класса. Однако это также возможно:
class MyClass def MyClass.hello "hello" end end MyClass.hello # => "hello"
И что еще важнее , это также возможно:
MyClass = Class.new def MyClass.hello "hello" end MyClass.hello # => "hello"
Итак, вы можете видеть, что Ruby позволяет вам определять методы для классов. Но классы — это просто объекты класса Class
, поэтому логически вы должны иметь возможность определять методы для других типов объектов, таких как строка.
a_string = "blerp" def a_string.hello "hello" end a_string.hello # => "hello"
Как выясняется, def self.whatever
не является специальным синтаксисом. Ruby позволяет вам определять методы для объектов , включая классы, и для некоторого объекта с именем self
.
Но что такое self
?
Для любой строки кода Ruby есть ссылка, называемая self
. self
всегда ссылается на текущий объект в этой точке кода, что важно, потому что многие другие аспекты Ruby полагаются на него. Например, переменные экземпляра — это просто переменные, хранящиеся в self
.
# At the top level, self is a special object called "main" self # => main self.class # => Object # class definitions change the value of self class SelfClass self # => SelfClass # Will be called on instances def self self end # Same as SelfClass.self def self.self self end end SelfClass.self # => SelfClass SelfClass.new.self # => Instance of SelfClass
Так кого это волнует , верно? Ну, тот факт, что self
всегда является чем-то, имеет некоторые интересные последствия:
- Переменные экземпляра могут быть назначены и разрешены где угодно (хотя это может не иметь смысла семантически, это всегда возможно синтаксически)
- Методы могут быть определены где угодно
Это заметно отличается от других языков, где существуют строгие правила относительно того, где могут происходить определенные вещи.
Ленивая инициализация
Разработчики, привыкшие к языкам, требующим объявления переменных экземпляра в классах, могут испытывать искушение «определить» переменные экземпляра в конструкторах следующим образом:
class Item attr_accessor :title def initialize @title = "" end end
Есть некоторые проблемы, которые возникают при выполнении таких действий:
- Подклассы могут беспокоиться о вызове
super
. - Объекты распределяются независимо от того, используются ли они.
- Невозможно сказать, что получит получатель, просто взглянув на него.
Часть 1 показала, что неназначенная переменная экземпляра возвращает nil
вместо того, чтобы выдавать ошибку. Это можно комбинировать с оператором ||=
для установки значений по умолчанию без конструктора. Он выполняет присваивание только в том случае, если левый операнд ложный , то есть nil
или false
.
class Item attr_writer :title def title @title ||= "" end end
Теперь очевидно, что когда экземпляр Item
получает сообщение #title
, он либо возвращает пустую строку, либо последний назначенный заголовок. Еще лучше, если нам все равно, каков заголовок элемента, память для него никогда не будет выделена.
Оператор разрешения области (: 🙂
Проблема может возникнуть, когда в вашем коде есть модуль или класс, имя которого совпадает с именем основного модуля Ruby, который вы хотите использовать.
module MyModule class File end class Thing def exist?(path) File.exist?(path) end end end thing = MyModule::Thing.new thing.exist?('/etc/hosts') # => udefined method `exist?' for MyModule::File:Class
Самый простой способ исправить это — добавить оператор ::
в начало константы столкновения.
module MyModule class File end class Thing def exist?(path) ::File.exist?(path) end end end thing = MyModule::Thing.new thing.exist?('/etc/hosts') # => true
Все время выполнения
Исторически языки делали различие между тем, что объявляется, и тем, что запускается. Например, в C ++ классы объявляются во время компиляции, а экземпляры создаются и используются во время выполнения. Это может быть неприятно, поскольку код времени компиляции может быть ограничен отсутствием доступа к информации.
В мире Руби все время выполнения.
class Hello puts "Hello World" end
В этом примере печатается строка, потому что «объявления» класса Ruby — это просто код, который запускается, как и везде. Это открывает окно во все виды возможностей — не в последнюю очередь это написание кода, который пишет код . Нет необходимости иметь специальный плагин-контейнер для вашего приложения, когда код может изменить само приложение.
Например, иногда может быть полезно определить методы динамически:
print "What noise does the animal make?: " speak_noise = gets Animal = Class.new do define_method :speak do puts "Animal: #{speak_noise}" end end Animal.new.speak
Также возможно наследовать динамически (в основном для впечатления друзей, но кто знает?)
class RandomClass < [Array, Hash].sample end RandomClass.superclass # => either Array or Hash
Внутренние DSL
Специфичные для домена языки позволяют разработчикам писать элегантный код, адаптированный для конкретных ситуаций. Примеры DSL включают RSpec, Cucumber, Prawn и Shoes .
DSLs бывают двух видов:
- внутренние — основаны на подмножестве языка хоста
- внешний — используйте полноценный парсер
Поскольку Ruby является гибким языком без синтаксического шума, он хорошо подходит для разработки внутренних DSL. Это DSL, которые на самом деле просто Ruby, но предназначены для конкретного домена.
Итак, как можно создать одну из этих вещей?
В первой части было показано, что модули Ruby являются пакетами для методов, которые можно смешивать в классы. Если вы проверите #ancestors
класса, вы увидите все модули, которые были включены.
Глядя на предков Object
, вы заметите странный модуль:
Object.ancestors #=> [Object, PP::ObjectMixin, Kernel, BasicObject]
Каждый объект в Ruby поставляется с включенным модулем Kernel
. Kernel
содержит методы, такие как #puts
и #lambda
. Одним из способов создания DSL в Ruby является добавление методов в ядро, чтобы они работали на верхнем уровне.
module Kernel def task(name, &block) tasks << block end def tasks @tasks || @tasks = [] end def run_task tasks.each { |t| t.call } end end
Теперь в файле, использующем наш DSL, мы можем запустить:
task :hello do puts "Hello world" end task :goodbye do puts "Later world" end run_tasks
Конечно, run_tasks
излишни и, вероятно, будут вызываться из-под капота в реальной DSL.
instance_eval
и class_eval
DSL были бы довольно ограничены, если бы определение методов в Kernel
было единственным способом их создания. К счастью, Ruby предоставляет #instance_eval
. Когда предоставляется блок или строка, содержимое будет оцениваться в контексте экземпляра.
a_string = "word" a_string.instance_eval("reverse") # => "drow"
Давайте сделаем более переносимую версию нашей задачи под управлением DSL. При использовании #instance_eval
таким образом, определенные методы формируют ключевые слова DSL. Мы просто сделаем одно ключевое слово DSL: task
.
module TaskList # Make every instance method a class method # Shorter than putting "self" in front of every method extend self def run(&block) instance_eval(&block) process_tasks end private def task(name, &block) # Leaving the "&" off converts it to a storeable proc tasks << block end def tasks @tasks || @tasks = [] end def process_tasks tasks.each { |t| t.call } end end TaskList.run do task :hello do puts "Hello" end task :goodbye do puts "Goodbye" end end
Эта версия использует #instance_eval
в модуле. Пусть вас это не смущает — модули действительно являются экземплярами класса Class
. Вот пример в типичном смысле версии (т.е. экземпляр класса, который не является Class
):
class TaskList def run(&block) instance_eval(&block) process_tasks end private def task(name, &block) tasks << block end def tasks @tasks || @tasks = [] end def process_tasks tasks.each { |t| t.call } end end TaskList.new.run do task :hello do puts "Hello" end . . . end
В отличие от #instance_eval
, #module_eval
/ #class_eval
работает только для модулей и классов, соответственно. Метод экземпляра, определенный в блоке #class_eval
будет определен для всех объектов этого класса. Это может быть полезно для основных классов по исправлению обезьян.
String.class_eval do def spaceify self.split("").join(" ") end end "hello".spaceify # => "hello"
Конфиденциальность
Допустим, у вас есть класс с некоторыми переменными экземпляра и закрытый метод, подобный этому:
class Secret def initialize @hidden = 123 end private def hidden_method "You can't call me!" end end
Поскольку переменные экземпляра нуждаются в методах для манипулирования извне класса, они являются закрытыми , верно? Что ж…
secret = Secret.new secret.instance_eval { @hidden } # => 123 secret.instance_eval { hidden_method } # => "You can't call me!"
Хорошо, но даже с #instance_eval
кому-то нужно знать имена переменных и методов экземпляра, верно?
secret.instance_variables # => [:@hidden] secret.private_methods # => [:initialize, :hidden_method]
Довольно легко заглянуть внутрь объекта Ruby и увидеть, чем он владеет и что он может делать. Это известно как отражение .
secret.hidden_method # => NoMethodError: private method `hidden_method'...
Вызов частных методов для объекта не будет работать. Но что, если мы отправим сообщение объекту ?
secret.send(:hidden_method) # => "This method is private - you can't call me!"
То, как частные методы действительно работают в Ruby, интересно своей простотой. Частные методы — это просто методы, которые нельзя вызвать с явным получателем . Это означает, что частные методы нельзя вызывать с явным получателем, даже если явный получатель является self
.
class Secret def initialize self.private_method end private def private_method "You can't call me!" end end secret = Secret.new # => private method `private_method' called for...
Таким образом, инкапсуляция в Ruby в значительной степени работает в системе чести. Здесь нет настоящей конфиденциальности, только псевдо-конфиденциальность. Означает ли это, что можно просто нарушить инкапсуляцию? Ну нет. Если разработчик сделал что-то личное, это было, вероятно, по уважительной причине. Тем не менее, вы часто будете видеть это, и хорошо знать, почему разработчик может отправлять сообщение объекту, а не просто вызывать метод.
Мрачные Методы
Многие языки программирования полагаются на тесную связь между тем, что такое класс и что он может делать. В Ruby эти отношения разрушаются. Вместо того, чтобы заботиться о происхождении объекта, важнее подумать о том, какие сообщения он может получать.
В Ruby, если объект знает, как обрабатывать сообщение, которое мы отправляем, то это, вероятно, правильный тип объекта. Этот способ мышления обычно называют типизацией утки .
Например, это не будет хорошим кодом Ruby:
if user.is_a? EmailUser "Email:" #{user.email}" end
Хотя такие проверки типов могут повысить надежность нашего кода, они полностью уничтожают преимущества развязки при утке. Вместо этого лучше спросить, может ли класс отвечать на сообщения, которые вы #send
.
if user.respond_to?(:email) "Email: #{user.email}" end
Пока что ничего странного. Тем не менее, это то место, где все меняется. Поскольку объекты получают сообщения для вызова методов, можно вмешиваться при получении сообщения. Это позволяет вызывать методы, которые даже не существуют . Такая возможность позволяет разрабатывать гибкие классы, которые могут включать новые функции без изменения одной строки кода (например, клиент веб-API).
Чтобы увидеть, как мы можем это сделать, давайте посмотрим, какие шаги выполняет Ruby, когда объект получает сообщение:
- Метод определен в классе получателя или где-нибудь в его предках?
-
#method_missing
в классе получателя или где-либо в его предках? - Raise
NoMethodError
Итак, для обработки неопределенных методов просто определите #method_missing
.
class MethodHandler def method_missing(method_name, *args) puts "Handling instance method #{method_name}" puts "The arguments were: #{args.join(', ')}" end end handler = MethodHandler.new handler.iammadeup!(1, 2, "cat")
Обычным шаблоном является проверка имени метода по регулярному выражению, а в противном случае NoMethodError
, вызвав super
(если родительский класс не определил method_missing
). Обратите внимание, что имя метода является символом, а не строкой. Об этом важно помнить при создании регулярных выражений, поскольку символы начинаются с двоеточия. Некоторые разработчики предпочитают называть его method_sym
для большей method_sym
.
К сожалению, даже если #method_missing
отвечает на метод, это не значит, что #respond_to?
или # #method
будет. Поэтому важно всегда определять #respond_to_missing
при определении #method_missing
.
У Thoughtbot есть хороший пост на эту тему здесь .
Синглтон Классы / Собственные классы
Трудно объяснить, что будет дальше. Я думаю, что это лучше всего представить с помощью пары утверждений:
- Методы действительно определены для классов, а не для объектов.
- Любой объект Ruby может иметь свои собственные методы.
Звучит противоречиво? Давайте докажем второе утверждение с некоторым кодом.
word = "Hello" def word.double self * 2 end puts word.double # => "HelloHello"
Оба эти утверждения верны для Ruby. И все же — как? Если методы живут внутри классов, как у объекта могут быть методы, которые не определены для его класса или его предков? Там должно быть … другой класс. Где-то должен быть скрытый класс, в котором живут такие методы.
puts word.method(:length).owner # => "String" puts word.method(:double).owner # => "#<Class:#<String:0x00000001bf4680>>"
И это действительно так.
Лучше не привязываться к какому-либо конкретному имени для этого скрытого класса, так как несколько лет использовались несколько раз: метакласс, собственный класс, класс-призрак, класс-одиночка, анонимный класс. Официально, Ruby принял #singleton_class
в 1.9.2. Обратите внимание, что слово «синглтон» не используется в смысле синглтон-паттерна. Поскольку это может сбивать с толку, в этой статье будет использоваться термин «собственный класс».
Канонический способ определить вещи в собственном классе объекта заключается в использовании
class << [some object]
нотации.
letters = ['a', 'b', 'c'] class << letters def capitalize self.map do |letter| letter.upcase end end end letters.capitalize #=> ['A', 'B', 'C']
Ранее мы видели, что методы закрытого класса не настолько приватны. Это можно исправить, определив методы на собственном классе.
class Secretive class << self private def hidden "This method is private - You can't call me!" end end end Secretive.hidden #=> private method `hidden' called for Secretive:Class (NoMethodError)
Создание attr_accessor-подобных методов
В attr_reader
части мы увидели, как Ruby позволяет определять методы получения и установки с помощью вспомогательных методов attr_reader
, attr_writer
и attr_accessor
.
class Foo attr_accessor :bar, :baz end
Что если мы захотим создать свои собственные вспомогательные методы, подобные этим? Имеет смысл определить метод уровня класса, поскольку self
— это класс внутри определения класса.
class Foo def self.attr_printer(*properties) puts "attr_printer called with #{properties}" end attr_printer :bar, :baz end
Это работает, но определение метода внутри каждого класса, который использует attr_printer
побеждает цель. Нам нужно найти другое место для его определения, которое будет применяться ко всем классам.
class Class def attr_printer(*properties) puts "attr_printer called with #{properties}" end end class Something attr_printer :attr_one, :attr_two, :attr_three end
Теперь у всех классов есть новый метод, но мы можем и этого не захотеть. Вместо этого, вероятно, лучше поместить код в модуль, который можно включать в классы индивидуально, особенно если существуют различные реализации вспомогательного метода. Вот шанс для нас снова использовать собственные классы.
module AttrPrinterable def self.included(klass) class << klass def attr_printer(*properties) puts "attr_printer called with #{properties}" end end end end class Something include AttrPrinterable attr_printer :attr_one, :attr_two, :attr_three end
Кроме того, вы можете использовать #instance_eval
в экземпляре Class
. Обратите внимание, что используется def attr_printer
а не def self.attr_printer
.
module AttrPrinterable def self.included(klass) klass.instance_eval do def attr_printer(*properties) puts "attr_printer called with #{properties}" end end end end
Резюме
-
self
всегда текущий объект. - Вместо того, чтобы объявлять переменные экземпляра в конструкторах, как в статических языках, иногда лучше использовать ленивые методы инициализации.
-
#instance_eval
запускает блок или строку в контексте экземпляра, включая экземплярыClass
- Используйте
#class_eval
для определения методов для всех экземпляров класса. - В Ruby нет никакой разницы между кодом компиляции и временем выполнения.
- В Ruby константа — это любое слово, которое начинается с заглавной буквы.
- Константы в основном используются для имен классов и модулей, а не перечисления, как в других языках.
- Оператор разрешения области
::
используется для указания констант - Поставьте
::
перед константой, чтобы указать константу корневого уровня. - Частные методы — это методы, которые не могут иметь явного получателя, включая
self
. - Объекты получают сообщения для вызова методов.
-
.
оператор является сокращением для#send
для отправки сообщения объекту -
#method_missing
может вмешаться, когда объект не знает, как ответить на сообщение. - Важно определить
#respond_to_missing
при определении#method_missing
. - Собственный класс или одноэлементный класс — это уникальный класс, с которым связан объект.
- Синтаксис
class <<
можно использовать для открытия собственного класса объекта, включая объект класса. - Методы класса не могут быть закрытыми, но методы собственного класса, определенные для классов, могут.
- Такие методы, как
attr_accessor
могут быть определены как методы уровня класса классов или методы уровня экземпляраClass
.