Программирование на языке Ruby - Хэл Фултон 7 стр.


1.3.4. Создание классов

В 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_writer создают соответственно версии методов доступа к атрибуту для чтения и для изменения.

Внутри методов экземпляра, определенных в классе, можно при необходимости пользоваться переменной 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, который ведет себя аналогично, но в случае его применения параметры должны разделяться запятыми, как и для любого другого метода.

1.3.5. Методы и атрибуты

Как мы уже видели, методы обычно используются в сочетании с простыми экземплярами классов и переменными, причем вызывающий объект отделяется от имени метода точкой (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 # Ошибка!

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

1.4. Динамические аспекты Ruby

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

Наверное, это самая трудная тема для программиста, приступающего к изучению Ruby. В данном разделе мы вкратце рассмотрим некоторые следствия, вытекающие из динамической природы языка.

1.4.1. Кодирование во время выполнения

Мы уже упоминали директивы load и require. Важно понимать, что это не встроенные предложения и не управляющие конструкции; на самом деле это методы. Поэтому их можно вызывать, передавая переменные или выражения как параметры, в том числе условно. Сравните с директивой #include в языках С и C++, которая обрабатывается во время компиляции.

Код можно строить и интерпретировать по частям. В качестве несколько искусственного примера рассмотрим приведенный ниже метод calculate и вызывающий его код:

def calculate(op1, operator, op2)

string = op1.to_s + operator + op2.to_s

# Предполагается, что operator - строка; построим длинную

# строку, состоящую из оператора и операндов.

eval(string) # Вычисляем и возвращаем значение.

end

@alpha = 25

@beta = 12

puts calculate(2, "+",2) # Печатается 4

puts calculate(5, "*", "@alpha") # Печатается 125

puts calculate("@beta", "**", 3) # Печатается 1728

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

puts "Имя метода: "

meth_name = gets

puts "Строка кода: "

code = gets

string = %[def #{meth_name}\n #{code}\n end] # Строим строку.

eval(string) # Определяем метод.

eval(meth_name) # Вызываем метод.

Зачастую необходимо написать программу, которая могла бы работать на разных платформах или при разных условиях, но при этом сохранить общий набор исходных текстов. Для этого в языке С применяются директивы #ifdef, но в Ruby все определения исполняются. Не существует такого понятия, как "этап компиляции"; все конструкции динамические, а не статические. Поэтому для принятия решения такого рода мы можем просто вычислить условие во время выполнения:

if platform == Windows

action1

elsif platform == Linux

action2

else

default_action

end

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

if platform == Windows

def my_action

Назад Дальше