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


Стоит отметить, что в старых версиях Ruby (до 1.6) символьные константы были полноценными объектами, поскольку преобразовывались в Fixnum и в таком виде хранились. Внутреннее представление осталось таким же; символу ставится в соответствие число, и хранится он как непосредственное значение. Само число можно получить, вызвав метод to_i, но в этом редко возникает необходимость.

По словам Джима Вайриха, символ - это "объект, у которого есть имя". Остин Зиглер предпочитает говорить об "объекте, который сам является именем". Как бы то ни было, существует взаимно однозначное соответствие между символами и именами. К чему можно применить имена? Например, к переменным, методам и произвольным константам.

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

class SomeClass

attr_accessor :whatever

end

To же самое можно выразить иначе:

class SomeClass

def whatever

@whatever

end

def whatever=(val)

@whatever = val

end

end

Другими словами, символ :whatever говорит методу attr_accessor, что методам чтения и установки (а равно и самой переменной экземпляра) следует присвоить имена, определяемые указанным символом.

Но почему не воспользоваться просто строкой? Вообще-то можно. Многие, даже большинство системных методов, ожидающих символ в качестве параметра, соглашаются и на строку.

attr_reader :alpha

attr_reader "beta" # Так тоже можно.

На самом деле символ "похож" на строку в том смысле, что ему соответствует последовательность символов. Поэтому некоторые говорят, что "символ - это просто неизменяемая строка". Но класс Symbol не наследует классу String, а типичные операции над строками необязательно применимы к символам.

Также неправильно думать, что символы напрямую соответствуют идентификаторам. Из-за этого непонимания некоторые говорят о "таблице символов" (как если бы речь шла об ассемблированном объектном коде). В действительности это представление бессмысленно; хотя символы и хранятся в какой-то внутренней таблице (а как же иначе?), Ruby не дает к ней доступа, поэтому программистам все равно, существует она или нет.

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

sym1 = :"This is a symbol"

sym2 = :"This is, too!"

sym3 = :")(*&^%$" # И даже такой.

Можно даже использовать символы для определения переменных и методов экземпляра, но тогда для ссылки на них пришлось бы применять такие методы, как send и instance_variable_get. Вообще говоря, такая практика не рекомендуется.

6.1.1. Символы как перечисления

В языке Pascal и в поздних версиях С есть понятие перечисляемого типа. В Ruby ничего подобного быть не может, ведь никакого контроля типов не производится. Но символы часто используются как мнемонические имена; стороны света можно было бы представить как :north, :south, :east и :west.

Быть может, немного понятнее хранить их в виде констант:

North, South, East, West = :north, :south, :east, :west

Если бы это были строки, а не символы, то определение их в виде констант могло бы сэкономить память, но каждый символ все равно существует в объектном пространстве в единственном экземпляре. (Символы, подобно объектам Fixnum, хранятся как непосредственные значения.)

6.1.2. Символы как метазначения

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

В таком механизме часто возникает необходимость. Когда-то символ NUL кода ASCII вообще не считался символом. В языке С есть понятие нулевого указателя (NULL), в Pascal есть указатель nil, в SQL NULL означает отсутствие какого бы то ни было значения. В Ruby, конечно, тоже есть свой nil.

Проблема в том, что такие метазначения часто путают с действительными значениями. В наши дни все считают NUL настоящим символом кода ASCII. И в Ruby нельзя сказать, что nil не является объектом; его можно хранить, над ним можно выполнять какие-то операции. Поэтому не вполне понятно, как интерпретировать ситуацию, когда hash [key] возвращает nil: то ли указанный ключ вообще не найден, то ли с ним ассоциировано значение nil.

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

str = get_string

case str

when String

# Нормальная обработка.

when :eof

# Конец файла, закрытие сокета и т.п.

when :error

# Ошибка сети или ввода/вывода.

when :timeout

# Ответ не получен вовремя.

end

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

6.1.3. Символы, переменные и методы

Наверное, чаще всего символы применяются для определения атрибутов класса:

class MyClass

attr_reader :alpha, :beta

attr_writer :gamma, :delta

attr_accessor :epsilon

# ...

end

Имейте в виду, что в этом фрагменте на самом деле исполняется некий код. Например, attr_accessor использует имя символа для определения имени переменной экземпляра, а также методов для ее чтения и изменения. Это не означает, что всегда имеется точное соответствие между символом и именем переменной экземпляра. Например, обращаясь к методу instance_variable_set, мы должны задать точное имя переменной, включая и знак @:

sym1 = :@foo

sym2 = :foo

instance_variable_set(sym1,"str") # Правильно.

instance_variable_set(sym2,"str") # Ошибка.

Короче говоря, символ, передаваемый методам из семейства attr, - всего лишь аргумент, а сами эти методы создают требуемые переменные и методы экземпляра, основываясь на значении символа. (В конец имени метода изменения добавляется знак равенства, а в начало имени переменной экземпляра - знак @.) Бывают также случаи, когда символ должен точно соответствовать идентификатору, на который ссылается.

В большинстве случаев (если не во всех!) методы, ожидающие на входе символ, принимают также строку. Обратное не всегда верно.

6.1.4. Преобразование строки в символ и обратно

Строки и символы можно преобразовывать друг в друга с помощью методов to_str и to_sym:

a = "foobar"

b = :foobar

a == b.to_str # true

b == a.to_sym # true

Для метапрограммирования иногда бывает полезен такой метод:

class Symbol

def +(other)

(self.to_s + other.to_s).to_sym

end

end

Он позволяет конкатенировать символы (или дописывать строку в конец символа). Ниже приведен пример использования; мы принимаем на входе символ и пытаемся определить, представляет ли он какой-нибудь метод доступа (то есть существует ли метод чтения или установки атрибута с таким именем):

class Object

def accessor?(sym)

return (self .respond_to?(sym) and self .respond_to?(sym+"="))

end

end

Упомяну также о более изощренном способе применения символов. Иногда при выполнении операции map нужно указать сложный блок. Однако во многих случаях мы просто вызываем некоторый метод для каждого элемента массива или набора:

list = words.map {|x| x.capitalize }

He кажется ли вам, что для такой простой задачи слишком много знаков препинания? Давайте вместо этого определим метод to_proc в классе Symbol. Он будет приводить любой символ к типу объекта proc. Но какой именно объект proc следует вернуть? Очевидно, соответствующий самому символу в контексте объекта; иными словами, такой, который пошлет сам символ в виде сообщения объекту.

def to_proc

proc {|obj, *args| obj.send(self, *args) }

end

Кстати, этот код заимствован из проекта Гэвина Синклера (Gavin Sinclair) "Расширения Ruby". Имея такой метод, мы можем следующим образом переписать первоначальный код:

list = words.map(&:capitalize)

Стоит потратить немного времени и разобраться, как это работает. Метод map обычно принимает только блок (никаких других параметров). Наличие знака & (амперсанд) позволяет передать объект proc вместо явно указанного блока. Поскольку мы применяем амперсанд к объекту, не являющемуся proc, то интерпретатор пытается вызвать метод to_proc этого объекта. Получающийся в результате объект proc подставляется вместо явного блока, чтобы метод map вызывал его для каждого элемента массива. А зачем передавать self в виде сообщения элементу массива? Затем, что объект proc является замыканием и, следовательно, помнит контекст, в котором был создан. А в момент создания self был ссылкой на символ, для которого вызывался метод to_proc.

6.2. Диапазоны

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

digits = 0..9

scalel = 0..10

scale2 = 0...10

Оператор .. включает конечную точку, а оператор ... не включает. (Если это вас неочевидно, просто запомните.) Таким образом, диапазоны digits и scale2 из предыдущего примера одинаковы.

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

Основные операции над диапазоном - обход, преобразование в массив, а также выяснение, попадает ли некоторый объект в данный диапазон. Рассмотрим разнообразные варианты этих и других операций.

6.2.1. Открытые и замкнутые диапазоны

Диапазон называется замкнутым, если включает конечную точку, и открытым - в противном случае:

r1 = 3..6 # Замкнутый.

r2 = 3...6 # Открытый.

a1 = r1.to_a # [3,4,5,6]

а2 = r2.to_a # [3,4,5]

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

6.2.2. Нахождение границ диапазона

Методы first и last возвращают соответственно левую и правую границу диапазона. У них есть синонимы begin и end (это еще и ключевые слова, но интерпретируются как вызов метода, если явно указан вызывающий объект).

r1 = 3..6

r2 = 3...6

r1a, r1b = r1. first, r1.last # 3,6

r1c, r1d = r1.begin, r1.end # 3,6

r2a, r2b = r1.begin, r1.end # 3,6

Метод exclude_end? сообщает, включена ли в диапазон конечная точка:

r1.exclude_end? # false

r2.exclude_end? # true

6.2.3. Обход диапазона

Обычно диапазон можно обойти. Для этого класс, которому принадлежат границы диапазона, должен предоставлять осмысленный метод succ (следующий).

(3..6).each {|x| puts x } # Печатаются четыре строки

# (скобки обязательны).

Пока все хорошо. И тем не менее будьте очень осторожны при работе со строковыми диапазонами! В классе String имеется метод succ, но он не слишком полезен. Пользоваться этой возможностью следует только при строго контролируемых условиях, поскольку метод succ определен не вполне корректно. (В определении используется, скорее, "интуитивно очевидный", нежели лексикографический порядок, поэтому существуют строки, для которых "следующая" не имеет смысла.)

r1 = "7".."9"

r2 = "7".."10"

r1.each {|x| puts x } # Печатаются три строки.

r2.each {|x| puts x } # Ничего не печатается!

Предыдущие примеры похожи, но ведут себя по-разному. Отчасти причина в том, что границы второго диапазона - строки разной длины. Мы ожидаем, что в диапазон входят строки "7", "8", "9" и "10", но что происходит на самом деле?

При обходе диапазона r2 мы начинаем со значения "7" и входим в цикл, который завершается, когда текущее значение окажется больше правой границы. Но ведь "7" и "10" - не числа, а строки, и сравниваются они как строки, то есть лексикографически. Поэтому левая граница оказывается больше правой, и цикл не выполняется ни разу.

А что сказать по поводу диапазонов чисел с плавающей точкой? Такой диапазон можно сконструировать и, конечно, проверить, попадает ли в него конкретное число. Это полезно. Но обойти такой диапазон нельзя, так как метод succ отсутствует.

fr = 2.0..2.2

fr.each {|x| puts x } # Ошибка!

Почему для чисел с плавающей точкой нет метода succ? Теоретически можно было бы увеличивать число на некоторое приращение. Но величина такого приращения сильно зависела бы от конкретной машины, при этом даже для обхода "небольшого" диапазона понадобилось бы гигантское число итераций, а полезность такой операции весьма сомнительна.

6.2.4. Проверка принадлежности диапазону

Зачем нужен диапазон, если нельзя проверить, принадлежит ли ему конкретный объект? Эта задача легко решается с помощью метода include?:

r1 = 23456..34567

x = 14142

y = 31416

r1.include?(x) # false

r1.include?(у) # true

У этого метода есть также синоним member?.

А как он работает? Как интерпретатор определяет, принадлежит ли объект диапазону? Просто путем сравнения с границами (поэтому проверка принадлежности диапазону возможна лишь, если определен осмысленный оператор <=>). Следовательно, запись (a..b).include?(x) эквивалентна x >= a and x <= b. Еще раз предупреждаем: будьте осторожны со строковыми диапазонами!

s1 = "2".."5"

str = "28"

s1.include?(str) # true (неправильно!)

6.2.5. Преобразование в массив

Когда диапазон преобразуется в массив, интерпретатор последовательно вызывает метод succ, пока не будет достигнута правая граница, и помещает каждый элемент диапазона в возвращаемый массив:

r = 3..12

arr = r.to_a # [3,4,5,6,7,8,9,10,11,12]

Ясно, что для диапазонов чисел типа Float такой подход не работает. Со строковыми диапазонами иногда будет работать, но лучше этого не делать, поскольку результат не всегда очевиден или осмыслен.

6.2.6. Обратные диапазоны

Имеет ли смысл говорить об обратном диапазоне? И да, и нет. Следующий диапазон допустим:

r = 6..3

x = r.begin # 6

y = r.end # 3

flag = r.end_excluded? # false

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

arr = r. to_a # []

r.each {|x| p x } # Ни одной итерации.

y = 5

r.include?(у) # false (для любого значения y)

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

Назад Дальше