Шесть нетривиальных возможностей Ruby

Узнайте о возможностях Ruby, которые могут поставить в тупик разработчиков C++

Предположим, что вы – разработчик на C++, и вам необходимо быстро создать на Ruby какой-нибудь прототип. Когда вы берете справочник по Ruby наподобие Pickaxe или просматриваете Web-сайт Ruby, вы видите знакомые конструкции, такие как объявления классов, поддержка потоков или обработка прерываний. И вот когда вы думаете, что знаете, как работает Ruby, то осознаете, что параллелизм в коде Ruby работает совсем не так, как потоки Boost, операторы catch и throw – совсем не то, чем они кажутся, а кто-то повсюду использует в своих Ruby-сценариях нечто под названием self. Добро пожаловать в Ruby!

Арпан Сен, технический директор, Synapti Computer Aided Design Pvt Ltd

Арпан Сен (Arpan Sen) – ведущий инженер, работающий над разработкой программного обеспечения в области автоматизации электронного проектирования. На протяжении нескольких лет он работал над некоторыми функциями UNIX, в том числе Solaris, SunOS, HP-UX и IRIX, а также Linux и Microsoft Windows. Он проявляет живой интерес к методикам оптимизации производительности программного обеспечения, теории графов и параллельным вычислениям. Арпан является аспирантов в области программных систем.



25.12.2012

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

  • Иерархия классов Ruby.
  • Singleton-методы в Ruby.
  • Ключевое слово self.
  • Метод method_missing.
  • Обработка исключений.
  • Работа с потоками

Примечание. Весь код в этой статье написан и протестирован в Ruby версии 1.8.7.

Иерархия классов в Ruby

Иерархия классов в Ruby может показаться запутанной. Давайте создадим класс типа Cat и поиграем с его иерархией (см. листинг 1).

Листинг 1. Неявная иерархия классов в Ruby
irb(main):092:0> class Cat
irb(main):093:1> end
=> nil

irb(main):087:0> c = Cat.new
=> #<Cat:0x2bacb68>
irb(main):088:0> c.class
=> Cat
irb(main):089:0> c.class.superclass
=> Object
irb(main):090:0> c.class.superclass.superclass
=> nil
irb(main):091:0> c.class.superclass.superclass.superclass
NoMethodError: undefined method `superclass' for nil:NilClass
        from (irb):91
        from :0

Все объекты в Ruby (даже определенные пользователем) являются потомками класса Object, как четко видно из листинга 1. Это сильно отличается от C++. Здесь нет никаких явно указываемых типов данных, как, например, int или double в C/C++. В листинге 2 показана иерархия классов для целого числа 1.

Листинг 2. Иерархия классов для 1
irb(main):100:0> 1.class
=> Fixnum
irb(main):101:0> 1.class.superclass
=> Integer
irb(main):102:0> 1.class.superclass.superclass
=> Numeric
irb(main):103:0> 1.class.superclass.superclass.superclass
=> Object

Пока все хорошо. Теперь вы знаете, что сами по себе классы – это объекты типа Class. В свою очередь, Class в конечном счете является производным классом Object, как показано в листинге 3 на примере встроенного класса Ruby String.

Листинг 3. Иерархия классов для классов
irb(main):100:0> String.class
=> Class
irb(main):101:0> String.class.superclass
=> Module
irb(main):102:0> String.class.superclass.superclass
=> Object

Module – это базовый класс для Class, и здесь необходимо предупредить о том, что вы не сможете напрямую приписать значения пользовательским объектам Module. Если вы не хотите вдаваться во внутренние подробности Ruby, то безопаснее считать, что объект Module имеет характеристики, схожие с характеристиками пространства имен C++: вы можете определять свои собственные методы, ограничения и т. д. Вы помещаете объект Module внутрь класса Class, и тут же все три элемента Module волшебным образом становятся элементами Class. Пример приведен в листинге 4.

Листинг 4. Нельзя напрямую приписать значения модулям, а можно использовать их только с классами
irb(main):020:0> module MyModule
irb(main):021:1> def hello
irb(main):022:2> puts "Hello World"
irb(main):023:2> end
irb(main):024:1> end
irb(main):025:0> test = MyModule.new
NoMethodError: undefined method `new' for MyModule:Module
        from (irb):25
irb(main):026:0> class MyClass
irb(main):027:1> include MyModule
irb(main):028:1> end
=> MyClass
irb(main):029:0> test = MyClass.new
=> #<MyClass:0x2c18bc8>
irb(main):030:0> test.hello
Hello World 
=> nil

Итак, краткое резюме: когда вы пишете в Ruby c = Cat.new, то c – это объект типа Cat, который унаследован из класса Object. Класс Cat является объектом типа Class, унаследованного из Module, который, в свою очередь, унаследован из Object. Таким образом, и объект, и его тип являются допустимыми объектами Ruby.


Singleton-методы и редактируемые классы

Теперь давайте рассмотрим singleton-методы (синглтоны). Предположим, вам нужно смоделировать в C++ нечто похожее на человеческое общество. Как бы вы это сделали? Создали бы класс под названием Human и миллионы объектов типа Human ? Это больше похоже на моделирование общества зомби, поскольку каждый человек должен иметь какие-то уникальные характеристики. В этой ситуации удобно использовать singleton-методы Ruby, как показано в листинге 5.

Листинг 5. Singleton-методы в Ruby
irb(main):113:0> y = Human.new
=> #<Human:0x319b6f0>
irb(main):114:0> def y.paint
irb(main):115:1> puts "Can paint"
irb(main):116:1> end
=> nil
irb(main):117:0> y.paint
Can paint
=> nil
irb(main):118:0> z = Human.new
=> #<Human:0x3153fc0>
irb(main):119:0> z.paint
NoMethodError: undefined method `paint' for #<Human:0x3153fc0>
        from (irb):119

Singleton-методы (синглтоны) в Ruby – это методы, связанные только с конкретным объектом и не доступные в общем классе. Эти методы начинаются с имени объекта. В листинге 5 метод paint является специфичным для объекта y и определен только для y; вызов метода z.paint приведет к ошибке, связанной с неопределенным методом. Можно получить список singleton-методов объекта, вызвав метод singleton_methods:

irb(main):120:0> y.singleton_methods
=> ["paint"]

Существует другой способ определения singleton-методов в Ruby. Рассмотрим листинг 6.

Листинг 6. Еще один способ создания singleton-методов
irb(main):113:0> y = Human.new
=> #<Human:0x319b6f0>
irb(main):114:0> class << y
irb(main):115:1> def sing
irb(main):116:1> puts "Can sing"
irb(main):117:1> end
irb(main):118:1>end
=> nil
irb(main):117:0> y.sing
Can sing
=> nil

В листинге 5 также открываются интересные возможности, связанные с добавлением новых методов в пользовательские классы, а также в существующие встроенные классы Ruby, наподобие String. В C++ такое невозможно, если у вас нет доступа к исходному коду используемых классов. Давайте взглянем на класс String еще раз (листинг 7).

Листинг 7. Ruby позволяет изменять существующие классы
irb(main):035:0> y = String.new("racecar")
=> "racecar"
irb(main):036:0> y.methods.grep(/palindrome/)
=> [ ]
irb(main):037:0> class String
irb(main):038:1> def palindrome?
irb(main):039:2> self == self.reverse
irb(main):040:2> end
irb(main):041:1> end
irb(main):050:0> y.palindrome?
=> true

Листинг 7 наглядно показывает, как можно отредактировать существующий класс Ruby и добавить в него любые методы на ваше усмотрение. В этом примере я добавил в класс String метод palindrome?. Таким образом, в Ruby реализована мощная функция – возможность редактирования классов во время выполнения.

Теперь, когда вы немного познакомились с иерархией классов и singleton-методами в Ruby, давайте перейдем к рассмотрению ключевого слова self. Обратите внимание на то, что я использовал self при определении метода palindrome?.


Ключевое слово self

Вероятно, самый распространенный способ использования ключевого слова self – это объявление статических методов классов Ruby, как показано в листинге 8.

Листинг 8. Использование ключевого слова self для объявления статических методов класса
class SelfTest
   def self.test
      puts "Hello World with self!"
   end
end

class SelfTest2
   def test
      puts "This is not a class static method"
   end
end

SelfTest.test
SelfTest2.test

Как видно из вывода листинга 8 (показан в листинге 9), вы не можете вызывать нестатические методы без объектов. Это поведение похоже на поведение в C++.

Листинг 9. Ошибка при вызове нестатических методов без объектов
irb(main):087:0> SelfTest.test
Hello World with self!
=> nil
irb(main):088:0> SelfTest2.test
NoMethodError: undefined method 'test' for SelfTest2:Class
        from (irb):88

Прежде чем перейти к более изощренным методам использования ключевого слова self, следует обратить внимание на то, что Ruby позволяет определить статический метод, указав перед его именем имя класса:

class TestMe
   def TestMe.test
       puts "Yet another static member function"
   end
end

TestMe.test  # works fine

В листинге 10 приведен более интересный (и выглядящий более сложным) пример использования self.

Листинг 10. Использование метакласса для объявления статических методов
class MyTest
   class << self 
     def test
        puts "This is a class static method"
     end
   end
end

MyTest.test   # works fine

В этом примере метод test определен как статический метод класса немного по-другому. Чтобы понять, что происходит, необходимо разобраться в деталях синтаксиса class < < self. Конструкция class < < self … end создает метакласс. В цепочке поиска метода метакласс объекта ищется перед тем, как осуществляется доступ к базовому классу объекта. Если вы определяете метод в метаклассе, то он может быть вызван в классе. Это похоже на идею статических методов в C++.

Возможно ли получить доступ к метаклассу? Ответ – да. Просто возвратите self из class < < self … end. Заметьте, что в объявлении класса Ruby вы не обязаны помещать только определения метода. В листинге 11 приведен пример метакласса.

Листинг 11. Работа с метаклассом
irb(main):198:0> class MyTest
irb(main):199:1> end
=> nil
irb(main):200:0> y = MyTest.new
=> #< MyTest:0x2d43fe0>
irb(main):201:0> z = class MyTest
irb(main):202:1> class << self
irb(main):203:2> self
irb(main):204:2> end
irb(main):205:1> end
=> #<Class: MyTest > 
irb(main):206:0> z.class
=> Class
irb(main):207:0> y.class
=> MyTest

Возвращаясь к коду листинга 7, мы видим, что метод palindrome определен как self == self.reverse. В этом случае использование ключевого слова self не отличается от его использования в C++. Чтобы изменить или извлечь информацию состояния, для выполнения методов как в C++, так и в Ruby требуется объект. Здесь ключевое слово self ссылается на этот объект. Обратите внимание на то, что при вызове публичного метода перед его именем может опционально указываться ключевое слово self для обозначения объекта, метод которого выполняется (см. листинг 12).

Листинг 12. Использование ключевого слова self для вызова методов
irb(main):094:0> class SelfTest3
irb(main):095:1> def foo
irb(main):096:2> self.bar()
irb(main):097:2> end
irb(main):098:1> def bar
irb(main):099:2> puts "Testing Self"
irb(main):100:2> end
irb(main):101:1> end
=> nil
irb(main):102:0> test = SelfTest3.new
=> #<SelfTest3:0x2d15750>
irb(main):103:0> test.foo
Testing Self 
=> nil

В Ruby нельзя вызывать закрытые методы, указывая перед их именем ключевое слово self. Разработчику на C++ это может показаться странным. Код в листинге 13 ясно дает понять, что ключевое слово self нельзя использовать с закрытыми методами – вызовы закрытых методов можно осуществлять только с неявными объектами.

Листинг 13. Ключевое слово self нельзя использовать при вызове закрытого метода
irb(main):110:0> class SelfTest4
irb(main):111:1> def method1
irb(main):112:2> self.method2
irb(main):113:2> end
irb(main):114:1> def method3
irb(main):115:2> method2
irb(main):116:2> end
irb(main):117:1> private
irb(main):118:1> def method2
irb(main):119:2> puts "Inside private method"
irb(main):120:2> end
irb(main):121:1> end
=> nil
irb(main):122:0> y = SelfTest4.new
=> #<SelfTest4:0x2c13d80>
irb(main):123:0> y.method1
NoMethodError: private method `method2' called for #<SelfTest4:0x2c13d80>
        from (irb):112:in `method1'
irb(main):124:0> y.method3
Inside private method 
=> nil

Поскольку все в Ruby является объектами, вот, что вы получите при вызове self в приглашении irb:

irb(main):104:0> self
=> main
irb(main):105:0> self.class
=> Object

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

Итак, хватит о self. Давайте перейдем к динамическим методам и методу method_missing.


Загадка метода method_missing

Рассмотрим код Ruby, представленный в листинге 14.

Листинг 14. Метод method_missing в действии
irb(main):135:0> class Test
irb(main):136:1> def method_missing(method, *args)
irb(main):137:2> puts "Method: #{method} Args: (#{args.join(', ')})"
irb(main):138:2> end
irb(main):139:1> end
=> nil
irb(main):140:0> t = Test.new
=> #<Test:0x2c7b850>
irb(main):141:0> t.f(23)
Method: f Args: (23)
=> nil

Если вы занимаетесь магией вуду, то, несомненно, листинг 14 вызовет у вас улыбку. Что же происходит в нем? Мы создали объект типа Test, а затем вызвали метод t.f с аргументом 23. Но объект Test не содержит метода f, и мы должны были получить сообщение об ошибке NoMethodError или подобное ему. Однако, Ruby делает здесь нечто очень интересное: вызовы методов перехватываются и обрабатываются с помощью method_missing. Первым аргументом method_missing является имя отсутствующего метода – в нашем случае это f. Вторым, и последним аргументом является выражение *args, перехватывающее передаваемые в f аргументы. Где можно использовать это? Помимо всего прочего, вы легко можете направить вызовы методов включенному модулю Module или объекту компонентной модели без явного использования API-интерфейса упаковщика для каждого вызова в классе верхнего уровня.

Давайте рассмотрим еще немного магии вуду в листинге 15.

Листинг 15. Использование метода send для передачи аргументов в функцию
irb(main):142:0> class Test
irb(main):143:1> def method1(s, y)
irb(main):144:2> puts "S: #{s} Y: #{y}"
irb(main):145:2> end
irb(main):146:1> end
=> nil
irb(main):147:0>t = Test.new
irb(main):148:0> t.send(:method1, 23, 12)
S: 23 Y: 12
=> nil

В листинге 15 для класса class Test был определен метод method1. Однако вместо непосредственного вызова этого метода мы вызываем метод send. Метод send является публичным методом класса Object и, следовательно, доступен для класса Test (помните о том, что все классы наследуют Object). Первым аргументом метода send является символ или строка, содержащая имя метода. Что же умеет делать метод send, чего обычно не можете делать вы? С помощью него вы можете получить доступ к закрытым методам класса. Конечно, полезность этой функции остается предметом многочисленных споров. Давайте рассмотрим код, представленный в листинге 16.

Листинг 16. Получение доступа к закрытым методам класса
irb(main):258:0> class SendTest
irb(main):259:1> private
irb(main):260:1> def hello
irb(main):261:2> puts "Saying Hello privately"
irb(main):262:2> end
irb(main):263:1> end
=> nil
irb(main):264:0> y = SendTest.new
=> #< SendTest:0x2cc52c0>
irb(main):265:0> y.hello
NoMethodError: private method `hello' called for #< SendTest:0x2cc52c0>
        from (irb):265
irb(main):266:0> y.send(:hello)
Saying Hello privately
=> nil

Throw и catch – не то, чем они кажутся

Если вы (так же, как и я) привыкли писать на C++ и хотите написать в Ruby код с обработкой исключений, то, вероятно, почувствуете себя как дома, увидев, что здесь тоже есть ключевые слова throw и catch. К несчастью, throw и catch имеют в Ruby совершенно другое значение.

Исключения в Ruby обычно обрабатываются с помощью блоков begin…rescue, как показано в листинге 17.

Листинг 17. Обработка исключений в Ruby
begin
  f = File.open("ruby.txt")
  # .. continue file processing 
rescue ex => Exception
  # .. handle errors, if any 
ensure
  f.close unless f.nil?
  # always execute the code in ensure block 
end

Если при открытии файла в этом примере происходит что-то непредвиденное (например, файл отсутствует или имеет недопустимые разрешения), то выполняется код в блоке rescue. Код в блоке ensure выполняется всегда независимо от того, возникают ли исключения или нет. Тем не менее следует отметить, что помещать блок ensure после блока rescue не обязательно. Кроме того, если исключение необходимо инициировать явно, то в этом случае синтаксис имеет вид raise <MyException>. Если вы решите, что вам нужен собственный класс исключений, то можете унаследовать его из встроенного класса Ruby Exception, чтобы получить все преимущества существующих методов.

Конструкция catch-throw в Ruby на самом деле не является обработкой исключений: ключевое слово throw можно использовать для изменения хода выполнения программы. В листинге 18 приведен пример использования throw и catch.

Листинг 18. Throw и catch в Ruby
irb(main):185:0> catch :label do
irb(main):186:1* puts "This will print"
irb(main):187:1> throw :label
irb(main):188:1> puts "This will not print"
irb(main):189:1> end
This will print
=> nil

Когда в этом примере программа доходит до оператора throw, ее выполнение прекращается и интерпретатор начинает искать блок catch, который обрабатывает соответствующий символ. Выполнение программы продолжается с того места, в котором заканчивается блок catch. Посмотрите, как используются операторы throw и catch в листинге 19: обратите внимание на то, что можно без труда использовать операторы catch и throw в функциях.

Листинг 19. Обработка исключений в Ruby: вложенные блоки catch
irb(main):190:0> catch :label do
irb(main):191:1* catch :label1 do
irb(main):192:2* puts "This will print"
irb(main):193:2> throw :label
irb(main):194:2> puts "This won't print"
irb(main):195:2> end
irb(main):196:1> puts "Neither will this print"
irb(main):197:1> end
This will print
=> nil

Некоторые даже говорят, что Ruby с его поддержкой операторов catch и throw выводит безумие оператора goto языка C на принципиально новый уровень. Учитывая, что можно организовать несколько вложенных уровней функций, содержащих блоки catch на каждом уровне, аналогия с безумием goto вполне уместна.


Потоки в Ruby могут быть зелеными

Ruby версии 1.8.7 не поддерживает реальную многопоточность. Совсем. Тем не менее вы скажете, что в Ruby есть конструкция thread. Да, есть. Но метод Thread.new не порождает реальный поток в операционной системе каждый раз, когда вы вызываете его. Потоки, которые поддерживаются в Ruby – это green threads (зеленые потоки): для выполнения заданий, поступающих от нескольких потоков на уровне приложения, интерпретатор Ruby использует единственный поток операционной системы.

Концепция "зеленых потоков" оказывается полезной, например, тогда, когда один из потоков ожидает начала каких-либо операций ввода/вывода; чтобы центральный процессор не простаивал зря, вы легко можете выделить это время другому потоку. Однако эта конструкция не может использовать современные многоядерные процессоры (в Википедии есть отличная статья, объясняющая, что такое зеленые потоки – см. раздел Ресурсы).

Это доказывает последний пример (листинг 20).

Листинг 20. Несколько потоков в Ruby
#!/usr/bin/env ruby
 
def func(id, count)
  i = 0;
  while (i < count)
    puts "Thread #{i} Time: #{Time.now}"
    sleep(1)
    i = i + 1
  end
end
 
puts "Started at #{Time.now}"
thread1 = Thread.new{func(1, 100)}
thread2 = Thread.new{func(2, 100)}
thread3 = Thread.new{func(3, 100)}
thread4 = Thread.new{func(4, 100)}

thread1.join
thread2.join
thread3.join
thread4.join
puts "Ending at #{Time.now}"

Предположим, что вы запускаете утилиту top в среде Linux® или UNIX®, выполняете в терминале код листинга 20, определяете идентификатор процесса и запускаете команду top –p <id процесса>. Когда команда top начинает работу, вы нажимаете Shift-H, чтобы увидеть количество запущенных потоков. Вы должны увидеть всего один поток, что и служит доказательством того, что многопоточность в Ruby 1.8.7 – это миф.

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


Заключение

В этой статье были рассмотрены следующие вопросы:

  • Идея иерархии классов в Ruby.
  • Singleton-методы.
  • Расшифровка ключевого слова self и метода method_missing.
  • Исключения.
  • Работа с потоками.

Несмотря на свои особенности, Ruby – очень интересный язык программирования, обладающий чрезвычайно мощными возможностями и позволяющий выполнять многие вещи, используя минимум кода. Поэтому не удивительно, что многие крупномасштабные приложения, как, например, Twitter, используют Ruby для раскрытия своего истинного потенциала. Желаю вам приятной работы в Ruby!

Ресурсы

Научиться

  • Оригинал статьи: Meet six misunderstood Ruby features (EN).
  • Обязательно прочтите второе издание руководства Programming Ruby: The Pragmatic Programmers' Guide (EN) (авторы Dave Thomas, Chad Fowler и Andy Hunt), известного под названием "кирка" по рисунку на обложке книги.
  • The Ruby Programming Language (EN) [авторы Yukihiro "Matz" Matsumoto (создатель Ruby) и David Flanagan, издательство O'Reilly, 2008 г.] – еще один бесценный ресурс, посвященный Ruby.
  • Посетите Web-сайт To Ruby From C and C++ (EN) – отличный ресурс для программистов на C/C++, которые хотят освоить Ruby.
  • Узнайте больше о зеленых потоках (EN) на страницах Википедии.

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Open source, Linux
ArticleID=853363
ArticleTitle=Шесть нетривиальных возможностей Ruby
publish-date=12252012