Загрузка...


  • 6.1. Символы
  • 6.1.1. Символы как перечисления
  • 6.1.2. Символы как метазначения
  • 6.1.3. Символы, переменные и методы
  • 6.1.4. Преобразование строки в символ и обратно
  • 6.2. Диапазоны
  • 6.2.1. Открытые и замкнутые диапазоны
  • 6.2.2. Нахождение границ диапазона
  • 6.2.3. Обход диапазона
  • 6.2.4. Проверка принадлежности диапазону
  • 6.2.5. Преобразование в массив
  • 6.2.6. Обратные диапазоны
  • 6.2.7. Оператор переключения
  • 6.2.8. Нестандартные диапазоны
  • 6.3. Заключение
  • Глава 6. Символы и диапазоны

    Я слышу и забываю. Я вижу и запоминаю. Я делаю и понимаю.

    (Конфуций)

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

    Концепцию символа в Ruby понять непросто. Они напоминают «атомы» в языке Lisp. Вместо того чтобы давать длинное и сложное определение, я расскажу о том, что можно делать с символами и как они применяются. В конце концов, на вопрос «что такое число» можно дать очень глубокомысленный ответ, но нам нужно всего лишь знать, как манипулировать числами.

    Диапазоны проще. Это всего лишь представление множества, заданного конечными точками. Аналогичные конструкции есть в языках Pascal, PHP и даже SQL.

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

    6.1. Символы

    Символ в Ruby — это экземпляр класса

    Symbol
    . Синтаксически он обычно обозначается двоеточием (:), за которым следует идентификатор.

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

    Fixnum
    ). Следовательно, имеет место проблема потребления памяти или производительности, о которой нужно помнить. Например, в нижеприведенном коде строка
    "foo"
    представлена в памяти тремя различными объектами, а символ
    :foo
    — одним, на который есть несколько ссылок:

    array = ["foo", "foo", "foo", :foo, :foo, :foo]

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

    Стоит отметить, что в старых версиях 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)

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

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

    string = "flowery

    str1 = string[0..-2]  # "flower"

    str2 = string[1..-2]  # "lower"

    str3 = string[-5..-3] # "owe" (по существу, прямой диапазон)

    6.2.7. Оператор переключения

    Диапазон в составе условия обрабатывается особым образом. В этом случае

    ..
    называется оператором переключения (flip-flop operator), поскольку это, по существу, переключатель, который сохраняет свое состояние.

    Такой прием, позаимствованный из языка Perl, бывает полезен. Но понять, как он работает, довольно трудно.

    Представьте себе исходный текст программы на Ruby, в который встроена документация, ограниченная маркерами

    =begin
    и
    =end
    . Как бы вы подошли к задаче отыскания и вывода этих и только этих фрагментов? (Состояние переключается между «внутри раздела» и «вне раздела», отсюда и понятие переключения.) Решение, хотя интуитивно и не очевидное, дает следующий код:

    loop do

     break if eof?

     line = gets

     puts line if (line=~/=begin/)..(line=~/=end/)

    end

    «Волшебство» объясняется принципом работы оператора переключения.

    Во-первых, надо осознать, что «диапазон» сохраняет свое состояние, хотя оно и скрыто. Когда становится истинным условие, заданное в качестве левой границы, сам диапазон принимает значение

    true
    . Он сохраняет это состояние до тех пор пока не станет истинным условие на правой границе, и в этот момент состояние переключается в
    false
    .

    Такое поведение полезно во многих случаях, в частности для разбора HTML-документов или конфигурационных файлов, разбитых на разделы, выбора диапазонов элементов из списков и т.д.

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

    Что меня не устраивает в операторе переключения? В контексте предыдущего примера рассмотрим строку, начинающуюся с маркера

    =begin
    . Напомним, что оператор
    =~
    не возвращает
    true
    или
    false
    , как можно было бы ожидать; он возвращает начальную позицию найденного соответствия (
    Fixnum
    ) или
    nil
    , если соответствие не найдено. Следовательно, при вычислении выражений для строк, попадающих и не попадающих в диапазон, мы получаем
    0
    и
    nil
    соответственно.

    Однако при попытке сконструировать диапазон от

    0
    до
    nil
    возникает ошибка, поскольку такой диапазон не имеет смысла:

    range = 0..nil # Ошибка!

    Далее, напомню, что в Ruby только

    false
    и
    nil
    дают значение «ложь» — все остальные объекты в логическом контексте вычисляются как «истина». А значит, следуя общей идеологии диапазон не должен вычисляться как «ложь».

    puts "hello" if x..y

    # Печатается "hello" для любого допустимого диапазона x..y.

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

    true
    .

    loop do

     break if eof?

     line = gets

     start = line=~/=begin/

     stop = line=~/=end/

     puts line if start..stop

    end

    А что если сам диапазон поместить в переменную? Тоже не получится — проверка снова дает

    true
    .

    loop do

     break if eof?

     line = gets

     range = (line=~/=begin/)..(line=~/=end/)

     puts line if range

    end

    Чтобы понять, в чем дело, нужно осознать, что весь диапазон (включая обе границы) вычисляется на каждой итерации цикла, но с учетом внутреннего состояния. Поэтому оператор переключения — вообще не настоящий диапазон. Тот факт, что он выглядит похожим на диапазон, хотя по сути таковым не является, многие считают «злом».

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

    proc
    . Это не значения, а исполняемый код. Тот факт, что нечто, выглядящее как обычное выражение, на самом деле представляет собой
    proc
    , тоже не вызывает восторга.

    И несмотря на все вышесказанное, функциональность-то полезная!.. Можно ли написать класс, который инкапсулирует ее, но при этом не будет таким «магическим»? Можно и даже не очень трудно. В листинге 6.1 приведен простой класс

    Transition
    , имитирующий поведение оператора переключения.

    Листинг 6.1. Класс Transition

    class Transition

     А, В = :А, :В

     T, F = true, false


     # state,p1,p2 => newstate, result

     Table = {[A,F,F]=>[A,F], [B,F,F]=>[B,T],

              [A,T,F]=>[B,T], [B,T,F]=>[B,T],

              [A,F,T]=>[A,F], [B,F,T]=>[A,T],

              [A,T,T]=>[A,T], [B,T,T]=>[A,T]}

     def initialize(proc1, proc2)

      @state = A

      @proc1, @proc2 = proc1, proc2

      check?

     end

     def check?

      p1 = @proc1.call ? T : F

      p2 = @proc2.call ? T : F

      @state, result = *Table[[@state,p1,p2]]

      return result

     end

    end

    В классе

    Transition
    для управления переходами применяется простой конечной автомат. Он инициализируется парой объектов
    proc
    (теми же, что для оператора переключения). Мы утратили небольшое удобство: все переменные (например,
    line
    ), которые используются внутри этих объектов, должны уже находиться в области видимости. Зато теперь у нас есть решение, свободное от «магии», и все выражения ведут себя так, как в любом другом контексте Ruby.

    Вот слегка измененный вариант того же подхода. Здесь метод

    initialize
    принимает
    proc
    и два произвольных выражения:

    def initialize(var,flag1,flag2)

     @state = A

     @proc1 = proc { flag1 === var.call }

     @proc2 = proc { flag2 === var.call }

     check?

    end

    Оператор ветвящегося равенства проверяет соотношение между границами и переменной. Переменная обернута в объект

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

    line = nil

    trans = Transition.new(proc {line}, /=begin/, /=end/)

    loop do break if eof? line = gets

     puts line if trans.check?

    end

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

    6.2.8. Нестандартные диапазоны

    Рассмотрим пример диапазона, состоящего из произвольных объектов. В листинге 6.2 приведен класс для работы с римскими числами.

    Листинг 6.2. Класс для работы с римскими числами

    class Roman

     include Comparable


     I,IV,V,IX,X,XL,L,XC,C,CD,D,CM,M =

      1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000


     Values = %w[M CM D CD С XC L XL X IX V IV I]


     def Roman.encode(value)

      return "" if self == 0

      str = ""

      Values.each do |letters|

       rnum = const_get(letters)

       if value >= rnum

        return(letters + str=encode(value-rnum))

       end

      end

      str

     end


     def Roman.decode(rvalue)

      sum = 0

      letters = rvalue.split('')

      letters.each_with_index do |letter,i|

       this = const_get(letter)

       that = const_get(letters[i+1]) rescue 0

       op = that > this ? :- : :+

       sum = sum.send(op,this)

      end

      sum

     end


     def initialize(value)

      case value

       when String

        @roman = value

        @decimal = Roman.decode(@roman)

       when Symbol

        @roman = value.to_s

        @decimal = Roman.decode(@roman)

       when Numeric

        @decimal = value

        @roman = Roman.encode(@decimal)

      end

     end


     def to_i

      @decimal

     end


     def to_s

      @roman

     end


     def succ

      Roman.new(@decima1 +1)

     end


     def <=>(other)

      self.to_i <=> other.to_i

     end

    end


    def Roman(val)

     Roman.new(val)

    end

    Сначала несколько слов о самом классе. Его конструктору можно передать строку, символ (представляющий число, записанное римскими цифрами) или

    Fixnum
    (число, записанное обычными арабскими цифрами). Внутри выполняется преобразование и сохраняются обе формы. Имеется вспомогательный метод
    Roman
    , это просто сокращенная запись вызова
    Roman.new
    . Методы класса
    encode
    и
    decode
    занимаются преобразованием из арабской формы в римскую и наоборот.

    Для простоты я опустил контроль данных. Кроме того, предполагается, что римские цифры представлены прописными буквами.

    Метод

    to_i
    , конечно же, возвращает десятичное значение, a
    to_s
    — число, записанное римскими цифрами. Метод
    succ
    возвращает следующее римское число: например,
    Roman(:IV).succ
    вернет
    Roman(:V)
    .

    Оператор сравнения сравнивает десятичные эквиваленты. Мы включили с помощью директивы

    include
    модуль
    Comparable
    , чтобы получить доступ к операторам «меньше» и «больше» (реализация которых опирается на наличие метода сравнения
    <=>
    ).

    Обратите внимание на использование символов в следующем фрагменте:

    op = that > this ? :- : :+

    sum = sum.send(op,this)

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

    if that > this

     sum -= this

    else

     sum += this

    end

    Второй вариант длиннее, зато более понятен.

    Поскольку в этом классе есть метод

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

    require 'roman'


    y1 = Roman(:MCMLXVI)

    y2 = Roman(:MMIX)

    range = y1..y2 # 1966..2009

    range.each {|x| puts x}      # Выводятся 44 строки.


    epoch = Roman(:MCMLXX)

    range.include?(epoch)        # true


    doomsday = Roman(2038)

    range.include?(doomsday)     # false


    Roman(:V) == Roman(:IV).succ # true

    Roman(:MCM) < Roman(:MM)     # true

    6.3. Заключение

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

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

    На этом обсуждение символов и диапазонов заканчивается. Но, поскольку они используются в Ruby очень часто (и, несомненно, полезны), то мы еще не раз встретимся в ними в примерах кода.









    Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх