Текст книги "Программирование на языке Ruby"
Автор книги: Хэл Фултон
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 5 (всего у книги 56 страниц) [доступный отрывок для чтения: 20 страниц]
Как и многие другие современные языки, Ruby поддерживает исключения.
Исключения – это механизм обработки ошибок, имеющий существенные преимущества по сравнения с прежними подходами. Нам удается избежать возврата кодов ошибок и запутанной логики их анализа, а код, который обнаруживает ошибку, можно отделить от кода, который ее обрабатывает (чаще всего они так или иначе разделены).
Предложение raise
возбуждает исключение. Отметим, что raise
– не зарезервированное слово, а метод модуля Kernel
. (У него есть синоним fail
.)
raise # Пример 1
raise "Произошла ошибка" # Пример 2
raise ArgumentError # Пример 3
raise ArgumentError, "Неверные данные" # Пример 4
raise ArgumentError.new("Неверные данные ") # Пример 5
raise ArgumentError, " Неверные данные ", caller[0] # Пример 6
В первом примере повторно возбуждается последнее встретившееся исключение. В примере 2 создается исключение RuntimeError
(подразумеваемый тип), которому передается сообщение "Произошла ошибка"
.
В примере 3 возбуждается исключение типа ArgumentError
, а в примере 4 такое же исключение, но с сообщением "Неверные данные"
. Пример 5 – просто другая запись примера 4. Наконец, в примере 6 еще добавляется трассировочная информация вида "filename:line"
или "filename:line:in 'method'"
(хранящаяся в массиве caller
).
А как обрабатываются исключения в Ruby? Для этой цели служит блок begin-end
. В простейшей форме внутри него нет ничего, кроме кода:
begin
#Ничего полезного.
#...
end
Просто перехватывать ошибки не очень осмысленно. Но у блока может быть один или несколько обработчиков rescue
. Если произойдет ошибка в любой точке программы между begin
и rescue
, то управление сразу будет передано в подходящий обработчик rescue
.
begin
x = Math.sqrt(y/z)
# ...
rescue ArgumentError
puts "Ошибка при извлечении квадратного корня."
rescue ZeroDivisionError
puts "Попытка деления на нуль."
end
Того же эффекта можно достичь следующим образом:
begin
x = Math.sqrt(y/z)
# ...
rescue => err
puts err
end
Здесь в переменной err
хранится объект-исключение; при выводе ее на печать объект будет преобразован в осмысленную символьную строку. Отметим, что коль скоро тип ошибки не указан, то этот обработчик rescue
будет перехватывать все исключения, производные от класса StandardError
. В конструкции rescue => variable
можно перед символом =>
дополнительно указать тип ошибки.
Если типы ошибок указаны, то может случиться так, что тип реально возникшего исключения не совпадает ни с одним из них. На этот случай после всех обработчиков rescue
разрешается поместить ветвь else
.
begin
# Код, в котором может возникнуть ошибка...
rescue Type1
# ...
rescue Type2
# ...
else
#Прочие исключения...
end
Часто мы хотим каким-то образом восстановиться после ошибки. В этом поможет ключевое слово retry
(внутри тела обработчика rescue
). Оно позволяет повторно войти в блок begin
и попытаться еще раз выполнить операцию:
begin
# Код, в котором может возникнуть ошибка...
rescue
# Пытаемся восстановиться...
retry # Попробуем еще раз.
end
Наконец, иногда необходим код, который «подчищает» что-то после выполнения блока begin-end
. В этом случае можно добавить часть ensure
:
begin
# Код, в котором может возникнуть ошибка...
rescue
# Обработка исключений.
ensure
# Этот код выполняется в любом случае.
end
Код, помещенный внутрь части ensure
, выполняется при любом способе выхода из блока begin-end
– вне зависимости от того, произошло исключение или нет.
Исключения можно перехватывать еще двумя способами. Во-первых, существует форма rescue
в виде модификатора:
x = a/b rescue puts("Деление на нуль!")
Кроме того, тело определения метода представляет собой неявный блок begin-end
; слово begin
опущено, а все тело метода подготовлено к обработке исключения и завершается словом end
:
def some_method
# Код...
rescue
# Восстановление после ошибки...
end
На этом мы завершаем как обсуждение обработки исключений, так и рассмотрение основ синтаксиса и семантики в целом.
У Ruby есть многочисленные аспекты, которых мы не коснулись. Оставшаяся часть главы посвящена более развитым возможностям языка, в том числе рассмотрению ряда практических приемов, которые помогут программисту среднего уровня научиться «думать на Ruby».
1.3. ООП в Ruby
В языке Ruby есть все элементы, которые принято ассоциировать с объектно-ориентированными языками: объекты с инкапсуляцией и сокрытием данных, методы с полиморфизмом и переопределением, классы с иерархией и наследованием. Но Ruby идет дальше, добавляя ограниченные возможности создания метаклассов, синглетные методы, модули и классы-примеси.
Похожие идеи, только под иными именами, встречаются и в других объектно-ориентированных языках, но одно и то же название может скрывать тонкие различия. В этом разделе мы уточним, что в Ruby понимается под каждым из элементов ООП.
1.3.1. ОбъектыВ Ruby все числа, строки, массивы, регулярные выражения и многие другие сущности фактически являются объектами. Работа программы состоит в вызове методов разных объектов:
3.succ # 4
"abc".upcase # "ABC"
[2,1,5,3,4].sort # [1,2,3,4,5]
someObject.someMethod # какой-то результат
В Ruby каждый объект представляет собой экземпляр какого-то класса. Класс содержит реализацию методов:
"abc".class # String
"abc".class.class # Class
Помимо инкапсуляции собственных атрибутов и операций объект в Ruby имеет уникальный идентификатор:
"abc".object_id # 53744407
Этот идентификатор объекта обычно не представляет интереса для программиста.
1.3.2. Встроенные классыСвыше 30 классов уже встроено в Ruby. Как и во многих других объектно-ориентированных языках, в нем не допускается множественное наследование, но это еще не означает, что язык стал менее выразительным. Современные языки часто построены согласно модели одиночного наследования. Ruby поддерживает модули и классы-примеси, которые мы обсудим в следующей главе. Также реализованы идентификаторы объектов, что позволяет строить устойчивые, распределенные и перемещаемые объекты.
Для создания объекта существующего класса обычно используется метод new
:
myFile = File.new("textfile.txt","w")
myString = String.new("Это строковый объект")
Однако не всегда его обязательно вызывать явно. В частности, при создании объекта String можно и не упоминать этот метод:
yourString = "Это тоже строковый объект"
aNumber =5 # и здесь метод new не нужен
Ссылки на объекты хранятся в переменных. Выше уже отмечалось, что сами переменные не имеют типа и не являются объектами – они лишь ссылаются на объекты.
x = "abc"
Из этого правила есть исключение: небольшие неизменяемые объекты некоторых встроенных классов, например Fixnum
, непосредственно копируются в переменные, которые на них ссылаются. (Размер этих объектов не превышает размера указателя, поэтому хранить их таким образом более эффективно.) В таком случае во время присваивания делается копия объекта, а куча не используется.
При присваивании переменных ссылки на объекты обобществляются.
y = "abc"
x = y
x # "abc"
После выполнения присваивания x = y
и x
, и y
ссылаются на один и тот же объект:
x.object_id # 53732208
y.object_id # 53732208
Если объект изменяемый, то модификация, примененная к одной переменной, отражается и на другой:
x.gsub!(/а/, "x")
y # "хbс"
Однако новое присваивание любой из этих переменных не влияет на другую:
# Продолжение предыдущего примера
x = "abc"
y # по-прежнему равно "хbс"
Изменяемый объект можно сделать неизменяемым, вызвав метод freeze
:
x.freeze
x.gsub!(/b/,"y") # Ошибка!
Символ в Ruby ссылается на переменную по имени, а не по ссылке. Во многих случаях он может вообще не ссылаться на идентификатор, а вести себя как некая разновидность неизменяемой строки. Символ можно преобразовать в строку с помощью метода to_s
.
Hearts = :Hearts # Это один из способов присвоить
Clubs = :Clubs # уникальное значение константе,
Diamonds = :Diamonds # некий аналог перечисления
Spades = :Spades # в языках Pascal или С.
puts Hearts.to_s # Печатается "Hearts"
Продемонстрированный выше фокус с «перечислением» был более осмыслен на ранних этапах развития Ruby, когда еще не было класса Symbol
, а наличие двоеточия перед идентификатором превращало его в целое число. Если вы пользуетесь таким трюком, не предполагайте, что фактическое значение символа будет неизменным или предсказуемым – просто используйте его как константу, значение которой неважно.
Многие встроенные методы наследуются от классов-предков. Особо стоит отметить методы модуля Kernel
, подмешиваемые к суперклассу Object
. Поскольку класс Object
повсеместно доступен, то и добавленные в него из Kernel
методы также доступны в любой точке программы. Эти методы играют важную роль в Ruby.
Термины «модуль» и «примесь» – почти синонимы. Модуль представляет собой набор методов и констант, внешних по отношению к программе на Ruby. Его можно использовать просто для управления пространством имен, но основное применение модулей связано с «подмешиванием» его возможностей в класс (с помощью директивы include
). В таком случае он используется как класс-примесь.
Этот термин очевидно заимствован из языка Python. Стоит отметить, что в некоторых вариантах LISP такой механизм существует уже больше двадцати лет.
Не путайте описанное выше употребление термина «модуль» с другим значением, которое часто придается ему в информатике. Модуль в Ruby – это не внешний исходный текст и не двоичный файл (хотя может храниться и в том, и в другом виде). Это объектно-ориентированная абстракция, в чем-то похожая на класс.
Примером использования модуля для управления пространством имен служит модуль Math
. Так, чтобы получить определение числа π, необязательно включать модуль Math
с помощью предложения include;
достаточно просто написать Math::PI
.
Примесь дает способ получить преимущества множественного наследования, не отягощенные характерными для него проблемами. Можно считать, что это ограниченная форма множественного наследования, но создатель языка Мац называет его одиночным наследованием с разделением реализации.
Отметим, что предложение include
включает имена из указанного пространства имен (модуля) в текущее. Метод extend
добавляет объекту функции из модуля. В случае применения include
методы модуля становятся доступны как методы экземпляра, а в случае extend
– как методы класса.
Необходимо оговориться, что операции load
и require
не имеют ничего общего с модулями: они относятся к исходным и двоичным файлам (загружаемым динамически или статически). Операция load
читает файл и вставляет его в текущую точку исходного текста, так что начиная с этой точки становятся видимы все определения, находящиеся во внешнем файле. Операция require
аналогична load
, но не загружает файл, если он уже был загружен ранее.
Программисты, только начинающие осваивать Ruby, особенно имеющие опыт работы с языком С, могут поначалу путать операции require
и include
, которые никак не связаны между собой. Вы еще поймаете себя на том, что сначала вызываете require
, а потом include
для того, чтобы воспользоваться каким-то внешним модулем.
В Ruby есть множество встроенных классов, и вы сами можете определять новые. Для определения нового класса применяется такая конструкция:
class ClassName
# ...
end
Само имя класса – это глобальная константа, поэтому оно должно начинаться с прописной буквы. Определение класса может содержать константы, переменные класса, методы класса, переменные экземпляра и методы экземпляра. Данные уровня класса доступны всем объектам этого класса, тогда как данные уровня экземпляра доступны только одному объекту
Попутное замечание: строго говоря, классы в Ruby не имеют имен. «Имя» класса – это всего лишь константа, ссылающаяся на объект типа Class
(поскольку в Ruby Class
– это класс). Ясно, что на один и тот же класс могут ссылаться несколько констант, и их можно присваивать переменным точно так же, как мы поступаем с любыми другими объектами (поскольку в Ruby Class
– это объект). Если вы немного запутались, не расстраивайтесь. Удобства ради новичок может считать, что в Ruby имя класса – то же самое, что в C++.
Вот как определяется простой класс:
class Friend
@@myname = "Эндрю" # переменная класса
def initialize(name, sex, phone)
@name, @sex, @phone = name, sex, phone
# Это переменные экземпляра
end
def hello # метод экземпляра
puts "Привет, я #{@name}."
end
def Friend.our_common_friend # метод класса
puts "Все мы друзья #{@@myname}."
end
end
f1 = Friend.new("Сюзанна","F","555-0123")
f2 = Friend.new("Том","M","555-4567")
f1.hello # Привет, я Сюзанна.
f2.hello # Привет, я Том.
Friend.our_common_friend # Все мы друзья Эндрю.
Поскольку данные уровня класса доступны во всем классе, их можно инициализировать в момент определения класса. Если определен метод с именем initialize
, то гарантируется, что он будет вызван сразу после выделения памяти для объекта. Этот метод похож на традиционный конструктор, но не выполняет выделения памяти. Память выделяется методом new
, а освобождается неявно сборщиком мусора.
Теперь взгляните на следующий фрагмент, обращая особое внимание на методы getmyvar
, setmyvar
и myvar=
:
class MyClass
NAME = "Class Name" # константа класса
@@count = 0 # инициализировать переменную класса
def initialize # вызывается после выделения памяти для объекта
@@count += 1
@myvar = 10
end
def MyClass.getcount # метод класса
@@count # переменная класса
end
def getcount # экземпляр возвращает переменную класса!
@@count # переменная класса
end
def getmyvar # метод экземпляра
@myvar # переменная экземпляра
end
def setmyvar(val) # метод экземпляра устанавливает @myvar
@myvar = val
end
def myvar=(val) # другой способ установить @myvar
@myvar = val
end
end
foo = MyClass.new # @myvar равно 10
foo.setmyvar 20 # @myvar равно 20
foo.myvar =30 # @myvar равно 30
Здесь мы видим, что getmyvar
возвращает значение переменной @myvar
, а setmyvar
устанавливает его. (Многие программисты говорят о методах чтения и установки). Все это работает, но не является характерным способом действий в Ruby. Метод myvar=
похож на перегруженный оператор присваивания (хотя, строго говоря, таковым не является); это более удачная альтернатива setmyvar
, но есть способ еще лучше.
Класс Module
содержит методы attr
, attr_accessor
, attr_reader
и attr_writer
. Ими можно пользоваться (передавая символы в качестве параметров) для автоматизации управления доступом к данным экземпляра. Например, все три метода getmyvar
, setmyvar
и myvar=
можно заменить одной строкой в определении класса:
attr_accessor :myvar
При этом создается метод myvar
, который возвращает значение @myvar
, и метод myvar=
, который позволяет изменить значение той же переменной. Методы attr_reader
и attr_write
r создают соответственно версии методов доступа к атрибуту для чтения и для изменения.
Внутри методов экземпляра, определенных в классе, можно при необходимости пользоваться переменной self
. Это просто ссылка на объект, от имени которого вызван метод экземпляра.
Для управления видимостью методов класса можно пользоваться модификаторами private
, protected
и public
. (Переменные экземпляра всегда закрыты, обращаться к ним извне класса можно только с помощью методов доступа.) Каждый модификатор принимает в качестве параметра символ, например :foo
, а если он опущен, то действие модификатора распространяется на все последующие определения в классе. Пример:
class MyClass
def method1
# ...
end
def method2
# ...
end
def method3
# ...
end
private :method1
public
:method2
protected :method3
private
def my_method
# ...
end
def another_method
# ...
end
end
В этом классе метод method1
закрытый, method2
открытый, a method3
защищенный. Поскольку далее вызывается метод private
без параметров, то методы my_method
и another_method
будут закрытыми.
Уровень доступа public
не нуждается в объяснениях, он не налагает никаких ограничений ни на доступ к методу, ни на его видимость. Уровень private
означает, что метод доступен исключительно внутри класса или его подклассов и может вызываться только в «функциональной форме» от имени self
, причем вызывающий объект может указываться явно или подразумеваться неявно. Уровень protected
означает, что метод вызывается только внутри класса, но, в отличие от закрытого метода, не обязательно от имени self
.
По умолчанию все определенные в классе методы открыты. Исключение составляет лишь initialize
. Методы, определенные на верхнем уровне программы, тоже по умолчанию открыты. Если они объявлены закрытыми, то могут вызываться только в функциональной форме (как, например, методы, определенные в классе Object
).
Классы в Ruby сами являются объектами – экземплярами метакласса Class
. Классы в этом языке всегда конкретны, абстрактных классов не существует. Однако теоретически можно реализовать и абстрактные классы, если вам это для чего-то понадобится.
Класс Object
является корнем иерархии. Он предоставляет все методы, определенные во встроенном модуле Kernel
.
Чтобы создать класс, наследующий другому классу, нужно поступить следующим образом:
class MyClass < OtherClass
# ...
end
Помимо использования встроенных методов, вполне естественно определить и собственные либо переопределить унаследованные. Если определяемый метод имеет то же имя, что и существующий, то старый метод замещается. Если новый метод должен обратиться к замещенному им «родительскому» методу (так бывает часто), можно воспользоваться ключевым словом super
.
Перегрузка операторов, строго говоря, не является неотъемлемой особенностью ООП, но этот механизм знаком программистам на C++ и некоторых других языках. Поскольку большинство операторов в Ruby так или иначе являются методами, то не должен вызывать удивления тот факт, что их можно переопределять или определять в пользовательских классах. Переопределять семантику оператора в существующем классе редко имеет смысл, зато в новых классах определение операторов – обычное дело.
Можно создавать синонимы методов. Для этого внутри определения класса предоставляется такой синтаксис:
alias newname oldname
Число параметров будет таким же, как для старого имени, и вызываться метод-синоним будет точно так же. Обратите внимание на отсутствие запятой; alias
– это не имя метода, а ключевое слово. Существует метод с именем alias_method
, который ведет себя аналогично, но в случае его применения параметры должны разделяться запятыми, как и для любого другого метода.
Как мы уже видели, методы обычно используются в сочетании с простыми экземплярами классов и переменными, причем вызывающий объект отделяется от имени метода точкой (receiver.method
). Если имя метода является знаком препинания, то точка опускается. У методов могут быть аргументы:
Time.mktime(2000, "Aug", 24, 16, 0)
Поскольку каждое выражение возвращает значение, то вызовы методов могут сцепляться:
3.succ.to_s
/(x.z).*?(x.z).*?/.match("x1z_1a3_x2z_1b3_").to_a[1..3]
3+2.succ
Отметим, что могут возникать проблемы, если выражение, являющееся результатом сцепления, имеет тип, который не поддерживает конкретный метод. Точнее, при определенных условиях некоторые методы возвращают nil
, а вызов любого метода от имени такого объекта приведет к ошибке. (Конечно, nil
– полноценный объект, но он не обладает теми же методами, что и, например, массив.)
Некоторым методам можно передавать блоки. Это верно для всех итераторов – как встроенных, так и определенных пользователем. Блок обычно заключается в операторные скобки do-end
или в фигурные скобки. Но он не рассматривается так же, как предшествующие ему параметры, если таковые существуют. Вот пример вызова метода File.open
:
my_array.each do |x|
some_action
end
File.open(filename) { |f| some_action }
Именованные параметры будут поддерживаться в последующих версиях Ruby, но на момент работы над этой книгой еще не поддерживались. В языке Python они называются ключевыми аргументами, сама идея восходит еще к языку Ada.
Методы могут принимать переменное число аргументов:
receiver.method(arg1, *more_args)
В данном случае вызванный метод трактует more_args
как массив и обращается с ним, как с любым другим массивом. На самом деле звездочка в списке формальных параметров (перед последним или единственным параметром) может «свернуть» последовательность фактических параметров в массив:
def mymethod(a, b, *с)
print a, b
с.each do |x| print x end
end
mymethod(1,2,3,4,5,6,7)
# a=1, b=2, c=[3,4,5,6,7]
В Ruby есть возможность определять методы на уровне объекта (а не класса). Такие методы называются синглетными; они принадлежат одному-единственному объекту и не оказывают влияния ни на класс, ни на его суперклассы. Такая возможность может быть полезна, например, при разработке графических интерфейсов пользователя: чтобы определить действие кнопки, вы задаете синглетный метод для данной и только данной кнопки.
Вот пример определения синглетного метода для строкового объекта:
str = "Hello, world!"
str2 = "Goodbye!"
def str.spell
self.split(/./).join("-")
end
str.spell # "H-e-l-l-o-,– -w-o-r-l-d-!"
str2.spell # Ошибка!
Имейте в виду, что метод определяется для объекта, а не для переменной. Теоретически с помощью синглетных методов можно было бы создать систему объектов на базе прототипов. Это менее распространенная форма ООП без классов[5]5
Типичный пример – язык JavaScript (Прим. перев.)
[Закрыть]. Основной структурный механизм в ней состоит в конструировании нового объекта путем использования существующего в качестве образца; новый объект ведет себя как старый за исключением тех особенностей, которые были переопределены. Тем самым можно строить системы на основе прототипов, а не наследования. Хотя у нас нет опыта в этой области, мы полагаем, что создание такой системы позволило бы полнее раскрыть возможности Ruby.