|
||||||||||||||||||||||||||||||||
|
Глава 1. Обзор Ruby
Стоит напомнить, что в новом языке программирования иногда видят панацею, особенно его адепты. Но ни один язык не сможет заменить все остальные. Не существует инструмента, безусловно пригодного для решения любой мыслимой задачи. Есть много предметных областей и много ограничений, налагаемых решаемыми в них задачами. А самое главное — есть разные пути обдумывания задач, и это следствие разного опыта и личных качеств самих программистов. Поэтому в обозримой перспективе будут появляться все новые и новые языки. А пока есть много языков, будет много людей, которые их критикуют и защищают. Короче говоря, «языковым войнам» конца не предвидится, но мы в этой книге не станем принимать в них участия. И тем не менее в постоянном поиске новой, более удачной системы записи программ нас иногда озаряют идеи, переживающие контекст, в котором зародились. Как Pascal многое позаимствовал у Algol, как Java выросла из С, так и каждый язык что-то берет у своих предшественников. Язык - это одновременно набор инструментов и площадка для игр. У него есть практическая сторона, но он же служит и полигоном для испытания новых идей, которые могут быть приняты или отвергнуты сообществом программистов. Одна из наиболее далеко идущих идей — концепция объектно-ориентированного программирования (ООП). Многие скажут, что значимость ООП имеет скорее эволюционный, нежели революционный характер, но никто не возразит против того, что оно оказало огромное влияние на индустрию. Двадцать пять лет назад объектная ориентированность представляла в основном академический интерес; сегодня это универсально принятая парадигма. Вездесущность ООП породила много «рекламной чепухи» в индустрии. В классической работе, написанной в конце 1980-х годов, Роджер Кинг отметил: «Если вы хотите продать кошку специалисту по компьютерам, скажите, что она объектно-ориентированная». Мнения по поводу того, что на самом деле представляет собой ООП, весьма неоднородны, и даже среди тех, кто разделяет общую точку зрения, имеются разногласия относительно терминологии. Мы не ставим себе целью поучаствовать в спорах. Мы согласны, что ООП — полезный инструмент и ценная методология решения задач; мы не утверждаем, что она способна излечить рак. Что касается истинной природы ООП, то у нас есть любимые определения и термины, но мы пользуемся ими лишь для эффективного общения, так что пререкаться по поводу смысла слов не станем. Обо всем этом пришлось сказать лишь потому, что знакомство с основами ООП необходимо для чтения этой книги и понимания примеров и подходов. Что бы ни говорили о Ruby, он безусловно является объектно-ориентированным языком. 1.1. Введение в объектно-ориентированное программированиеПрежде чем начать разговор о самом языке Ruby, неплохо было бы потолковать об объектно-ориентированном программировании вообще. Поэтому сейчас мы вкратце рассмотрим общие идеи, лишь слегка касаясь Ruby. 1.1.1. Что такое объектВ объектно-ориентированном программировании объект — фундаментальное понятие. Объект — это сущность, служащая контейнером для данных и управляющая доступом к этим данным. С объектом связан набор атрибутов, которые в сущности представляют собой просто переменные, принадлежащие объекту. (В этой книге мы будем без стеснения употреблять привычный термин «переменная» в применении к атрибутам.) Кроме того, с объектом ассоциирован набор функций, предоставляющих интерфейс к функциональным возможностям объекта. Эти функции называются методами. Важно отметить, что любой объектно-ориентированный язык предоставляет механизм инкапсуляции. В общепринятом смысле это означает, во-первых, что атрибуты и методы объекта ассоциированы именно с этим объектом, а во-вторых, что область видимости атрибутов и методов по умолчанию ограничена самим объектом (применение принципа сокрытия информации). Объект считается экземпляром класса объекта (обычно он называется просто классом). Класс можно представлять себе как чертеж или образец, а объект — как вещь, изготовленную по этому чертежу. Также класс часто называют абстрактным типом, то есть типом более сложным, нежели целое или строка символов. Создание объекта (экземпляра класса) называют инстанцированием. В некоторых языках имеются явные конструкторы и деструкторы — функции, выполняющие действия, необходимые соответственно для инициализации и уничтожения объекта. Отметим попутно, что в Ruby есть нечто, что можно назвать конструктором, но никакого аналога деструктора не существует (благодаря наличию механизма сборки мусора). Иногда возникает ситуация, когда некоторые данные имеют широкую область видимости, не ограниченную одним объектом, и помещать копию такого атрибута в каждый экземпляр класса неправильно. Рассмотрим, к примеру, класс MyDogsи три объекта этого класса: fido, roverи spot. У каждой собаки могут быть такие атрибуты, как возраст и дата вакцинации. Предположим, однако, что нужно сохранить еще и имя владельца всех собак. Можно, конечно, поместить его в каждый объект, но это пустая трата памяти, к тому же искажающая смысл дизайна. Ясно, что атрибут «имя владельца» принадлежит не отдельному объекту, а классу в целом. Такие атрибуты (синтаксис их определения в разных языках различен) называются атрибутами класса (или переменными класса). Есть немало ситуаций, в которых может понадобиться переменная класса. Допустим, например, что нужно знать, сколько всего было создано объектов некоторого класса. Можно было бы завести переменную класса, инициализировать ее нулем и увеличивать на единицу при создании каждого объекта. Эта переменная ассоциирована именно с классом, а не с каким-то конкретным объектом. С точки зрения области видимости она не отличается от любого другого атрибута, но существует лишь одна ее копия для всего множества объектов данного класса. Чтобы отличить атрибуты класса от обыкновенных атрибутов, последние часто называют атрибутами объекта (или атрибутами экземпляра). Условимся, что в этой книге под словом «атрибут» понимается атрибут экземпляра, если явно не оговорено, что это атрибут класса. Точно так же методы объекта служат для управления доступом к его атрибутам и предоставляют четко определенный интерфейс для этой цели. Но иногда желательно или даже необходимо определить метод, ассоциированный с самим классом. Неудивительно, что метод класса управляет доступом к переменным класса, кроме того, выполняя действия, распространяющиеся на весь класс, а не на какой-то конкретный объект. Как и в случае с атрибутами, мы будем считать, что метод принадлежит объекту, если явно не оговорено противное. Стоит отметить, что в некотором смысле все методы являются методами класса. Не нужно думать, что, создав сто объектов, мы породили сотню копий кода методов! Однако правила ограничения области видимости гласят, что метод каждого объекта оперирует данными только того объекта, от имени которого вызван. Тем самым у нас создается иллюзия, будто методы объекта ассоциированы с самими объектами. 1.1.2. НаследованиеМы подходим к одной из самых сильных сторон ООП — наследованию. Наследование —- это механизм, позволяющий расширять ранее определенную сущность путем добавления новых возможностей. Короче говоря, наследование - это способ повторного использования кода. (Простой и эффективный механизм повторного использования долго был Святым Граалем в информатике. Много десятилетий назад его поиски привели к изобретению параметризованных процедур и библиотек. ООП - лишь одна из последних попыток реализации искомого.) Обычно наследование рассматривается на уровне класса. Если нам необходим какой-то класс, а в наличии имеется более общий, то можно определить свой класс так, чтобы он наследовал поведение уже существующего. Предположим, например, что есть класс Polygon, описывающий выпуклые многоугольники. Тогда класс прямоугольника Rectangleможно унаследовать от Polygon. При этом Rectangleбудет иметь все атрибуты и методы класса Polygon. Так, может уже быть написан метод, вычисляющий периметр путем суммирования длин всех сторон. Если все было реализовано правильно, этот метод автоматически будет работать и для нового класса; переписывать код не придется. Если класс Bнаследует классу A, мы говорим, что Bявляется подклассом A, а A— суперкласс B. По-другому говорят, что А— базовый или родительский класс, а B— производный или дочерний класс. Как мы видели, производный класс может трактовать методы своего базового класса как свои собственные. С другой стороны, он может переопределить метод базового класса, предоставив иную его реализацию. Кроме того, в большинстве языков есть возможность вызвать из переопределенного метода метод базового класса с тем же именем. Иными словами, метод fоокласса Bзнает, как вызвать метод fooкласса A. (Любой язык, не предоставляющий такого механизма, можно заподозрить в отсутствии истинной объектной ориентированности.) То же верно и в отношении атрибутов. Отношение между классом и его суперклассом интересно и важно, обычно его называют отношением «является». Действительно, квадрат Square«является» прямоугольником Rectangle, а прямоугольник Rectangle«является» многоугольником Polygonи т.д. Поэтому, рассматривая иерархию наследования (а такие иерархии в том или ином виде присутствуют в любом объектно-ориентированном языке), мы видим, что в любой ее точке специализированные сущности «являются» подклассами более общих. Отметим, что это отношение транзитивно, — если обратиться к предыдущему примеру, то квадрат «является» многоугольником. Однако отношение «является» не коммутативно — каждый прямоугольник есть многоугольник, но не каждый многоугольник — прямоугольник. Это подводит нас к теме множественного наследования. Можно представить себе класс, который наследует нескольким классам. Например, классы Dog(Собака) и Cat(Кошка) могут наследовать классу Mammal(Млекопитающее), а Sparrow(Воробей) и Raven(Ворон) — классу W ingedCreature(Крылатое). Но как быть с классом Bat(ЛетучаяМышь)? Он с равным успехом может наследовать и Mammal, и WingedCreature! Это хорошо согласуется с нашим жизненным опытом, ведь многие вещи можно отнести не к одной категории, а сразу к нескольким, не вложенным друг в друга. Множественное наследование, вероятно, наиболее противоречивая часть ООП. Некоторые указывают на потенциальные неоднозначности, требующие разрешения. Например, если в обоих классах Mammalи WingedCreatureимеется атрибут size(размер) или метод eat(есть), то какой из них имеется в виду, когда мы обращаемся к нему из объекта класса Bat? С этой трудностью тесно связана проблема ромбовидного наследования; она называется так из-за формы диаграммы наследования, возникающей, когда оба суперкласса наследуют одному классу. Представьте себе, что классы Mammalи WingedCreatureнаследуют общему предку Organism(Организм); тогда иерархия наследования от Organismк Batбудет иметь форму ромба. Но как быть с атрибутами, которые оба промежуточных класса наследуют от своего родителя? Получает ли Batдве копии? Или они должны быть объединены в один атрибут, поскольку все равно заимствованы у общего предка? Это скорее проблемы проектировщика языка, а не программиста. В разных объектно-ориентированных языках они решаются по-разному. Иногда вводятся правила, согласно которым какое-то одно определение атрибута «выигрывает». Либо же предоставляется возможность различать одноименные атрибуты. Иногда даже язык позволяет вводить псевдонимы или переименовывать идентификаторы. Многими это рассматривается как аргумент против множественного наследования — о механизмах разрешения подобных конфликтов имен нет единого мнения, поэтому все они «языкозависимы». В языке C++ предлагается минимальный набор средств для разрешения неоднозначностей; механизмы языка Eiffel, наверное, получше, а в Perl проблема решается совсем по-другому. Есть и альтернатива — полностью запретить множественное наследование. Такой подход принят в языках Java и Ruby. На первый взгляд, это даже не назовешь компромиссным решением, но, вскоре мы убедимся, что все не так плохо, как кажется. Мы познакомимся с приемлемой альтернативой традиционному множественному наследованию, но сначала обсудим полиморфизм — еще одно понятие из арсенала ООП. 1.1.3. ПолиморфизмТермин «полиморфизм», наверное, вызывает самые жаркие семантические споры. Каждый знает, что это такое, но все понимают его по-разному. (Не так давно вопрос «Что такое полиморфизм?» стал популярным во время собеседования при поступлении на работу. Если его зададут вам, рекомендую процитировать какого-нибудь эксперта, например Бертрана Мейера или Бьерна Страуструпа; если собеседник не согласится, то пусть он спорит с классиком, а не с вами.) Буквально слово «полиморфизм» означает «способность принимать разные формы или обличья». В самом широком смысле так называют ситуацию, когда различные объекты по-разному отвечают на одно и то же сообщение или вызов метода. Дамиан Конвей (Damian Conway) в книге «Object-Oriented Perl» проводит смысловое различие между двумя видами полиморфизма. Первый, наследственный полиморфизм, - то, что имеет в виду большинство программистов, говорящих о полиморфизме. Если некоторый класс наследует своему суперклассу, то по определению все методы суперкласса присутствуют также и в подклассе. Таким образом, цепочка наследования представляет собой линейную иерархию классов, отвечающих на одни и те же методы. Нужно, конечно, помнить, что в любом подклассе метод может быть переопределен; именно это и составляет сильную сторону наследования. При вызове метода объекта обычно отвечает либо метод, унаследованный от суперкласса, либо более специализированный вариант этого метода, созданный в интересах именно данного подкласса. В языках со статической типизацией, например в C++, наследственный полиморфизм гарантирует совместимость типов вниз по цепочке наследования (но не в обратном направлении). Скажем, если Bнаследует A, то указатель на объект класса Аможет указывать и на объект класса в; обратное же неверно. Совместимость типов — существенная черта ООП в подобных языках, можно даже сказать, что полиморфизм ей и исчерпывается. Но, конечно же, полиморфизм можно реализовать и в отсутствие статической типизации (как в Ruby). Второй вид полиморфизма, упомянутый Конвеем, — это интерфейсный полиморфизм. Для него не требуется наличия отношения наследования между классами; нужно лишь, чтобы в интерфейсах объектов были методы с одним и тем же именем. Такие объекты можно трактовать как принадлежащие одному виду, и потому мы имеем некую разновидность полиморфизма (хотя в большинстве работ он так не называется). Читатели, знакомые с языком Java, понимают, что в нем реализованы оба вида полиморфизма. Класс в Java может расширять другой класс, наследуя ему с помощью ключевого слова extends, а может с помощью ключевого слова implementsреализовывать интерфейс, за счет чего приобретает заранее известный набор методов (которые необходимо переопределить). Такой синтаксис позволяет интерпретатору Java во время компиляции определить, можно ли вызывать данный метод для конкретного объекта. Ruby поддерживает интерфейсный полиморфизм, но по-другому. Он позволяет определять модули, методы которых допускается «подмешивать» к существующим классам. Но обычно модули так не используются. Модуль состоит из методов и констант, которые можно использовать так, будто они являются частью класса или объекта. Когда модуль подмешивается с помощью предложения include, мы получаем ограниченную форму множественного наследования. (По словам проектировщика языка Юкихиро Мацумото, это можно рассматривать как одиночное наследование с разделением реализации.) Таким образом удается сохранить преимущества множественного наследования, не страдая от его недостатков. 1.1.4. Еще немного терминовВ языках, подобных C++, существует понятие абстрактного класса. Такому классу разрешается наследовать, но создать его экземпляр невозможно. В более динамичном языке Ruby такого понятия нет, но если программист пожелает, то может смоделировать его, потребовав, чтобы все методы были переопределены в производных классах. Полезно это или нет, оставляем на усмотрение читателя. Создатель языка C++ Бьерн Страуструп определяет также понятие конкретного типа. Это класс, существующий только для удобства. Он спроектирован не для наследования; более того, ожидается, что ему никто никогда наследовать не будет. Другими словами, преимущества ООП в этом случае сводятся только к инкапсуляции. Ruby не поддерживает такой конструкции синтаксически (как и C++), но по природе своей прекрасно приспособлен для создания подобных классов. Считается, что некоторые языки поддерживают более «чистую» модель ООП, чем другие. (К ним мы применяем термин «радикально объектно-ориентированный».) Это означает, что любая сущность в языке является объектом, даже примитивные типы представлены полноценными классами, а переменные и константы рассматриваются как экземпляры. В таких языках, как Java, C++ и Eiffel, дело обстоит иначе. В них примитивные типы (особенно константы) не являются настоящими объектами, хотя иногда могут рассматриваться как таковые с помощью «классов-оберток». Вероятно, есть языки, которые более радикально объектно ориентированы, чем Ruby, но их немного. Большинство объектно-ориентированных языков статично; методы и атрибуты, принадлежащие классу, глобальные переменные и иерархия наследования определяются во время компиляции. Быть может, самый сложный концептуальный переход заключается в том, что в Ruby все это происходит динамически. И определения, и даже порядок наследования можно задавать во время исполнения. Честно говоря, каждое объявление или определение исполняется во время работы программы. Помимо прочих достоинств, это позволяет избавиться от условной компиляции, и во многих случаях получается более эффективный код. На этом мы завершаем беглую экскурсию в мир ООП. Мы старались последовательно применять введенные здесь термины на протяжении всей книги. Перейдем теперь к краткому обзору самого языка Ruby. 1.2. Базовый синтаксис и семантика RubyВыше мы отметили, что Ruby — настоящий динамический объектно-ориентированный язык. Прежде чем переходить к обзору синтаксиса и семантики, упомянем некоторые другие его особенности. Ruby — прагматичный (agile) язык. Он пластичен и поощряет частую переработку (рефакторинг), которая выполняется без особого труда. Ruby — интерпретируемый язык. Разумеется, в будущем ради повышения производительности могут появиться и компиляторы Ruby, но мы считаем, что у интерпретатора много достоинств. Он не только позволяет быстро создавать прототипы, но и сокращает весь цикл разработки. Ruby ориентирован на выражения. Зачем писать предложение, когда выражения достаточно? Это означает, в частности, что программа становится более компактной, поскольку общие части выносятся в отдельное выражение и повторения удается избежать. Ruby — язык сверхвысокого уровня (VHLL). Один из принципов, положенных в основу его проектирования, заключается в том, что компьютер должен работать для человека, а не наоборот. Под «плотностью» Ruby понимают тот факт, что сложные, запутанные операции можно записать гораздо проще, чем в языках более низкого уровня. Начнем мы с рассмотрения общего духа языка и некоторых применяемых в нем терминов. Затем вкратце обсудим природу программ на Ruby, а потом уже перейдем к примерам. Прежде всего отметим, что программа на Ruby состоит из отдельных строк, — как в С, но не как в «древних» языках наподобие Фортрана. В одной строке может быть сколько угодно лексем, лишь бы они правильно отделялись пропусками. В одной строке может быть несколько предложений, разделенных точками с запятой; только в этом случае точка с запятой и необходима. Логическая строка может быть разбита на несколько физических при условии, что все, кроме последней, заканчиваются обратной косой чертой или лексическому анализатору дан знак, что предложение еще не закончено. Таким знаком может, например, быть запятая в конце строки. Главной программы как таковой (функции main) не существует; исполнение происходит сверху вниз. В более сложных программах в начале текста могут располагаться многочисленные определения, за которыми следует (концептуально) главная программа. Но даже в этом случае программа исполняется сверху вниз, так как в Ruby все определения исполняются. 1.2.1. Ключевые слова и идентификаторыКлючевые (или зарезервированные) слова в Ruby обычно не применяются ни для каких иных целей. Вот их полный перечень: BEGIN END alias and begin break case class def defined? do else elsif end ensure false for if in module next nil not or redo rescue retry return self super then true undef unless until when while yield Имена переменных и других идентификаторов обычно начинаются с буквы или специального модификатора. Основные правила таковы: • имена локальных переменных (и таких псевдопеременных, как selfи nil) начинаются со строчной буквы или знака подчеркивания _; • имена глобальных переменных начинаются со знака доллара $; • имена переменных экземпляра (принадлежащих объекту) начинаются со знака «собачки» @; • имена переменных класса (принадлежащих классу) предваряются двумя знаками @( @@); • имена констант начинаются с прописной буквы; • в именах идентификаторов знак подчеркивания _ можно использовать наравне со строчными буквами; • имена специальных переменных, начинающиеся со знака доллара (например, $1и $/), здесь не рассматриваются. Примеры: • локальные переменные alpha, _ident, some_var; • псевдопеременные self, nil, __FILE__; • константы K6chip, Length, LENGTH; • переменные экземпляра @foobar, @thx1138, @not_const; • переменные класса @@phydeaux, @@my_var, @@not_const; • глобальные переменные $beta, $B2vitamin, $not_const. 1.2.2. Комментарии и встроенная документацияКомментарии в Ruby начинаются со знака решетки ( #), находящегося вне строки или символьной константы, и продолжаются до конца строки: x = y + 5 # Это комментарий. # Это тоже комментарий. print "# А это не комментарий." Предполагается, что встроенная документация будет извлечена из программы каким-нибудь внешним инструментом. С точки зрения интерпретатора это обычный комментарий. Весь текст, расположенный между строками, которые начинаются с лексем =beginи =end(включительно), игнорируется интерпретатором (этим лексемам не должны предшествовать пробелы). =begin Назначение этой программы - излечить рак и установить мир во всем мире. =end 1.2.3. Константы, переменные и типыВ Ruby переменные не имеют типа, однако объекты, на которые переменные ссылаются, тип имеют. Простейшие типы — это символ, число и строка. Числовые константы интуитивно наиболее понятны, равно как и строки. В общем случае строка, заключенная в двойные кавычки, допускает интерполяцию выражений, а заключенная в одиночные кавычки интерпретируется почти буквально — в ней распознается только экранированная обратная косая черта. Ниже показана «интерполяция» переменных и выражений в строку, заключенную в двойные кавычки: а = 3 b = 79 puts "#{а} умноженное на #{b} = #{а*b}" # 3 умноженное на 79 = 237 Более подробная информация о литералах (числах, строках, регулярных выражениях и т.п.) приведена в следующих главах. Стоит упомянуть особую разновидность строк, которая полезна прежде всего в небольших сценариях, применяемых для объединения более крупных программ. Строка, выводимая программой, посылается операционной системе в качестве подлежащей исполнению команды, а затем результат выполненной команды подставляется обратно в строку. В простейшей форме для этого применяются строки, заключенные в обратные кавычки. В более сложном варианте используется синтаксическая конструкция %x: `whoami` `ls -l` %x[grep -i meta *.html | wc -l] Регулярные выражения в Ruby похожи на символьные строки, но используются по-другому. Обычно в качестве ограничителя выступает символ косой черты. Синтаксис регулярных выражений в Ruby и Perl имеет много общего. Подробнее о регулярных выражениях см. главу 3. Массивы в Ruby — очень мощная конструкция; они могут содержать данные любого типа. Более того, в одном массиве можно хранить данные разных типов. В главе 8 мы увидим, что все массивы — это экземпляры класса Array, а потому к ним применимы разнообразные методы. Массив-константа заключается в квадратные скобки. Примеры: [1, 2, 3] [1, 2, "застегни мне молнию на сапоге"] [1, 2, [3,4], 5] ["alpha", "beta", "gamma", "delta"] Во втором примере показан массив, содержащий целые числа и строки. В третьем примере мы видим вложенный массив, а в четвертом - массив строк. Как и в большинстве других языков, нумерация элементов массива начинается с нуля. Так, в последнем из показанных выше примеров элемент "gamma"имеет индекс 2. Все массивы динамические, задавать размер при создании не нужно. Поскольку массивы строк встречаются очень часто (а набирать их неудобно), для них предусмотрен специальный синтаксис: %w[alpha beta gamma delta] %w(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec) %w/am is are was were be being been/ Здесь не нужны ни кавычки, ни запятые; элементы разделяются пробелами. Если встречаются элементы, содержащие внутренние пробелы, такой синтаксис, конечно, неприменим. Для доступа к конкретному элементу массива по индексу применяются квадратные скобки. Результирующее выражение можно получить или выполнить для него присваивание: val = myarray[0] print stats[j] x[i] = x[i+1] Еще одна «могучая» конструкция в Ruby — это хэш. Его также называют ассоциативным массивом или словарем. Хэш — это множество пар данных; обыкновенно он применяется в качестве справочной таблицы или как обобщенный массив, в котором индекс не обязан быть целым числом. Все хэши являются экземплярами класса Hash. Хэш-константа, как правило, заключается в фигурные скобки, а ключи отделяются от значений символом =>. Ключ можно считать индексом для доступа к ассоциированному с ним значению. На типы ключей и значений не налагается никаких ограничений. Примеры: {1=>1, 2=>4, 3=>9, 4=>16, 5 = >25, 6=>36} {"cat"=>"cats", "ox"=>"oxen", "bacterium"=>"bacteria"} {"водород"=>1, "гелий"=>2, "углерод"=>12} {"нечетные"=>[1,3,5,7], "четные"=>[2,4,6,8]} {"foo"=>123, [4,5,6]=>"my array", "867-5309"=>"Jenny"} К содержимому хэша-переменной доступ осуществляется так же, как для массивов, — с помощью квадратных скобок: print phone_numbers["Jenny"] plurals["octopus"] = "octopi" Однако следует подчеркнуть, что у массивов и хэшей много методов, именно они и делают эти контейнеры полезными. Ниже, в разделе «ООП в Ruby», мы раскроем эту тему более подробно. 1.2.4. Операторы и приоритетыПознакомившись с основными типами данных, перейдем к операторам в языке Ruby. В приведенном ниже списке они представлены в порядке убывания приоритета: ::Разрешение области видимости []Взятие индекса **Возведение в степень + - ! ~Унарный плюс/минус, НЕ… * / %Умножение, деление… + -Сложение/вычитание << >>Логические сдвиги… &Поразрядное И || ^Поразрядное ИЛИ, исключающее ИЛИ > >= < <=Сравнение == === <=> != =~ !~Равенство, неравенство… &&Логическое И ||Логическое ИЛИ .. ...Операторы диапазона = (also +=, -=, …)Присваивание ?:Тернарный выбор notЛогическое отрицание and orЛогическое И, ИЛИ Некоторые из перечисленных символов служат сразу нескольким целям. Например, оператор <<обозначает поразрядный сдвиг влево, но также применяется для добавления в конец (массива, строки и т.д.) и как маркер встроенного документа. Аналогично знак +означает сложение чисел и конкатенацию строк. Ниже мы увидим, что многие операторы — это просто сокращенная запись вызова методов. Итак, мы определили большую часть типов данных и многие из возможных над ними операций. Прежде чем двигаться дальше, приведем пример программы. 1.2.5. Пример программыВ любом руководстве первой всегда приводят программу, печатающую строку Hello, world!, но мы рассмотрим что-нибудь более содержательное. Вот небольшая интерактивная консольная программа, позволяющая переводить температуру из шкалы Фаренгейта в шкалу Цельсия и наоборот. print "Введите температуру и шкалу (С or F): " str = gets exit if str.nil? or str.empty? str.chomp! temp, scale = str.split(" ") abort "#{temp} недопустимое число." if temp !~ /-?\d+/ temp = temp.to_f case scale when "С", "с" f = 1.8*temp + 32 when "F", "f" с = (5.0/9.0)*(temp-32) else abort "Необходимо задать С или F." end if f.nil? print "#{c} градусов C\n" else print "#{f} градусов F\n" end Ниже приведены примеры прогона этой программы. Показано, как она переводит градусы Фаренгейта в градусы Цельсия и наоборот, а также как обрабатывает неправильно заданную шкалу или число: Введите температуру и шкалу (С or F): 98.6 F 37.0 градусов С Введите температуру и шкалу (С or F): 100 С 212.0 градусов F Введите температуру и шкалу (С or F): 92 G Необходимо задать С или F. Введите температуру и шкалу (С or F): junk F junk недопустимое число. Теперь рассмотрим, как эта программа работает. Все начинается с предложения Kernel. Данный метод выполняет печать на стандартный вывод. Это самый простой способ оставить курсор в конце строки. Далее мы вызываем метод gets(прочитать строку из стандартного ввода) и присваиваем полученное значение переменной str. Для удаления хвостового символа новой строки вызывается метод chomp!. Обратите внимание, что gets, которые выглядят как «свободные» функции, на самом деле являются методами класса Object(который, вероятно, наследует Kernel). Точно так же chomp!— метод, вызываемый от имени объекта str. При вызовах методов в Ruby обычно можно опускать скобки: print "foo"и print("foo")— одно и то же. В переменной strхранится символьная строка, но могли бы храниться данные любого другого типа. В Ruby данные имеют тип, а переменные - нет. Переменная начинает существовать, как только интерпретатор распознает присваивание ей; никаких предварительных объявлений не существует. Метод exitзавершает программу. В той же строке мы видим управляющую конструкцию, которая называется «модификатор if». Он аналогичен предложению if, существующему в большинстве языков, только располагается после действия. Для модификатора ifнельзя задать ветвь else, и он не требует закрытия. Что касается условия, мы проверяем две вещи: имеет ли переменная strзначение (то есть не равна nil) и не является ли она пустой строкой. Если встретится конец файла, то будет истинно первое условие; если же пользователь нажмет клавишу Enter, не введя никаких данных, — второе. Это предложение можно было бы записать и по-другому: exit if not str or not str[0] Эти проверки работают потому, что переменная может иметь значение nil, а nilв Ruby в логическом контексте вычисляется как «ложно». На самом деле как «ложно» вычисляются nilи false, а все остальное — как «истинно». Это означает, кстати, что пустая строка ""и число 0 — не «ложно». В следующем предложении над строкой выполняется операция chomp!(для удаления хвостового символа новой строки). Восклицательный знак в конце предупреждает, что операция изменяет значение самой строки, а не возвращает новую. Восклицательный знак применяется во многих подобных ситуациях как напоминание программисту о том, что у метода есть побочное действие или что он более «опасен», чем аналогичный метод без восклицательного знака. Так, метод chompвозвращает такой же результат, но не модифицирует значение строки, для которой вызван. В следующем предложении мы видим пример множественного присваивания. Метод splitразбивает строку на куски по пробелам и возвращает массив. Двум переменным в левой части оператора присваиваются значения первых двух элементов массива в правой части. В следующем предложении ifс помощью простого регулярного выражения выясняется, введено ли допустимое число. Если строка не соответствует образцу, который состоит из необязательного знака «минус» и одной или более цифр, то число считается недопустимым и программа завершается. Отметим, что предложение if оканчивается ключевым словом end. Хотя в данном случае это не нужно. Мы могли бы включить перед endветвь else. Ключевое слово thenнеобязательно; в этой книге мы стараемся не употреблять его. Метод to_fпреобразует строку в число с плавающей точкой. Это число записывается в ту же переменную temp, в которой раньше хранилась строка. Предложение caseвыбирает одну из трех ветвей: пользователь указал С, Fили какое-то другое значение в качестве шкалы. В первых двух случаях выполняется вычисление, в третьем мы печатаем сообщение об ошибке и выходим. Кстати, предложение caseв Ruby позволяет гораздо больше, чем показано в примере. Нет никаких ограничений на типы данных, а все выражения могут быть произвольно сложными, в том числе диапазонами или регулярными выражениями. В самом вычислении нет ничего интересного. Но обратите внимание, что переменные с и fвпервые встречаются внутри ветвей case. В Ruby нет никаких объявлений — переменная начинает существовать только в результате присваивания. А это означает, что после выхода из caseлишь одна из переменных elifбудет иметь действительное значение. Мы воспользовались этим фактом, чтобы понять, какая ветвь исполнялась, и в зависимости от этого вывести то или другое сообщение. Сравнение fс nilпозволяет узнать, есть ли у переменной осмысленное значение. Этот прием применен только для демонстрации возможности: ясно, что при желании можно было бы поместить печать прямо внутрь предложения case. Внимательный читатель заметит, что мы пользовались только «локальными» переменными. Это может показаться странным, так как, на первый взгляд, их областью видимости является вся программа. На самом деле они локальны относительно верхнего уровня программы. Глобальными они кажутся лишь потому, что в этой простой программе нет контекстов более низкого уровня. Но если бы мы объявили какие-нибудь классы или методы, то в них переменные верхнего уровня были бы не видны. 1.2.6. Циклы и ветвлениеПотратим немного времени на изучение управляющих конструкций. Мы уже видели простое предложение ifи модификатор if. Существуют также парные структуры, в которых используется ключевое слово unless(в них также может присутствовать необязательная ветвь else), а равно применяемые в выражениях формы ifи unless. Все они сведены в таблицу 1.1. Таблица 1.1. Условные предложения
Здесь формы с ключевыми словами ifи unless, расположенные в одной строке, выполняют в точности одинаковые функции. Обратите внимание, что слово thenможно опускать во всех случаях, кроме последнего (предназначенного для использования в выражениях). Также заметьте, что в модификаторах (третья строка) ветви elseбыть не может. Предложение caseв Ruby позволяет больше, чем в других языках. В его ветвях можно проверять различные условия, а не только сравнивать на равенство. Так, например, разрешено сопоставление с регулярным выражением. Проверки в предложении caseэквивалентны оператору ветвящегося равенства ( ===), поведение которого зависит от объекта. Рассмотрим пример: case "Это строка символов." when "одно значение" puts "Ветвь 1" when "другое значение" puts "Ветвь 2" when /симв/ puts "Ветвь 3" else puts "Ветвь 4" end Этот код напечатает Ветвь 3. Почему? Сначала проверяемое выражение сравнивается на равенство с двумя строками: "одно значение"и "другое значение". Эта проверка завершается неудачно, поэтому мы переходим к третьей ветви. Там находится образец, с которым сопоставляется выражение. Поскольку оно соответствует образцу, то выполняется предложение elseобрабатывается случай, когда ни одна из предшествующих проверок не прошла. Если проверяемое выражение — целое число, то его можно сравнивать с целочисленным диапазоном (например, 3..8); тогда проверяется, что число попадает в диапазон. В любом случае выполняется код в первой подошедшей ветви. В Ruby имеется богатый набор циклических конструкций. К примеру, whileи until— циклы с предварительной проверкой условия, и оба работают привычным образом: в первом случае задается условие продолжения цикла, а во втором — условие завершения. Есть также их формы с модификатором, как для предложений ifи unless. Кроме того, в модуле Kernelесть метод loop(по умолчанию бесконечный цикл), а в некоторых классах реализованы итераторы. В примерах из таблицы 1.2 предполагается, что где-то определен такой массив list: list = %w[alpha bravo charlie delta echo]; В цикле этот массив обходится и печатается каждый его элемент. Таблица 1.2. Циклы
Рассмотрим эти примеры более подробно. Циклы 1 и 2 — «стандартные» формы циклов whileи until; ведут они себя практически одинаково, только условия противоположны. Циклы 3 и 4 — варианты предыдущих с проверкой условия в конце, а не в начале итерации. Отметим, что использование слов beginи endв этом контексте — просто грязный трюк; на самом деле это был бы блок begin/end (применяемый для обработки исключений), за которым следует модификатор whileили until. Однако для тех, кто желает написать цикл с проверкой в конце, разницы нет. На мой взгляд, конструкции 3 и 4 — самый «правильный» способ кодирования циклов. Они заметно проще всех остальных: нет ни явной инициализации, ни явной проверки или инкремента. Это возможно потому, что массив «знает» свой размер, а стандартный итератор each(цикл 6) обрабатывает такие детали автоматически. На самом деле в цикле 5 производится неявное обращение к этому итератору, поскольку цикл forработает с любым объектом, для которого определен итератор each. Цикл for— лишь сокращенная запись для вызова each; часто такие сокращения называют «синтаксической глазурью», имея в виду, что это не более чем удобная альтернативная форма другой синтаксической конструкции. В циклах 5 и 6 используется конструкция loop. Выше мы уже отмечали, что хотя loopвыглядит как ключевое слово, на самом деле это метод модуля Kernel, а вовсе не управляющая конструкция. В циклах 7 и 8 используется тот факт, что у массива есть числовой индекс. Итератор timesисполняется заданное число раз, а итератор uptoувеличивает свой параметр до заданного значения. И тот, и другой для данной ситуации приспособлены плохо. Цикл 9 — это вариант цикла for, предназначенный специально для работы со значениями индекса при помощи указания диапазона. В цикле 10 мы пробегаем весь диапазон индексов массива с помощью итератора each_index. В предыдущих примерах мы уделили недостаточно внимания вариантам циклов whileи loopс модификаторами. Они довольно часто используются из-за краткости. Вот еще два примера, в которых делается одно и то же: perform_task() until finished perform_task() while not finished Также из таблицы 1.2 осталось неясным, что циклы не всегда выполняются от начала до конца. Число итераций не всегда предсказуемо. Нужны дополнительные средства управления циклами. Первое из них — ключевое слово break, встречающееся в циклах 5 и 6. Оно позволяет досрочно выйти из цикла; в случае вложенных циклов происходит выход из самого внутреннего. Для программистов на С это интуитивно очевидно. Ключевое слово retryприменяется в двух случаях: в контексте итератора и в контексте блока begin-end(обработка исключений). В теле итератора (или цикла for) оно заставляет итератор заново выполнить инициализацию, то есть повторно вычислить переданные ему аргументы. Отметим, что к циклам общего вида это не относится. Ключевое слово redo— обобщение retryна циклы общего вида. Оно работает в циклах whileи until, как retryв итераторах. Ключевое слово nextосуществляет переход на конец самого внутреннего цикла и возобновляет исполнение с этой точки. Работает для любого цикла и итератора. Как мы только что видели, итератор — важное понятие в Ruby. Но следует отметить, что язык позволяет определять и пользовательские итераторы, не ограничиваясь встроенными. Стандартный итератор для любого объекта называется each. Это существенно отчасти из-за того, что позволяет использовать цикл for. Но итераторам можно давать и другие имена и применять для разных целей. В качестве примера рассмотрим многоцелевой итератор, который имитирует цикл с проверкой условия в конце (как в конструкции do-whileв С или repeat-untilв Pascal): def repeat(condition) yield retry if not condition end В этом примере ключевое слово yieldслужит для вызова блока, который задается при таком вызове итератора: j=0 repeat (j >= 10) do j += 1 puts j end С помощью yieldможно также передать параметры, которые будут подставлены в список параметров блока (между вертикальными черточками). В следующем искусственном примере итератор всего лишь генерирует целые числа от 1 до 10, а вызов итератора порождает кубические степени этих чисел: def my_sequence for i in 1..10 do yield i end end my_sequence {|x| puts x**3 } Отметим, что вместо фигурных скобок, в которые заключен блок, можно написать ключевые слова doи end. Различия между этими формами есть, но довольно тонкие. 1.2.7. ИсключенияКак и многие другие современные языки, 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будет перехватывать все исключения, производные от класса S tandardError. В конструкции 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, а наличие двоеточия перед идентификатором превращало его в целое число. Если вы пользуетесь таким трюком, не предполагайте, что фактическое значение символа будет неизменным или предсказуемым - просто используйте его как константу, значение которой неважно. 1.3.3. Модули и классы-примесиМногие встроенные методы наследуются от классов-предков. Особо стоит отметить методы модуля 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для того, чтобы воспользоваться каким-то внешним модулем. 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 # Ошибка! Имейте в виду, что метод определяется для объекта, а не для переменной. Теоретически с помощью синглетных методов можно было бы создать систему объектов на базе прототипов. Это менее распространенная форма ООП без классов[5]. Основной структурный механизм в ней состоит в конструировании нового объекта путем использования существующего в качестве образца; новый объект ведет себя как старый за исключением тех особенностей, которые были переопределены. Тем самым можно строить системы на основе прототипов, а не наследования. Хотя у нас нет опыта в этой области, мы полагаем, что создание такой системы позволило бы полнее раскрыть возможности Ruby. 1.4. Динамические аспекты RubyRuby — динамический язык в том смысле, что объекты и классы можно изменять во время выполнения. 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 action1 end elsif platform == Linux def my_action action2 end else def my_action default_action end end Таким способом мы достигаем желаемого результата, но условие вычисляется только один раз. Когда программа вызовет метод my_action, он уже будет правильно определен. 1.4.2. ОтражениеВ языках Smalltalk, LISP и Java реализована (с разной степенью полноты) идея рефлексивного программирования — активная среда может опрашивать структуру объектов и расширять либо модифицировать их во время выполнения. В языке Ruby имеется развитая поддержка отражения, но все же он не заходит так далеко, как Smalltalk, где даже управляющие конструкции являются объектами. В Ruby управляющие конструкции и блоки не представляют собой объекты. (Объект Procможно использовать для того, чтобы представить блок в виде объекта, но управляющие конструкции объектами не бывают никогда.) Для определения того, используется ли идентификатор с данным именем, служит ключевое слово defined?(обратите внимание на вопросительный знак в конце слова): if defined? some_var puts "some_var = #{some_var}" else puts "Переменная some_var неизвестна." end Аналогично метод respond_to?выясняет, может ли объект отвечать на вызов указанного метода (то есть определен ли данный метод для данного объекта). Метод respond_to?определен в классе Object. В Ruby запрос информации о типе во время выполнения поддерживается очень полно. Тип или класс объекта можно определить, воспользовавшись методом type(из класса Object). Метод is_a?сообщает, принадлежит ли объект некоторому классу (включая и его суперклассы); синонимом служит имя kind_of?. Например: puts "abc".class "" # Печатается String puts 345.class # Печатается Fixnum rover = Dog.new print rover.class # Печатается Dog if rover.is_a? Dog puts "Конечно, является." end if rover.kind_of? Dog puts "Да, все еще собака." end if rover.is_a? Animal puts "Да, он к тому же и животное." end Можно получить полный список всех методов, которые можно вызвать для данного объекта. Для этого предназначен метод methodsиз класса Object. Имеются также его варианты private_instance_methods, public_instance_methodsи т.д. Аналогично можно узнать, какие переменные класса или экземпляра ассоциированы с данным объектом. По самой природе ООП в перечни методов и переменных включаются те, что определены как в классе самого объекта, так и во всех его суперклассах. В классе Moduleимеется метод constants, позволяющий получить список всех констант, определенных в модуле. В классе Moduleесть метод ancestors, возвращающий список модулей, включенных в данный модуль. В этот список входит и сам данный модуль, то есть список, возвращаемый вызовом Mod.ancestors, содержит по крайней мере элемент Mod. В этот список входят не только родительские классы (отобранные в силу наследования), но и «родительские» модули (отобранные в силу включения). В классе Objectесть метод superclass, который возвращает суперкласс объекта или nil. Не имеет суперкласса лишь класс Object, и, значит, только для него может быть возвращен nil. Модуль ObjectSpaceприменяется для получения доступа к любому «живому» объекту. Метод _idtorefпреобразует идентификатор объекта в ссылку на него; можно считать, что это операция, обратная той, что выполняет двоеточие в начале имени. В модуле ObjectSpaceесть также итератор each_object, который перебирает все существующие в данный момент объекты, включая и те, о которых иным образом узнать невозможно. (Напомним, что некоторые неизменяемые объекты небольшого размера, например принадлежащие классам Fixnum, NilClass, TrueClassи FalseClass, не хранятся в куче из соображений оптимизации.) 1.4.3. Отсутствующие методыПри вызове метода ( myobject.mymethod) Ruby ищет поименованный метод в следующем порядке: 1. Синглетные методы, определенные для объекта myobject. 2. Методы, определенные в классе объекта myobject. 3. Методы, определенные в предках класса объекта myobject. Если найти метод mymethodне удается, Ruby ищет метод с именем method_missing. Если он определен, то ему передается имя отсутствующего метода (в виде символа) и все переданные ему параметры. Этот механизм можно применять для динамической обработки неизвестных сообщений, посланных во время выполнения. 1.4.4 Сборка мусораУправлять памятью на низком уровне трудно и чревато ошибками, особенно в таком динамичном окружении, какое создает Ruby. Наличие механизма сборки мусора — весомое преимущество. В таких языках, как C++, за выделение и освобождение памяти отвечает программист. В более поздних языках, например Java, память освобождается сборщиком мусора (когда объект покидает область видимости). Явное управление памятью может приводить к двум видам ошибок. Если освобождается память, занятая объектом, на который еще есть ссылки, то при последующем доступе к нему объект может оказаться в противоречивом состоянии. Так называемые висячие указатели трудно отлаживать, поскольку вызванные ими ошибки часто проявляются далеко от места возникновения. Утечка памяти имеет место, когда не освобождается объект, на который больше никто не ссылается. В этом случае программа потребляет все больше и больше памяти и в конечном счете аварийно завершается; такие ошибки искать тоже трудно. В языке Ruby для отслеживания неиспользуемых объектов и освобождения занятой ими памяти применяется механизм сборки мусора. Для тех, кто в этом разбирается, отметим, что в Ruby используется алгоритм пометки и удаления, а не подсчета ссылок (у последнего возникают трудности при обработке рекурсивных структур). Сборка мусора влечет за собой некоторое снижение производительности. Модуль GC предоставляет ограниченные средства управления, позволяющие программисту настроить его работу в соответствии с нуждами конкретной программы. Можно также определить чистильщика (finalizer) объекта, но это уже тема для «продвинутых» (см. раздел 11.3.14). 1.5. Потренируйте свою интуицию: что следует запомнитьНадо честно признаться: «все становится интуитивно ясным после того, как поймешь». Эта истина и составляет суть данного раздела, поскольку в Ruby немало особенностей, отличающих его от всего, к чему привык программист на одном из традиционных языков. Кто-то из читателей решит, что не нужно зря тратить время на повторение известного. Если вы из их числа, можете пропустить разделы, содержание которых кажется вам очевидным. Программисты имеют неодинаковый опыт; искушенные пользователи С и Smalltalk воспримут Ruby совсем по-разному. Впрочем, мы надеемся, что внимательное прочтение последующих разделов поможет многим читателям разобраться в том, что же такое Путь Ruby. 1.5.1. СинтаксисСинтаксический анализатор Ruby сложен и склонен прощать многие огрехи. Он пытается понять, что хотел сказать программист, а не навязывать ему жесткие правила. Но к такому поведению надо еще привыкнуть. Вот перечень того, что следует знать о синтаксисе Ruby. • Скобки при вызове методов, как правило, можно опускать. Все следующие вызовы допустимы: foobar foobar() foobar(a,b,c) foobar a, b, с • Коль скоро скобки необязательны, что означает такая запись: x у z? Оказывается, вот что: «Вызвать метод y, передав ему параметр z, а результат передать в виде параметра методу x.» Иными словами, x(y(z)). Это поведение в будущем изменится. См. обсуждение поэтического режима в разделе 1.6 ниже. • Попробуем передать методу хэш: my_method {а=>1, b=>2} Это приведет к синтаксической ошибке, поскольку левая фигурная скобка воспринимается как начало блока. В данном случае скобки необходимы: my_method({а=>1, b=>2}) • Предположим теперь, что хэш — единственный (или последний) параметр метода. Ruby снисходительно разрешает опускать фигурные скобки: my_method(а=>1, b=>2) Кто-то увидит здесь вызов метода с именованными параметрами. Это обманчивое впечатление, хотя никто не запрещает применять подобную конструкцию и в таком смысле. • Есть и другие случаи, когда пропуски имеют некоторое значение. Например, на первый взгляд все четыре выражения ниже означают одно и то же: x = y + z x = y+z x = y+ z x = y +z Но фактически эквивалентны лишь первые три. В четвертом же случае анализатор считает, что вызван метод ус параметром +z! И выдаст сообщение об ошибке, так как метода с именем уне существует. Мораль: пользуйтесь пробелами разумно. • Аналогично x = y*z— это умножение уна z, тогда как x = y *z— вызов метода у, которому в качестве параметра передается расширение массива z. • В именах идентификаторов знак подчеркивания _считается строчной буквой. Следовательно, имя идентификатора может начинаться с этого знака, но такой идентификатор не будет считаться константой, даже если следующая буква прописная. • В линейной последовательности вложенных предложений ifприменяется ключевое слово elsif, а не else ifили elif, как в некоторых других языках. • Ключевые слова в Ruby нельзя назвать по-настоящему зарезервированными. Если метод вызывается от имени некоторого объекта (и в других случаях, когда не возникает неоднозначности), имя метода может совпадать с ключевым словом. Но поступайте так с осторожностью, не забывая, что программу будут читать люди. • Ключевое слово then(в предложениях ifи case) необязательно. Если вам кажется, что с ним программа понятнее, включайте его в код. То же относится к слову doв циклах whileи until. • Вопросительный и восклицательный знаки не являются частью идентификатора, который модифицируют, — их следует рассматривать как суффиксы. Таким образом, хотя идентификаторы chopи chop!считаются различными, использовать восклицательный знак в любом другом месте имени не разрешается. Аналогично в Ruby есть конструкция defined?, но defined— ключевое слово. • Внутри строки символ решетки #— признак начала выражения. Значит, в некоторых случаях его следует экранировать обратной косой чертой, но лишь тогда, когда сразу за ним идет символ {, $или @. • Поскольку вопросительный знак можно добавлять в конец идентификатора, то следует аккуратно расставлять пробелы в тернарном операторе. Пусть, например, имеется переменная my_flag, которая может принимать значения trueили false. Тогда первое из следующих предложений правильно, а второе содержит синтаксическую ошибку: x = my_flag ? 23 : 45 # Правильно. x = my_flag? 23 : 45 # Синтаксическая ошибка. • Концевой маркер для встроенной документации не следует считать лексемой. Он помечает строку целиком, поэтому все находящиеся в той же строке символы не являются частью текста программы, а принадлежат встроенному документу. • В Ruby нет произвольных блоков, то есть нельзя начать блок в любом месте, как в С. Блоки разрешены только там, где они нужны, — например, могут присоединяться к итератору. Исключение составляет блок begin-end, который можно употреблять практически везде. • Не забывайте, что ключевые слова BEGINи ENDне имеют ничего общего с beginи end. • При статической конкатенации строк приоритет конкатенации ниже, чем у вызова метода. Например: str = "Первая " 'second'.center(20) # Примеры 1 and 2 str = "Вторая " + 'second'.center(20) # дают одно и то же. str = "Первая вторая".center(20) # Примеры 3 and 4 str = ("Первая " + 'вторая').center(20) # дают одно и то же. • В Ruby есть несколько псевдопеременных, которые выглядят как локальные переменные, но применяются для особых целей. Это self, nil, true, false, __FILE__и __LINE__. 1.5.2. Перспективы программированияНаверное, каждый, кто знает Ruby (сегодня), в прошлом изучал или пользовался другими языками. Это, с одной стороны, облегчает изучение Ruby, так как многие средства похожи на аналогичные средства в других языках. С другой стороны, у программиста может возникнуть ложное чувство уверенности при взгляде на знакомые конструкции Ruby. Он может прийти к неверным выводам, основанным на прошлом опыте; можно назвать это явление «багажом эксперта». Немало специалистов переходит на Ruby со Smalltalk, Perl, C/C++ и других языков. Ожидания этих людей сильно различаются, но так или иначе присутствуют. Поэтому рассмотрим некоторые вещи, на которых многие спотыкаются. • Символ в Ruby представляется целым числом. Это не самостоятельный тип, как в Pascal, и не эквивалент строки длиной 1. В ближайшем будущем положение изменится и символьная константа станет строкой, но на момент написания данной книги этого еще не произошло. Рассмотрим следующий фрагмент: x = "Hello" y = ?А puts "x[0] = #{x[0]}" # Печатается x[0] = 72 puts "y = #{y}" # Печатается y = 65 if y == "А" # Печатается no puts "yes" else puts "no" end • He существует булевского типа. TrueClassи FalseClass— это два разных класса, а единственными их экземплярами являются объекты trueи false. • Многие операторы в Ruby напоминают операторы в языке С. Два заметных исключения — операторы инкремента и декремента ( ++и --). Их в Ruby нет ни в «пост», ни в «пред» форме. • Известно, что в разных языках оператор деления по модулю работает по-разному для отрицательных чисел. Не вдаваясь в споры о том, что правильно, проиллюстрируем поведение в Ruby: puts (5 % 3) # Печатается 2 puts (-5 % 3) # Печатается 1 puts (5 % -3) # Печатается -1 puts (-5 % -3) # Печатается -2 • Некоторые привыкли думать, что «ложь» можно представлять нулем, пустой строкой, нулевым символом и т.п. Но в Ruby все это равно «истине». На самом деле истиной будет все кроме объектов falseи nil. • В Ruby переменные не принадлежат никакому классу: класс есть только у значений. • Переменные в Ruby не объявляются, однако считается хорошим тоном присваивать переменной начальное значение nil. Разумеется, при этом с переменной не ассоциируется никакой тип и даже не происходит истинной инициализации, но анализатор знает, что данное имя принадлежит переменной, а не методу. • ARGV[0]— первый аргумент в командной строке; они нумеруются начиная с нуля. Это не имя файла или сценария, предшествующего параметрам, как argv[0]в языке С. • Большинство операторов в Ruby на самом деле является методами; их запись в виде «знаков препинания» — не более чем удобство. Первое исключение из этого правила — набор операторов составного присваивания ( +=, -=и т.д.). Второе исключение - операторы =, .., ..., !, not, &&, and, ||, or, !=, !~. • Как и в большинстве современных языков программирования (хотя и не во всех), булевские операции закорачиваются, то есть вычисление булевского выражения заканчивается, как только его значение истинности становится известным. В последовательности операций orвычисление заканчивается, когда получено первое значение true, а в последовательности операций and— когда получено первое значение false. • Префикс @@применяется для переменных класса (то есть ассоциированных с классом в целом, а не с отдельным экземпляром). • loop— не ключевое слово. Это метод модуля Kernel, а не управляющая конструкция. • Кому-то синтаксис unless-elseможет показаться интуитивно неочевидным. Поскольку unless— противоположность if, то ветвь elseвыполняется, когда условие истинно. • Простой тип Fixnumпередается как непосредственное значение и, стало быть, не может быть изменен внутри метода. То же относится к значениям true, falseи nil. • Не путайте операторы &&и ||с операторами &и |. Те и другие используются в языке С; первые два предназначены для логических операций, последние два — для поразрядных. • Операторы andи orимеют более низкий приоритет, чем &&и ||. Взгляните на следующий фрагмент: а = true b = false с = true d = true a1 = a && b or с && d # Операции && выполняются первыми. a2 = a && (b or с) && d # Операция or выполняется первой. puts a1 # Печатается false puts a2 # Печатается true • He забывайте, что «оператор» присваивания имеет более высокий приоритет, чем операторы andи or!(это относится и к составным операторам присваивания: +=, -=и пр.). Например, код x = y or zвыглядит как обычное предложение присваивания, но на самом деле это обособленное выражение (эквивалент (x=у) or z). Вероятно, программист имел в виду следующее: x = (y or z). y = false z = true x = y or z # Оператор = выполняется РАНЬШЕ or! puts x # Печатается false (x = y) or z # Строка 5: то же, что и выше. puts x # Печатается false x = (y or z) # Оператор or вычисляется сначала. puts x # Печатается true • Не путайте атрибуты объектов с локальными переменными. Если вы привыкли к C++ или Java, можете забыть об этом! Переменная @my_varв контексте класса — это переменная экземпляра (или атрибут), но my_varв том же контексте — локальная переменная. • Во многих языках, и в Ruby в том числе, есть цикл for. Рано или поздно возникает вопрос, можно ли модифицировать индексную переменную. В некоторых языках эту управляющую переменную запрещено изменять вовсе (выводится предупреждение либо сообщение об ошибке на этапе компиляции или выполнения); в других это допустимо, хотя и приводит к изменению поведения цикла. В Ruby принят третий подход. Переменная, управляющая циклом for, считается обычной переменной, которую можно изменять в любой момент, но это изменение не оказывает влияния на поведение цикла! Цикл for присваивает этой переменной последовательные значения, что бы с ней ни происходило внутри тела цикла. Например, следующий цикл будет выполнен ровно 10 раз и напечатает значения от 1 до 10: for var in 1..10 puts "var = #{var}" if var > 5 var = var + 2 end end • Имена переменных не всегда легко «на глаз» отличить от имен методов. Как решает этот вопрос анализатор? Правило такое: если анализатор видит, что идентификатору присваивается значение до его использования, то он считается переменной; в противном случае это имя метода. (Отметим, что операция присваивания может и не выполняться: достаточно того, что интерпретатор ее видел.) 1.5.3. Предложение case в RubyВо всех современных языках есть та или иная форма многопутевого ветвления. В C/C++ и Java это предложение switch, а в Pascal — предложение case. Служат они одной и той же цели и функционируют примерно одинаково. Предложение caseв Ruby похоже, но при ближайшем рассмотрении оказывается настолько уникальным, что варианты ветвления, принятые в С и в Pascal, кажутся близкими родственниками. Точного аналога предложению caseв Ruby нет ни в каком другом знакомом мне языке, поэтому оно заслуживает особого внимания. Выше мы уже рассматривали синтаксис этого предложения, а теперь сосредоточимся на его семантике. • Для начала рассмотрим тривиальный пример. Выражение expressionсравнивается со значением value, и, если они совпадают, выполняется некоторое действие. Ничего удивительного. case expression when value некоторое действие end В Ruby для этой цели есть специальный оператор ===(называется оператором отношения). Иногда его еще называют (не совсем правильно) оператором ветвящегося равенства. Неправильность в том, что он не всегда относится именно к проверке на равенство. • Предыдущее предложение можно записать и так: if value === expression некоторое действие end • Не путайте оператор отношения с оператором проверки на равенство ( ==). Они принципиально различны, хотя во многих случаях ведут себя одинаково. Оператор отношения определен по-разному в разных классах, а для данного класса его поведение может зависеть от типа переданного операнда. • Не думайте, что проверяемое выражение — это объект, которому сравниваемое значение передается в качестве параметра. На самом деле как раз наоборот (мы это только что видели). • Это подводит нас к наблюдению, что x===yозначает вовсе не то же самое, что y===x! Иногда результат совпадает, но в общем случае оператор отношения не коммутативен. (Именно поэтому нам не нравится термин «оператор ветвящегося равенства» — ведь проверка на равенство всегда коммутативна.) Если перевернуть исходный пример, окажется, что следующий код ведет себя иначе: case value when expression некоторое действие end • В качестве примера рассмотрим строку strи образец (регулярное выражение) pat, с которым эта строка успешно сопоставляется. Выражение str =~ patистинно, как в языке Perl. Поскольку Ruby определяет противоположную семантику оператора =~в классе Regexp, можно также сказать, что выражение pat =~ strистинно. Следуя той же логике, мы обнаруживаем, что истинно и pat === str(исходя из того, как определен оператор ===в классе Regexp). Однако выражение str === patистинным не является. А значит, фрагмент case "Hello" when /Hell/ puts "Есть соответствие." else puts "Нет соответствия." end делает не то же самое, что фрагмент case /Hell/ when "Hello" puts "Есть соответствие." else puts "Нет соответствия." end Если это вас смущает, просто постарайтесь запомнить. А если не смущает, тем лучше! • Программисты, привыкшие к С, могут быть озадачены отсутствием предложений breakв ветвях case. Такое использование breakв Ruby необязательно (и недопустимо). Связано это с тем, что «проваливание» редко бывает желательно при многопутевом ветвлении. В конце каждой ветви whenимеется неявный переход на конец предложения case. В этом отношении Ruby напоминает Pascal. • Значения в каждой ветви caseмогут быть произвольными. На типы никаких ограничений не налагается. Они не обязаны быть константами; допускаются и переменные, и сложные выражения. Кроме того, в ветви может проверяться попадание в диапазон. • В ветвях caseмогут находиться пустые действия (пустые предложения). Значения в разных ветвях не обязательно должны быть уникальными — допускаются перекрытия, например: case x when 0 when 1..5 puts "Вторая ветвь" when 5..10 puts "Третья ветвь" else puts "Четвертая ветвь" end Если x принимает значение 0, ничего не делается. Для значения 5 печатается строка «Вторая ветвь» — несмотря на то что 5 удовлетворяет и условию в третьей ветви. • Перекрытие ветвей допускается потому, что они вычисляются в строгом порядке и выполняется закорачивание. Иными словами, если вычисление выражения в какой-то ветви оказалось успешным, то следующие ветви не вычисляются. Поэтому не стоит помещать в ветви caseвыражения, в которых вызываются методы с побочными эффектами. (Впрочем, такие вызовы в любом случае сомнительны). Имейте также в виду, что такое поведение может замаскировать ошибки, которые произошли бы во время выполнения, если бы выражение вычислялось. Например: case x when 1..10 puts "Первая ветвь" when foobar() # Возможен побочный эффект? puts "Вторая ветвь" when 5/0 # Деление на нуль! puts "Третья ветвь" else puts "Четвертая ветвь" end Если x находится в диапазоне от 1 до 10, то метод foobar()не вызывается, а выражение 5/0 (которое, естественно, привело бы к ошибке) не вычисляется. 1.5.4. Рубизмы и идиомыМатериал в этом разделе во многом пересекается с изложенным выше. Но не задумывайтесь особо, почему мы решили разбить его именно таким образом. Просто многие вещи трудно точно классифицировать и организовать единственно правильным образом. Мы ставили себе задачу представить информацию в удобном для усвоения виде. Ruby проектировался как непротиворечивый и ортогональный язык. Но вместе с тем это сложный язык, в котором есть свои идиомы и странности. Некоторые из них мы обсудим ниже. • С помощью ключевого слова aliasможно давать глобальным переменным и методам альтернативные имена (синонимы). • Пронумерованные глобальные переменные $1, $2, $3и т.д. не могут иметь синонимов. • Мы не рекомендуем использовать «специальные переменные» $=, $_, $/и им подобные. Иногда они позволяют написать более компактный код, но при этом он не становится более понятным. Поэтому в данной книге мы прибегаем к ним очень редко, что и вам рекомендуем. • Не путайте операторы диапазона ..и ...— первый включает верхнюю границу, второй исключает. Так, диапазон 5..10включает число 10, а диапазон 5...10— нет. • С диапазонами связана одна мелкая деталь, которая может вызвать путаницу. Если дан диапазон m..n, то метод endвернет конечную его точку n, равно как и его синоним last. Но те же методы возвращают значение nи для диапазона m...n, хотя nне включается в него. Чтобы различить эти две ситуации, предоставляется метод end_excluded?. • Не путайте диапазоны с массивами. Следующие два присваивания абсолютно различны: x = 1..5 x = [1, 2, 3, 4, 5] Однако есть удобный метод to_aдля преобразования диапазона в массив. (Во многих других типах тоже есть такой метод.) • Часто бывает необходимо присвоить переменной значение лишь в том случае, когда у нее еще нет никакого значения. Поскольку «неприсвоенная» переменная имеет значение nil, можно решить эту задачу так: x = x || 5или сокращенно x ||= 5. Имейте в виду, что значение false, а равно и nil, будет при этом перезаписано. • В большинстве языков для обмена значений двух переменных нужна дополнительная временная переменная. В Ruby наличие механизма множественного присваивания делает ее излишней: выражение x, y = y, xобменивает значения xи y. • Четко отличайте класс от экземпляра. Например, у переменной класса @@foobarобластью видимости является весь класс, а переменная экземпляра @foobarзаново создается в каждом объекте класса. • Аналогично метод класса ассоциирован с тем классом, в котором определен; он не принадлежит никакому конкретному объекту и не может вызываться от имени объекта. При вызове метода класса указывается имя класса, а при вызове метода экземпляра - имя объекта. • В публикациях, посвященных Ruby, часто для обозначения метода экземпляра применяют решеточную нотацию. Например, мы пишем File.chmod, чтобы обозначить метод chmodкласса File, и File#chmodдля обозначения метода экземпляра с таким же именем. Эта нотация не является частью синтаксиса Ruby. Мы старались не пользоваться ей в этой книге. • В Ruby константы не являются истинно неизменными. Их нельзя изменять в теле методов экземпляра, но из других мест это вполне возможно. • Ключевое слово yieldпришло из языка CLU и некоторым программистам может быть непонятно. Оно используется внутри итератора, чтобы передать управление блоку, с которым итератор был вызван. В данном случае yieldне означает, что нужно получить результат или вернуть значение. Скорее, речь идет о том, чтобы уступить процессор для работы. • Составные операторы присваивания +=, -=и пр. — это не методы (собственно, это даже не операторы). Это всего лишь «синтаксическая глазурь» или сокращенная форма записи более длинной формы. Поэтому x += yзначит в точности то же самое, что x = x + y. Если оператор +перегружен, то оператор +=«автоматически» учитывает новую семантику. • Из-за того, как определены составные операторы присваивания, их нельзя использовать для инициализации переменных. Если первое обращение к переменной xвыглядит как x += 1, возникнет ошибка. Это интуитивно понятно для программистов, если только они не привыкли к языку, в котором переменные автоматически инициализируются нулем или пустым значением. • Такое поведение можно в некотором смысле обойти. Можно определить операторы для объекта nil, так что в случае, когда начальное значение переменной равно nil, мы получим желаемый результат. Так, метод nil.+, приведенный ниже, позволит инициализировать объект типа stringили Fixnum, для чего достаточно вернуть аргумент other. Таким образом, nil + otherбудет равно other. def nil.+(other) other end Мы привели этот код для иллюстрации возможностей Ruby, но стоит ли поступать так на практике, оставляем на усмотрение читателя. • Уместно будет напомнить, что Class— это объект, a Object— это класс. Мы попытаемся прояснить этот вопрос в следующей главе, а пока просто повторяйте это как мантру. • Некоторые операторы нельзя перегружать, потому что они встроены в сам язык, а не реализованы в виде методов. К таковым относятся =, .., ..., and, or, not, &&, ||, !, !=и !~. Кроме того, нельзя перегружать составные операторы присваивания ( +=, -=и т.д.). Это не методы и, пожалуй, даже не вполне операторы. • Имейте в виду, что хотя оператор присваивания перегружать нельзя, тем не менее возможно написать метод экземпляра с именем fоо=(тогда станет допустимым предложение x.foo = 5). Можете рассматривать знак равенства как суффикс. • Напомним: «голый» оператор разрешения области видимости подразумевает наличие Objectперед собой, то есть ::Foo— то же самое, что Objеct::Foo. • Как уже говорилось, fail— синоним raise. • Напомним, что определения в Ruby исполняются. Вследствие динамической природы языка можно, например, определить два метода совершенно по-разному в зависимости от значения признака, проверяемого во время выполнения. • Напомним, что конструкция for (for x in а)на самом деле вызывает итератор each. Любой класс, в котором такой итератор определен, можно обходить в цикле for. • Не забывайте, что метод, определенный на верхнем уровне, добавляется в модуль Kernelи, следовательно, становится членом класса Object. • Методы установки (например, fоо=) должны вызываться от имени объекта, иначе анализатор решит, что речь идет о присваивании переменной с таким именем. • Напомним, что ключевое слово retryможно использовать в итераторах, но не в циклах общего вида. В контексте итератора оно заставляет заново инициализировать все параметры и возобновить текущую итерацию с начала. • Ключевое слово retryприменяется также при обработке исключений. Не путайте два этих вида использования. • Метод объекта initializeвсегда является закрытым. • Когда итератор заканчивается левой фигурной скобкой (или словом end) и возвращает значение, это значение можно использовать для вызова последующих методов, например: squares = [1,2,3,4,5].collect do |x| x**2 end.reverse # squares теперь равно [25,16,9,4,1] • В конце программы на Ruby часто можно встретить идиому if $0 == __FILE__ Таким образом проверяется, исполняется ли файл как автономный кусок кода ( true) или как дополнительный, например библиотека ( false). Типичное применение — поместить некую «главную программу» (обычно с тестовым кодом) в конец библиотеки. • Обычное наследование (порождение подкласса) обозначается символом <: class Dog < Animal # ... end Однако для создания синглетного класса (анонимного класса, который расширяет единственный экземпляр) применяется символ <<: class << platypus # ... end • При передаче блока итератору есть тонкое различие между фигурными скобками ( {}) и операторными скобками do-end. Связано оно с приоритетом: mymethod param1, foobar do ... end # Здесь do-end связано с mymethod. mymethod param1, foobar { ... } # А здесь {} связано с именем foobar, предполагается, что это метод. • Традиционно в Ruby однострочные блоки заключают в фигурные скобки, а многострочные — в скобки do-end, например: my_array.each { |x| puts x } my_array.each do |x| print x if x % 2 == 0 puts " четно." else puts " нечетно." end end Это необязательно и в некоторых случаях даже нежелательно. • Помните, что строки (strings) в некотором смысле двулики: их можно рассматривать как последовательность символов или как последовательность строчек (lines). Кому-то покажется удивительным, что итератор eachоперирует строками (здесь под «строкой» понимается группа символов, завершающаяся разделителем записей, который по умолчанию равен символу новой строки). У eachесть синоним each_line. Если вы хотите перебирать символы, можете воспользоваться итератором each_byte. Итератор sortтакже оперирует строками. Для строк (strings) не существует итератора each_indexиз-за возникающей неоднозначности. Действительно, хотим ли мы обрабатывать строку посимвольно или построчно? Все это со временем войдет в привычку. • Замыкание (closure) запоминает контекст, в котором было создано. Один из способов создать замыкание — использование объекта Proc. Например: def power(exponent) proc {|base| base**exponent} end square = power(2) cube = power(3) a = square.call(11) # Результат равен 121. b = square.call(5) # Результат равен 25. с = cube.call(6) # Результат равен 216. d = cube.call(8) # Результат равен 512. Обратите внимание, что замыкание «знает» значение показателя степени, переданное ему в момент создания. • Однако помните: в замыкании используется переменная, определенная во внешней области видимости (что вполне допустимо). Это свойство может оказаться полезным, но приведем пример неправильного использования: $exponent = 0 def power proc {|base| base**$exponent} end $exponent = 2 square = power $exponent = 3 cube = power a = square.call(11) # Неверно! Результат равен 1331. b = square.call(5) # Неверно! Результат равен 125. # Оба результата неверны, поскольку используется ТЕКУЩЕЕ # значение $exponent. Так было бы даже в том случае, когда # используется локальная переменная, покинувшая область # видимости (например, с помощью define_method). с = cube.call(6) # Результат равен 216. d = cube.call(8) # Результат равен 512. • Напоследок рассмотрим несколько искусственный пример. Внутри блока итератора timesсоздается новый контекст, так что x— локальная переменная. Переменная closureуже определена на верхнем уровне, поэтому для блока она не будет локальной. closure = nil # Определим замыкание, чтобы его имя было известно. 1.times do # Создаем новый контекст. x = 5 # Переменная x локальная в этом блоке, closure = Proc.new { puts "В замыкании, x = #{x}" } end x = 1 # Определяем x на верхнем уровне. closure.call # Печатается: В замыкании, x = 5 Обратите внимание, что переменная x, которой присвоено значение 1, — это новая переменная, определенная на верхнем уровне. Она не совпадает с одноименной переменной, определенной внутри блока. Замыкание печатает 5, так как запоминает контекст своего создания, в котором была определена переменная xсо значением 5. • Переменные с именами, начинающимися с одного символа @, определенные внутри класса, — это, вообще говоря, переменные экземпляра. Однако если они определены вне любого метода, то становятся переменными экземпляра класса. (Это несколько противоречит общепринятой терминологии ООП, в которой «экземпляр класса» — то же самое, что и «экземпляр>> или «объект».) Пример: class Myclass @x = 1 # Переменная экземпляра класса. @y = 2 # Еще одна. def mymethod @x = 3 # Переменная экземпляра. # Заметим, что в этой точке @y недоступна. end end Переменная экземпляра класса ( @yв предыдущем примере — в действительности атрибут объекта класса Myclass, являющегося экземпляром класса Class. (Напомним, что Class— это объект, a Object— это класс.) На переменные экземпляра класса нельзя ссылаться из методов экземпляра и, вообще говоря, они не очень полезны. • attr, attr_reader, attr_writerи attr_accessor— сокращенная запись для определения методов чтения и установки атрибутов. В качестве аргументов они принимают символы (экземпляры класса Symbol). • Присваивание переменной, имя которой содержит оператор разрешения области видимости, недопустимо. Например, Math::Pi = 3.2— ошибка. 1.5.5. Ориентация на выражения и прочие вопросыВ Ruby выражения важны почти так же, как предложения. Для программиста на С это звучит знакомо, а для программиста на Pascal — откровенная нелепость. Но Ruby ориентирован на выражения даже в большей степени, чем С. Заодно в этом разделе мы остановимся на паре мелких вопросов, касающихся регулярных выражений; считайте это небольшим бонусом. • В Ruby любое присваивание возвращает то же значение, которое стоит в правой части. Поэтому иногда мы можем немного сократить код, как показано ниже, но будьте осторожны, имея дело с объектами! Не забывайте, что это почти всегда ссылки. x = y = z = 0 # Все переменные сейчас равны 0. а = b = с = [] # Опасно! a, b и с ссылаются # на ОДИН И ТОТ ЖЕ пустой массив. x = 5 y = x += 2 # Сейчас x и у равны 7. Напомним однако, что значения типа Fixnumи им подобные хранятся непосредственно, а не как ссылки на объекты. • Многие управляющие конструкции возвращают значения, в частности if, unlessи case. Следующий код корректен; он показывает, что при принятии решения ветви могут быть выражениями, а не полноценными предложениями. а = 5 x = if а < 8 then 6 else 7 end # x равно 6. y= if a<8 # y тоже равно 6; 6 # предложение if может располагаться else # на одной строке 7 # или на нескольких. end # unless тоже работает; z присваивается значение 4. z = unless x == y then 3 else 4 end t = case a # t получает when 0..3 # значение "low" # medium, when 4..6 "medium" else "high" end Здесь мы сделали такие отступы, будто caseявляется присваиванием. Мы воспринимаем такую запись спокойно, хотя вам она может не понравиться. • Отметим, что циклы whileи until, напротив, не возвращают никаких полезных значений; обычно их значением является nil: i = 0 x = while (i < 5) # x равно nil. puts i+=1 end • Тернарный оператор можно использовать как в предложениях, так и в выражениях. В силу синтаксических причин (или ограничений анализатора) скобки здесь обязательны: x = 6 y = x == 5 ? 0 : 1 #y равно 1. x == 5 ? puts("Привет") : puts("Пока") # Печатается: "Пока" • Предложение returnв конце метода можно опускать. Метод всегда возвращает значение последнего вычисленного выражения, в каком бы месте это вычисление ни происходило. • Когда итератор вызывается с блоком, последнее выражение, вычисленное в блоке, возвращается в качестве значения блока. Если при этом в теле итератора есть предложение x = yield, то xбудет присвоено это значение. • Регулярные выражения. Напомним, что после регулярного выражения можно написать модификатор многострочности /m, и в этом случае точка ( .) будет сопоставляться с символом новой строки. • Регулярные выражения. Опасайтесь соответствий нулевой длины. Если все элементы регулярного выражения необязательны, то такому образцу будет соответствовать «ничто», причем соответствие всегда будет найдено в начале строки. Это типичная ошибка, особенно часто ее допускают новички. 1.6. Жаргон RubyЗаново начинать учить английский для освоения Ruby необязательно. Но нужно знать кое-какие жаргонные выражения, обычные в сообществе. Некоторые из них имеют другой смысл, чем принято в компьютерном мире. Им и посвящен настоящий раздел. В Ruby термин «атрибут» носит неофициальный характер. Можно считать, что атрибут — это переменная экземпляра, которая раскрывается внешнему миру с помощью одного из методов семейства attr. Но тут нет полной определенности: могут существовать методы fooи foo=, не соответствующие переменной @foo, как можно было бы ожидать. И, конечно, не все переменные экземпляра считаются атрибутами. Как обычно, нужно придерживаться здравого смысла. Атрибуты в Ruby можно подразделить на методы чтения (reader) и установки (writer). Метод доступа, или акцессор (accessor), является одновременно методом чтения и установки. Это согласуется с названием метода attr_accessor, но противоречит принятой в других сообществах семантике, согласно которой акцессор дает доступ только для чтения. Оператор ===имеется только в Ruby (насколько мне известно). Обыкновенно он называется оператором ветвящегося равенства (case equality operator), поскольку неявно используется в предложениях case. Но это название, как я уже говорил, не вполне точно, потому что речь идет не только о «равенстве». В данной книге я часто употребляю термин «оператор отношения» (relationship operator). Изобрел его не я, но проследить происхождение мне не удалось, к тому же он употребляется нечасто. Жаргонное название — «оператор тройного равенства» (threequal operator) или просто «три равно». Оператор <=>, наверное, лучше всего называть оператором сравнения. На жаргоне его называют космическим оператором (spaceship operator), поскольку он напоминает летающую тарелку — так ее изображали в старых видеоиграх. Термин «поэтический режим» (poetry mode) подчеркивает, что можно опускать ненужные знаки препинания и лексемы (насмешливый намек на отношение поэтов к пунктуации на протяжении последних шестидесяти лет). Поэтический режим также часто означает «опускание скобок при вызове метода». some_method(1, 2, 3) # Избыточные скобки. some_method 1, 2, 3 # "Поэтический режим". Но мне этот принцип представляется более общим. Например, когда хэш передается в качестве последнего или единственного параметра, можно опускать фигурные скобки. В конце строки можно не ставить точку с запятой (а потому никто этого и не делает). В большинстве случаев разрешается опускать ключевое слово thenв предложениях ifи case. Некоторые программисты заходят еще дальше, опуская скобки даже в определении методов, но большинство так не поступает: def my_method(a, b, с) # Можно и так: def my_method a, b, с # ... end Стоит отметить, что в некоторых случаях сложность грамматики Ruby приводит к сбоям анализатора. Во вложенных вызовах методов скобки для ясности лучше оставлять. Иногда в текущей версии Ruby выводятся предупреждения: def alpha(x) x*2 end def beta(y) y*3 end gamma = 5 delta = alpha beta gamma # 30 -- то же, что alpha(beta(gamma)) # Выдается предупреждение: # warning: parenthesize argument(s) for future version # предупреждение: заключайте аргумент(ы) в скобки для совместимости с # с будущими версиями Термин duck typing («утиная типизация» или просто «утипизация»), насколько я знаю, принадлежит Дейву Томасу (Dave Thomas) и восходит к поговорке: «если кто-то выглядит как утка, ковыляет как утка и крякает как утка, то, наверное, это и есть утка». Точный смысл термина «утипизация» — тема для дискуссий, но мне кажется, что это намек на тенденцию Ruby заботиться не столько о точном классе объекта, сколько о том, какие методы для него можно вызывать и какие операции над ним можно выполнять. В Ruby мы редко пользуемся методом is_a?или kind_of, зато гораздо чаще прибегаем к методу respond_to?. Обычное дело — просто передать объект методу, зная, что при неправильном использовании будет возбуждено исключение. Так оно рано или поздно и случается. Унарную звездочку, которая служит для расширения массива, можно было бы назвать оператором расширения массива, но не думаю, что кто-нибудь слышал такое выражение. В хакерских кругах ходят словечки «звездочка» (star) и «расплющивание» (splat), а также производные определения — «расплющенный» (splatted) и «сплющенный» (unsplatted). Дэвид Алан Блэк придумал остроумное название «унарный оператор линеаризации» (unary unarray operator)[6]. Термин синглет (singleton) многие считают перегруженным. Это вполне обычное английское слово, означающее вещь, существующую в единственном экземпляре. Пока мы используем его в качестве модификатора, никакой путаницы не возникает. Но Singleton (Одиночка) — это еще и хорошо известный паттерн проектирования, относящийся к классу, для которого может существовать лишь один объект. В Ruby для реализации этого паттерна имеется библиотека singleton. Синглетный класс (singleton class) в Ruby —подобная классу сущность, методы которой хранятся на уровне объекта, а не класса. Пожалуй, это не «настоящий класс», потому что его нельзя инстанцировать. Ниже приведен пример открытия синглетного класса для строкового объекта с последующим добавлением метода: str = "hello" class << str # Альтернатива: def hyphenated # def str.hyphenated self.split("").join("-") end end str.hyphenated # "h-e-l-l-o" Кто-то предложил использовать термин eigenclass (класс в себе) —производное от немецкого слова eigen (свой собственный), коррелирующее с термином «собственное значение» (eigenvalue), применяемым в математике и физике. Остроумно, но в сообществе не прижилось и некоторым активно не нравится. Вернемся к предыдущему примеру. Поскольку метод hyphenateне существует ни в каком-либо другом объекте, ни в классе, это синглетный метод данного объекта. Это не вызывает неоднозначности. Иногда сам объект называется синглетным, поскольку он единственный в своем роде — больше ни у кого такого метода нет. Однако вспомним, что в Ruby сам класс является объектом. Поэтому мы можем добавить метод в синглетный класс класса, и этот метод будет уникален для объекта, который - по чистой случайности - оказался классом. Пример: class MyClass class << self # Альтернатива: def self.hello def hello # или: def MyClass.hello puts "Привет от #{self}!" end end end Поэтому необязательно создавать объект класса MyClassдля вызова этого метода. MyClass.hello # Привет от MyClass! Впрочем, вы, наверное, заметили, что это не что иное, как метод класса в Ruby. Иными словами, метод класса —синглетный метод объекта-класса. Можно также сказать, что это синглетный метод, определенный для объекта, который случайно оказался классом. Осталась еще парочка терминов. Переменная класса — это, разумеется, то, имя чего начинается с двух символов @. Возможно, название неудачно из-за нетривиального поведения относительно наследования. Переменная экземпляра класса — нечто совсем иное. Это обычная переменная экземпляра; только объект, которому она принадлежит, является классом. Дополнительную информацию по этой теме вы найдете в главе 11. 1.7. ЗаключениеНа этом завершается наш обзор объектно-ориентированного программирования и краткая экскурсия по языку Ruby. В последующих главах изложенный материал будет раскрыт более полно. Хотя в мои намерения не входило «учить Ruby» новичков, не исключено, что даже начинающие программисты на Ruby почерпнут что-то полезное из этой главы. Как бы то ни было, последующие главы будут полезны «рубистам» начального и среднего уровня. Надеюсь, что даже опытные программисты на Ruby найдут для себя что-то новенькое. Примечания:5 Типичный пример — язык JavaScript (Прим. перев.) 6 Прошу читателя извинить меня за это нагромождение выдуманных слов, но жаргон вряд ли следует переводить академически правильным языком. Хочется надеяться, что это все же лучше, чем кальки английских слов. (Прим. перев.) |
|
||||||||||||||||||||||||||||||
Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх |
||||||||||||||||||||||||||||||||
|