Статьи

Общие Подводные камни для Новых Рубиистов, Часть I

Подлость красного человечка

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

Будет предпринята попытка придерживаться нотации RDoc для описания методов:

  • Array#length относится к length метода экземпляра класса Array .
  • Array::new ссылается на метод класса new для класса Array .
  • Math::PI относится к константе PI в модуле Math .

Ruby 1.9+ предполагается.

Переменные экземпляра и переменные класса

В большинстве языков программирования переменные экземпляра должны быть объявлены до того, как они могут быть назначены. Ruby — диаметральная противоположность — переменные экземпляра не могут быть объявлены вообще. Вместо этого переменная экземпляра в Ruby появляется при первом назначении .

Создать переменную экземпляра так же просто, как взять локальную переменную и нажать «@» в начале.

 class Item def initialize(title) @title = title end end item = Item.new('Chia Ruby') 

Под капотом переменная экземпляра — это просто переменная, хранящаяся в self — текущий экземпляр. Может быть заманчиво предположить, что переменные экземпляра могут быть назначены с помощью self.foo = как в Python. Фактически, self.foo = отправит сообщение foo= self , а Ruby попытается вызвать соответствующий метод. Это работает, только если метод существует.

 class Item def initialize(title) self.title = title end end item = Item.new('Chia Ruby') # => undefined method `title='... 

Чтобы получить доступ к переменным экземпляра объекта вне определений методов экземпляра, необходимо определить методы getter и setter.

 class Item def title=(t) @title = t end def title @title end end item = Item.new puts item.title.inspect # => nil item.title = "Chia Ruby" puts item.title # => "Chia Ruby" 

Конструктор #initialize был оставлен здесь, чтобы показать, что он не является обязательным. Также обратите внимание, что доступ к переменной экземпляра, которая не была назначена, не вызывает ошибку . @title равен nil когда к нему обращаются до назначения. Часть II покажет, как это можно использовать для ленивой инициализации.

Определение геттеров и сеттеров снова и снова быстро устареет. К счастью, Ruby предоставляет три вспомогательных метода:

  • #attr_reader — определить получатели уровня экземпляра
  • #attr_writer — определить установщики уровня экземпляра
  • #attr_accessor — определить оба

Как правило, эти помощники идут в начале определения класса.

 class Thing attr_accessor :foo, :bar attr_reader :baz def initialize @baz = "cat" end end thing = Thing.new thing.foo = 1 thing.bar = 2 puts thing.baz # => "cat" 

Следствием того факта, что переменные экземпляра являются просто переменными, определенными в self является то, что переменные экземпляра также работают на уровне класса . В конце концов, классы — это просто экземпляры Class . Они обычно называются переменными экземпляра класса .

 class Parent @value = 1 def self.value @value end end class Child < Parent @value = 2 end puts Parent.value # => 1 puts Child.value # => 2 

В дополнение к переменным экземпляра в Ruby также есть так называемые «переменные класса», использующие @@ вместо @ . К сожалению, они осуждаются из-за того, что они заменяют ценности своих потомков / предков. По этой причине лучше рассматривать их как переменные иерархии классов .

 class Parent @@value = 1 def self.value @@value end end class Child < Parent @@value = 2 end puts Parent.value #=> 2 (because Child overwrote @@value) 

На самом деле, это намного хуже, чем это. Подумайте о том, что происходит, когда переменная иерархии классов назначается на верхнем уровне, где self является main — экземпляр Object .

 @@value = 3 puts Parent.value # => 3 

Поскольку в основном каждый объект Ruby происходит от Object , он будет иметь доступ к тем же переменным иерархии классов, что и Object . Итак, переменные класса являются потенциально глобальными переменными . Это делает их крайне непредсказуемыми и склонными к неправильному использованию.

Модули

В Ruby модуль является контейнером для методов. С другой стороны, класс — это особый вид модуля, который может создавать экземпляры и иметь предков.

 module MyModule def hello puts "hello from instance" end end class MyClass def hello puts "hello from instance" end end instance = MyClass.new instance.hello # => "hello from instance" 

Методы в модуле могут быть методами экземпляра или методами класса — по крайней мере, семантически. Методы класса Ruby — это просто методы экземпляров объектов класса . Это немного нелогично, но модули являются экземплярами класса Module … хотя классы являются видами модулей.

 module MyModule # Instance method defined on MyModule which is an instance of Module def MyModule.hello puts 'hello from module' end # Same as "def MyModule.hello" because self is MyModule here def self.hello puts 'hello from module' end # Instance method for an instance of a class mixing-in MyModule def hello puts 'hello from instance' end end MyModule.hello # => "hello from module" 

Тот факт, что модули не имеют фабрики экземпляров, не означает, что они не могут иметь данные. Поскольку они являются экземплярами Module , они могут иметь переменные экземпляра.

 module Fooable def self.foo=(value) @foo = value end def self.foo @foo end end Fooable.foo = "baz" puts Fooable.foo # => "baz" 

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

Примеси

Есть два способа смешать модуль в класс:

  1. #include — добавить методы модуля в качестве методов экземпляра
  2. #extend — добавить методы модуля в качестве методов класса

Примеры:

 module Helloable def hello puts "Hello World" end end class IncludeClass include Helloable end class ExtendClass extend Helloable end IncludeClass.new.hello ExtendClass.hello 

Иногда желательно смешивать методы класса и экземпляра в классах. Очевидный, если избыточный, способ сделать это — поместить методы класса в один модуль, а методы экземпляра — в другой. Затем модуль экземпляра может быть включен, а затем модуль класса может быть расширен.

Вместо этого лучше использовать магию метапрограммирования. Метод подключаемого Module#included определяет, когда модуль был включен в класс. Когда модуль включен, класс может расширять внутренний модуль (часто называемый ClassMethods ), который содержит методы уровня класса.

 module Helloable # Gets called on 'include Helloable' def self.included(klass) # 'base' often used instead of 'klass' klass.extend(ClassMethods) end # Method that will become an instance method def hello puts "hello from instance" end # Methods that will become class methods module ClassMethods def hello puts "hello from class" end end end class HelloClass include Helloable end HelloClass.hello # => "hello from class" HelloClass.new.hello # => "hello from instance" методы module Helloable # Gets called on 'include Helloable' def self.included(klass) # 'base' often used instead of 'klass' klass.extend(ClassMethods) end # Method that will become an instance method def hello puts "hello from instance" end # Methods that will become class methods module ClassMethods def hello puts "hello from class" end end end class HelloClass include Helloable end HelloClass.hello # => "hello from class" HelloClass.new.hello # => "hello from instance" 

Быстрый способ продемонстрировать полезность миксинов — это модуль Comparable . Он определяет методы оператора сравнения на основе возвращаемого значения метода объединенного оператора сравнения <=> . Давайте создадим класс StringFraction который обеспечивает правильное сравнение между фракциями в строках.

 class StringFraction include Comparable def initialize(fraction) @fraction = fraction end def rational @fraction.to_r end def <=>(other) self.rational <=> other.rational end end f1 = StringFraction.new("1/2") f2 = StringFraction.new("1/3") puts f1 > f2 # => true 

Модуль Gotchas

1 Если модуль включен дважды, второе включение игнорируется.

 module HelloModule def say "hello from module" end end module GoodbyeModule def say "goodbye from module" end end class MyClass include HelloModule include GoodbyeModule include HelloModule end MyClass.new.say # => "goodbye from module" 

2 Если два модуля определяют один и тот же метод, используется второй для включения.

 module Module1 def hello "hello from module 1" end end module Module2 def hello "hello from module 2" end end class HelloClass include Module1 include Module2 end HelloClass.new.hello # => "hello from module 2" 

3 Методы модуля не могут заменить методы, уже определенные в классе.

 module HelloModule def hello 'hello from module' end end class HelloClass def hello 'hello from class' end include HelloModule end HelloClass.new.hello # => 'hello from class' 

Символы

В других языках (и, возможно, в Ruby) вы могли видеть константы, используемые в качестве имен:

 NORTH = 0 SOUTH = 1 EAST = 2 WEST = 3 

За пределами Ruby константы, используемые в качестве имен, а не для их значений, называются перечислителями (не путать с Enumerator Ruby). Как правило, есть более чистый способ сделать это, как в перечисляемом типе (здесь, в Java):

 public enum Direction { NORTH, SOUTH, EAST, WEST } 

В Ruby есть что-то даже лучше перечисленных типов: символы . Символ — это любая ссылка, которая начинается с : включая такие слова, как :name :@foo или :+ . Эта гибкость важна, потому что символы используются для представления таких вещей, как имена методов, переменные экземпляров и константы.

С практической точки зрения символы, подобные счетчикам, в основном являются быстрыми неизменяемыми строками. Однако, в отличие от перечислителей, символы не нужно создавать вручную . Если вам нужен символ для существования, просто используйте его, как будто он уже существует. Вот как вы можете создать хеш, который имеет несколько символов в качестве ключей:

 worker = { :name => "John Doe", :age => "35", :job => "Basically Programmer" } puts worker[:name] # => "John Doe" 

Потенциальная ловушка — динамическое создание символов, особенно на основе пользовательского ввода. Это не очень хорошая идея, потому что символы не являются мусором . После создания они существуют до выхода из программы. Это становится проблемой безопасности, когда пользователи косвенно отвечают за создание символов. Злоумышленник может потреблять много памяти, которая никогда не будет собираться мусором, что может привести к сбою виртуальной машины.

Блоки, процы и лямбды

За вызовами методов Ruby могут следовать пары токенов { / } или do / end содержащие произвольный код. Этот произвольный код может принимать заключенные в канал (например, | i, j |) аргументы, на которые ссылается код.

 [1,2,3].each { |i| print i } # => '123' 

Код между этими токенами называется блоком. Блок — это кусок кода, прикрепленный к вызову метода . Здесь для элемента #each в массиве выполняется блок. В этом случае он просто печатает элемент на стандартный вывод.

Что нового для рубинистов неочевидно, так это то, как каждый элемент в массиве попадает внутрь блока. Отличный способ понять это — написать собственный #each .

 class Colors def self.each yield 'Red' yield 'Green' yield 'Blue' end end Colors.each { |color| print color } # => 'RedGreenBlue' 

Ключевое слово yield выполняет блок с переданными ему аргументами. Другими словами, аргументы в блоке (например: | i |) происходят из вызовов yield для метода, к которому присоединен блок . Если вы хотите перебрать коллекцию, вам нужно просто yield каждый элемент в ней.

Но что происходит, когда метод вызывается без блока?

 Colors.each #=> ...no block given (yield) (LocalJumpError)... 

Оказывается, yield будет пытаться выполнить блок независимо от того, существует ли он на самом деле. Если нам нужен гибкий метод, который уступает блоку только при его наличии, Ruby предоставляет #block_given? ,

 class Colors @colors = ['Red', 'Green', 'Blue'] def self.each if block_given? # send each color to the block if there is one # #each returns the array @colors.each { |color| yield color } else # otherwise just return the array @colors end end end 

Хранение блоков

Для кого-то, пришедшего из языка, подобного JavaScript, блоки немного похожи на анонимные функции, определенные в вызовах функций (с оговоркой, что на вызов может быть только одна). JavaScriptists также будут использоваться для того факта, что функции JavaScript могут храниться в переменных и передаваться другим функциям. Они известны как первоклассные функции . Строго говоря, Ruby не имеет первоклассных функций , но его блоки могут храниться в вызываемых объектах.

Ruby предоставляет два контейнера для хранения блоков: procs и lambdas . Эти контейнеры могут быть созданы несколькими способами:

 time_proc = proc { Time.now } time_lambda = lambda { Time.now } # The popular, Ruby 1.8 compatible way to create a proc old_proc = Proc.new { |foo| foo.upcase } # Shorthand to create a lambda stabby_lambda = ->(a, b) do a + b end # Turning a block passed to a method into a Proc def a_method(&block) block end 

Все вызываемые объекты выполняются с помощью #call .

 add = lambda { |a, b| a + b } puts add.call(1,2) # => 3 

Procs и lambdas на самом деле являются объектами Proc . Однако есть как минимум два нюанса:

  1. Они обрабатывают ключевое слово return разному. Явный возврат в proc возвращается из контекста, в котором был определен proc . Явный возврат в лямбде просто возвращает из лямбды.
  2. Лямбды проверяют количество аргументов. Procs назначает nil отсутствующим аргументам

Первый пункт заслуживает дополнительного внимания, поскольку он может привести к непрозрачным ошибкам.

 return_proc = proc { return } return_lambda = lambda { return } def first(p) second(p) end def second(p) p.call end first(return_lambda) first(return_proc) # => LocalJumpError 

Лямбда выполняется без проблем. Однако, когда мы пытаемся вызвать proc, мы получаем LocalJumpError . Это потому, что процедура была определена на верхнем уровне. Самый простой способ обойти проблему возврата proc / block — это избежать явных возвратов . Вместо этого воспользуйтесь неявными возвратами Руби.

Примечание. В Ruby 1.8 proc создал procs, который проверял количество аргументов, таких как lambda, а Proc.new создал так называемые procs. В Ruby 1.9+ было исправлено поведение proc идентичное Proc.new . Помните об этом при использовании кода, написанного для 1.8.

Область локальной переменной

У Ruby есть два локальных барьера области видимости — точки, где локальные переменные и аргументы не могут пройти

  1. Определения модулей
  2. Определения методов

Так как классы являются модулями, нужно искать три ключевых слова: module , class и def .

 lvar = 'x' def print_local puts lvar end print_local #=> NameError: undefined local variable or method... 

Итак, как вы обходите границы барьеров? Оказывается, что блоки наследуют область, в которой они определены . Мы можем воспользоваться этим, чтобы передать локальные переменные за эти барьеры, используя альтернативы, которые принимают блоки.

 lvar = 'x' MyClass = Class.new do define_method :print_local do puts lvar end end MyClass.new.print_local #=> 'x' 

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

Резюме

  • Переменные экземпляра являются ссылками с префиксом «@» и принадлежат self — текущему экземпляру.
  • Переменные экземпляра могут быть созданы где угодно, включая модули.
  • Значение неназначенных переменных экземпляра равно nil
  • В Ruby также есть переменные класса , но они являются почти глобальными переменными и не должны использоваться.
  • Модули являются контейнерами для методов.
  • Классы — это модули, которые также являются фабриками экземпляров и имеют предков.
  • Ruby не имеет множественного наследования, но несколько модулей могут быть «смешаны» с классами.
  • Все методы являются методами экземпляра в Ruby. Методы класса являются методами экземпляра на объектах класса.
  • Блок — это фрагмент кода, прикрепленный к вызову метода, который наследует область, в которой он определен.
  • Аргументы в блоке (например, | i |) происходят из вызовов метода yield внутри метода.
  • Блоки могут храниться в процедурах или лямбдах .
  • Проц — это переносные блоки. Лямбды похожи на портативные методы.
  • Символы являются ответом Ruby на перечисляемые типы.
  • Символы не являются сборщиком мусора, поэтому они могут быть проблемой безопасности при динамическом генерировании.
  • Локальные переменные не могут пересекать границы видимости определений модулей и методов.
  • Блоки могут использоваться для переноса локальных переменных за пределы области видимости.

Далее, во второй части мы рассмотрим больше внутренней работы языка Ruby.