авторефераты диссертаций БЕСПЛАТНАЯ БИБЛИОТЕКА РОССИИ

КОНФЕРЕНЦИИ, КНИГИ, ПОСОБИЯ, НАУЧНЫЕ ИЗДАНИЯ

<< ГЛАВНАЯ
АГРОИНЖЕНЕРИЯ
АСТРОНОМИЯ
БЕЗОПАСНОСТЬ
БИОЛОГИЯ
ЗЕМЛЯ
ИНФОРМАТИКА
ИСКУССТВОВЕДЕНИЕ
ИСТОРИЯ
КУЛЬТУРОЛОГИЯ
МАШИНОСТРОЕНИЕ
МЕДИЦИНА
МЕТАЛЛУРГИЯ
МЕХАНИКА
ПЕДАГОГИКА
ПОЛИТИКА
ПРИБОРОСТРОЕНИЕ
ПРОДОВОЛЬСТВИЕ
ПСИХОЛОГИЯ
РАДИОТЕХНИКА
СЕЛЬСКОЕ ХОЗЯЙСТВО
СОЦИОЛОГИЯ
СТРОИТЕЛЬСТВО
ТЕХНИЧЕСКИЕ НАУКИ
ТРАНСПОРТ
ФАРМАЦЕВТИКА
ФИЗИКА
ФИЗИОЛОГИЯ
ФИЛОЛОГИЯ
ФИЛОСОФИЯ
ХИМИЯ
ЭКОНОМИКА
ЭЛЕКТРОТЕХНИКА
ЭНЕРГЕТИКА
ЮРИСПРУДЕНЦИЯ
ЯЗЫКОЗНАНИЕ
РАЗНОЕ
КОНТАКТЫ


Pages:     | 1 |   ...   | 7 | 8 || 10 | 11 |   ...   | 15 |

«АНДРЕЙ АЛЕКСАНДРЕСКУ Язык программирования D 16 лет вместе с профессионалами The D Programming ...»

-- [ Страница 9 ] --

Точное место, где смош енничало исходное определение класса Storable­ Shape, - ин ициализация поля _store выражением new DBObject. Это пол­ ностью отделяет подобъект _store от внешнего объекта класса Storable­ Shape, которому н уж н о переопределить методы DBObject. Итак, что нам необходимо сделать, так это определить новый класс MyDBObject внутри класса StorableShape. Этот класс сохранит обратную ссылку на внешний объект StorableShape и переопределит все методы, которые требуется пе­ реопределить. Н аконец, внутри переопределенных методов класс M B­ yD Object обладает доступом ко всему классу StorableShape, и все можно де­ лать так, будто в наш ем распоряж ении полноценное множественное на­ следование. Круто!

Если слова «внешний объект» напоминают вам что-то, уж е прозвучав­ ш ее в этой главе, поздравляю: вы заметили одно из самых счастливых совпадений в анн алах программирования. Вложенны е классы (см. раз­ дел 6.11) так хорошо подходят для «эмуляции» множественного насле 1 К сожалению, текущая на момент выхода книги версия компилятора до­ пускала только одно объявление a l i a s th is. - П р и м. на у ч.р е д.

6.13. Множественное порож дение подтипов дования, что их мож но принять за deu s ex m ach in a1. Н а самом деле, вло­ женные классы (на создание которых вдохновил язы к Java) появились задолго до конструкции a lia s th is.

Использование влож енны х классов делает переопределение с a lia s th is удивительно простым. Все, что нуж но сделать в этом случае, —опреде­ лить вложенный класс и сделать его наследником класса DBObject. В нут­ ри такого класса мож но переопределить какой угодно метод DBObject, и здесь вы обладаете полным доступом к общ едоступны м и защ ищ ен­ ным определениям класса DBObject и ко всем определениям класса Sto­ rableShape. Если бы это было чуть легче, то было бы запрещ ено по край­ ней мере в нескольких штатах.

c la s s StorableShape : Shape { p riv ate c l a s s MyDBObject : DBObject { override void sa v eS tate() { / / Доступ к DBObject и StorableShape } } p rivate MyDBObject _store;

a l i a s _sto re th is ;

th is() { / / Вот решающий момент установления связи _store = this.new MyDBObject;

} } Ключевым моментом является получение полем _store доступа ко внеш­ нему объекту StorableShape. Как показано в разделе 6.11, создание вло­ женного класса позволяет чудесным образом сохранить внеш ний объ­ ект (в данном случае th is) внутри вложенного класса. С помощью нота­ ции this.new MyObject всего лиш ь предпринята попытка прояснить, что th is обуславливает создание нового объекта MyDBObject. (На самом деле, this. и так подставляется неявно, поэтому в данном случае это можно не указывать.) Единственным камнем преткновения сл уж и т то, что внутренние эле­ менты DBObject будут перекрывать внутренние элементы StorableShape.

Предположим, что оба класса DBObject и Shape определили поле с именем _name:

c lass Shape { protected s tr in g _name;

a b s tra c t void draw();

1 Deus ex machina («бог из машины») - в древнегреческом театре: бог, спус­ кающийся с небес (изображающий его актер мог «летать» при помощи ме­ ханического крана) и решающий все проблемы героев. В переносном смыс­ ле - неожиданная удачная развязка неразрешимой ситуации. - Прим.ред.

290 Глава б. Классы. Объектно-ориентированный стиль } c l a s s DBObj0 c t { protected s t r i n g _name;

void sa v eS tate() { } void lo a d S ta te () { } } При реализации множественного порож дения подтипов с помощью класса MyDBObject, вложенного в класс StorableShape, поле DBObject._name перекрывает поле StorableShape._name. Таким образом, если код внутри MyDBObject будет использовать просто _name, то через этот идентифика­ тор он будет обращаться к DBObject._name.

c l a s s StorableShape : Shape { p r iv a t e c l a s s MyDBObject : DBObject { override void sa v eS tate() { / / Изменить Shape._name внешнего обьекта Shape this.o u te r._ n am e = "А";

/ / Изменить DBObject._name объекта-родителя _name = "B'';

/ / Просто для наглядности assert(super._nam e == "B");

p r iv a te MyDBObject _store;

a l i a s _ sto re t h is ;

th is() _ sto re = new MyDBObject;

} 6.14. Параметризированные классы и интерфейсы И ногда требуется параметризировать некоторый класс или интерфейс с помощью сущ ности, известной во время компиляции. Рассмотрим, например, определение интерфейса стека. Во избеж ание дублирования кода (StackInt, StackDouble, StackWidget,...) необходимо параметризиро­ вать этот интерфейс типом сохраняемы х в стеке элементов. Определе­ ние параметризированного интерфейса в D:

i n te r f a c e Stack(T) 0property bool empty();

0property r e f T to p ();

void push(T value);

void pop();

6.14. Параметризированные классы и интерфейсы С помощью синтаксиса (T) в интерфейс Stack вводится параметр типа.

В теле интерфейса T можно использовать так ж е, как любой другой тип.

Обращаясь к интерфейсу Stack в клиентском коде, нуж н о указать аргу­ мент. Такой аргумент мож но передать с помощью бинарного оператора !, как здесь:

unittest { alias S ta c k !( in t) StackOfInt;

alias S ta c k !in t SameAsAbove;

} Там, где используется всего один параметр (как и в наш ем случае с ин­ терфейсом Stack), круглые скобки можно опустить.

Логично было бы реализовать интерфейс в классе. В идеале реализация тож е должна быть обобщенной (не долж на быть настроена на какой-ли бо конкретный тип элементов). Следовательно, определяем параметри зированный класс StackImpl, который принимает параметр T, передает его в Stack и использует внутри своей реализации. А теперь реализуем стек на основе массива:

import s td.a rra y ;

class StackImpl(T) : Stack!T { private T[] _store;

eproperty bool empty() { return _store.empty;

} eproperty ref T to p () { assert(!empty);

return _store.back;

void push(T value) { sto re ^= value;

} void pop() { assert(!empty);

_store.popBack();

} Работать с StackImpl так ж е весело, как и реализовывать его:

unittest { auto stack = new StackImpl!int;

assert(stack.em pty);

s t a c k. push(3);

a s s e r t( s ta c k.t o p == 3);

s t a c k. push(5);

a s s e r t ( s t a c k.t o p == 5);

stack.pop();

a s s e r t ( s t a c k.t o p == 3);

292 Глава 6. Классы. Объектно-ориентированный стиль stack.pop();

a s s e r t ( s t a c k. empty);

} Как только вы создадите экземпляр параметризированного класса, он превратится в обычный класс, так что StackIm pl!int- это такой ж е класс, как и любой другой. Именно этот конкретный класс реализует Stack! in t, поскольку в формочку для вырезания StackImpl(T) под видом T вставили int по всему телу этого класса.

6.14.1. И снова гетерогенная трансляция Теперь, раз у ж мы заговорили о выведении настоящ их типов из пара­ метризированны х типов, приглядимся к процессу подстановки. Впер­ вые мы обсуж дал и понятие гетерогенной трансляции (которая проти­ воположна гомогенной трансляции) в разделе 5.3 в контексте функций с обобщ енными типами. Освежим в памяти основные моменты: в слу­ чае гомогенной трансляции инфраструктура языка предполагает, что все значения имеют один и тот ж е тип (например, все они объекты Object), и автоматически подгоняет обобщенный (содержащ ий аргумен­ ты типов) код к этом у общ ему типу. Подгонка мож ет включать приведе­ ние типов в обе стороны, а так ж е «упаковку» значений некоторых ти­ пов (чтобы заставить их соблюдать некоторый общий формат данных) и последую щ ую и х «распаковку» (когда пользовательский код захочет использовать упакованны е значения). Этот процесс безопасен для ти­ пов и полностью прозрачен. Языки Java и C # используют гомогенную трансляцию для своих параметризированных типов.

В соответствии с гомогенным подходом все стеки StackImpl разделяют один и тот ж е код для реализаций своих методов. И, что более важно, на уровне типов не сущ ествует никаких различий: динамические типы StackImpl!int и StackImpl!double одинаковы. Транслятор, по сути, опреде­ ляет один интерфейс для всех Stack!T и один класс для всех StackImpl!T.

Такие типы называют очищ енными, поскольку любая информация, специфичная для T, стирается. Затем транслятор искусно заменяет код, использую щ ий Stack и StackImpl с разными T, чтобы использовались только упом януты е очищенные типы. Статическая информация о ти­ пах, которые клиентский код вставляет в Stack и StackImpl, надолго не сохраняется;

эта информация служ ит для статической проверки типов, а затем сразу ж е забывается - или, лучш е сказать, стирается. При та­ ком подходе возникает ряд проблем - по той простой причине, что часть информации теряется. Вот простой пример: нельзя перегрузить функ­ цию Stack!int функцией Stack!double и наоборот, поскольку у них один и тот ж е тип. Есть и более глубокие вопросы безопасности, подлежащие обсуж дению. Отчасти они рассмотрены в научной литературе [14,1, 49].

Гетерогенный транслятор (такой как механизм шаблонов С++) подхо­ дит к проблеме по-другому. Д ля гетерогенного транслятора Stack - не тип, а лиш ь средство для создания типа. (Еще одно иносказание для 6.14. Параметризированные классы и интерфейсы ясности: тип - это схема значения, а параметризированный тип - схема типа.) К аж дое воплощение Stack!int, Stack! string, стек любых других значений, которые могут понадобиться в вашем прилож ении, породит отдельный, отличный от др уги х тип. Гетерогенный транслятор генери­ рует все эти типы, копируя и вставляя тело интерфейса Stack при зам е­ не T любым типом, который вы реш или использовать с интерфейсом Stack. При таком подходе, скорее всего, будет сгенерировано больше кода, но все ж е это более мощное реш ение, поскольку статическая ин­ формация о типах сохраняется во всей полноте. Кроме того, при гетеро­ генной трансляции в каж дом случае код генерируется индивидуально, следовательно, этот код мож ет оказаться более быстрым.

D везде использует гетерогенную трансляцию, а это значит, что Stack! int и Stack!double- разные интерфейсы, а StackImpl!int и StackImpl!double разные типы. Кроме общего происхождения от одного и того ж е парамет ризированного типа эти типы никак друг с другом не связаны. (Но вы, конечно, можете связать их некоторым образом, например, сделав все реализации интерфейса Stack наследниками общего интерфейса.) П о­ скольку класс StackImpl генерирует отдельную комбинацию методов для каждого подставленного в него типа, происходит частичное дублирова­ ние бинарного кода, что особенно раздражает, так как сгенерированный код часто оказывается полностью идентичным. Умный компилятор объ­ единил бы все эти идентичные функции в одну (на момент написания этой книги D не обладает такими способностями, но в более зрелых ком­ пиляторах С++ такое объединение уж е является принятой технологией).

Конечно ж е, у класса мож ет быть больше одного параметра типа. Пока­ ж ем это на примере класса StackImpl, наделив его одной интересной осо­ бенностью. Вместо того чтобы привязывать структуру хранения к мас­ сиву, вынесем это решение за пределы StackImpl. И з всей функциональ­ ности массивов StackImpl использует только empty, back, ^= и popBack. Так сделаем решение о выборе контейнера деталью реализации StackImpl:

c la ss StackImpl(T, Backend) : Stack!T { priv a te Backend _store;

©property bool empty() { return _store.empty;

} ©property ref T top () { assert(!em pty);

return _store.back;

} void push(T value) { _store ^= value;

} void pop() { assert(!em pty);

_store.popBack();

} } 294 Глава 6. Классы. Объектно-ориентированный стиль 6.15. Переопределение аллокаторов и деаллокаторов Как мы помним, D является языком для системного программирова­ ния. Иногда бывает нуж н о воспользоваться преимущ ествами объект но-ориентированного программирования, применяя при этом собствен­ ную стратегию выделения памяти. Текущ ие реализации D любезно предоставляют возможность определить для класса собственные алло­ кат ор (от англ. allocate - назначать, размещать) - функцию для выде­ ления памяти и деаллокат ор - функцию для ее освобождения. Вот как это выглядит:

im p o rt s t d. c. s t d l i b ;

im p o rt c o r e.e x c e p t i o n ;

c l a s s S td c C la s s { n e w ( s iz e _ t o b j _ s i z e ) { v o id * p t r = m a l l o c ( o b j _ s i z e ) ;

i f ( p tr i s n u ll) th ro w new O utO fM em oryE rror( FILE_, _ LINE_ );

_ r e tu r n p tr ;

} d e le te (v o id * p t r ) { i f (p tr) fre e (p tr);

Аллокатор объявляется как метод класса new без указания возвращае­ мого типа. Аллокатор всегда долж ен возвращать пустой указатель (void*). Аллокатор имеет м инимум один аргумент - размер экземпляра класса в байтах типа size_ t. В нашем примере память выделяется функ­ цией malloc стандартной библиотеки язы ка С. Эта функция принимает в качестве аргумента размер выделяемой памяти и в случае успеха воз­ вращ ает адрес выделенного блока памяти, иначе возвращает null. По­ сле вызова этой ф ункции мы проверяем полученный адрес. Если он ра­ вен null, то порож даем исключение, в которое передаем имя текущего ф айла и номер текущ ей строки - информацию, полезную при отладке.

Если указатель корректный, мы передаем его инструкции return алло­ катора.

Д еаллокатор объявляется с помощью ключевого слова delete и имеет один аргумент —указатель на блок памяти, в котором размещен объект.

1 О п исани е этой ч асти я зы к а нам еренно не бы ло приведено в оригинале кн и ­ ги, но п о ск о л ьк у эта возм ож н ость п ри сутствует в тек у щ и х р еал и зац и ях я з ы к а, м ы д о б ав и л и ее о п и сан и е в перевод. Прим. науч. ред.

6.15. Переопределение аллокаторов и деаллокаторов В приведенном примере, если указатель не пустой, мы освобож даем па­ мять, выделенную под объект.

Все. Мы получили класс, который не использует сборщик мусора. Те­ перь мы можем создавать и уничтож ать экземпляры этого класса при­ вычным для программистов на С ++ образом:

StdcClass obj = new StdcClass();

delete obj;

Но будьте внимательны. Если вы у ж е успели расслабиться, полагаясь на сборщик мусора D, то соберитесь. Вам придется самостоятельно от­ слеживать созданные объекты и уничтож ать и х, когда в ни х пропадает необходимость, с помощью оператора d elete. К аж дом у вызову new дол­ ж ен соответствовать вызов delete. Теперь от утечек пам яти вас не спасет никто, кроме вас сам их. Это плата за более быстрое и экономное созда­ ние объектов.

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

import s t d. c. s t d l i b ;

import core.exception;

class StdcClass { new(size_t obj_size) { void* p tr = m alloc(obj_size);

i f ( p tr is null) throw new OutOfMemoryError( FILE, LINE );

return ptr;

} new(size_t, void* p tr ) i f ( p tr is n u ll) throw new OutOfMemoryError( FILE, LINE );

return ptr;

} delete(void* p tr ) { i f ( p tr ) free(p tr);

} Попробуем его использовать.

s t a t i c void[_tra i t s ( c l a s s I n s t a n c e S i z e, StdcC lass)] data = void;

auto i_1 = new (data.ptr) StdcC lass();

/ / Разместить обьект / / в статическом буфере 296 Глава 6. Классы. Объектно-ориентированный стиль auto i_2 = new StdcClass();

/ / Выделить память встроенным аллокатором delete i_2;

К ак видим, аллокаторы могут быть перегружены обычным образом.

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

Аллокаторы и деаллокаторы классов считаются устаревшей возможно­ стью, и ее планирую т убрать из самого язы ка, так как, не считая особо­ го синтаксиса, ее м ож но запросто перевоплотить как простые функции в пользовательском коде. Например в стандартной библиотеке есть ф ункция emplace:

import std.conv;

class С { int i;

th is() { i = 42;

} unittest { void[_tr a i t s ( c l a s s I n s t a n c e S i z e, С)] data = void;

auto с = emplace!C (d ata[]);

a ssert(c.i == 42);

} К тому ж е, как всегда, при большой силе - большая ответственность:

если вы будете выделять память под классы, не используя кучу под кон­ тролем сборщ ика мусора, и ваш класс содерж ит указатели (в том числе неявные, унаследованны е от классов-родителей) на данные, выделен­ ные в памяти сборщика мусора, вы обязаны декларировать их создание сборщ ику мусора вызовом функции core.memory.GC.addRange(). Иначе сборщик мусора не будет знать о ссы лках на данные, хранящ ихся в ва­ ш и х классах, и м ож ет счесть мусором данные, на которые ваши классы все ещ е ссы лаются. Это мож ет повлечь за собой порчу данных и трудно­ уловимые ош ибки программы, так как в случаях порчи данных сбой м ож ет произойти у ж е спустя пару минут после порчи в совершенно по­ стороннем, невинном коде, который всего лиш ь пытался использовать свою ненароком испорченную область памяти. Мы вас предупредили!

6.16. Объекты scope В С ++ есть возмож ность создать экземпляр класса как автоматическую переменную. При создании экземпляра память выделяется в стеке и за­ пускается конструктор, а после выхода экземпляра из области видимо­ сти автоматически запускается деструктор.

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

6.16. Объекты scope Эта возможность полезна при использовании небольш их классов, соз­ даваемых для выполнения какой-то одной задачи, выполнив которую, они становятся не нуж ны. При использовании автоматических пере­ менных нет необходимости вызывать деструктор, поскольку он зап ус­ кается автоматически. Такая техника в язы ках ООП называется RAII (Resource A cquisition Is In itialization - «получение ресурса есть его инициализация»), так как она относится и к ин ициализации при созда­ нии объекта, и к его уничтож ению при покидании области видимости.

Для поддержки RAII язы к D предоставляет структуры с деструктора­ ми, которые будут описаны в следую щ ей главе, и похож ую конструк­ цию scope(exit). Но ещ е до того как D стал поддерживать структуры с деструкторами, подход R AII был реализован в объектах scope. Эта воз­ можность присутствует и в текущ их реализациях, но считается уста­ ревшей и небезопасной.

По сути, объект scope - это класс памяти, и используется примерно так же:

scope obj = new SomeClass(arg_1);

scope SomeClass obj_2 = new SomeClass(arg_1);

Обе записи делают одно и то ж е. Они создаю т временный экзем пляр класса, который автоматически будет уничтож ен при выходе из облас­ ти видимости. Память под него м ож ет быть выделена в стеке (а мож ет, и нет), но в любом случае нельзя передавать ссылку за пределы области видимости переменной. При объявлении собственных аллокатора и де­ аллокатора будут использованы они. Гарантируется, что сразу после выхода из области видимости будет вызвана инструкция delete.

В отличие от С++, в D объект scope сохраняет ссылочную семантику.

Как следствие того, что память выделяется в стековом фрейме ф унк­ ции, объект scope не мож ет быть возвращен функцией. Если это делает­ ся явно, компилятор генерирует ошибку:

Object oo() { scope t = new Object;

return t;

/ / Ошибка времени компиляции } Однако если объект scope возвращ ается неявно, отследить это ком пиля­ тор не сможет:

Object foo() { scope t = new Object;

Object p = t;

return p;

/ / Компилятор пропустит это, / / но результат ни к чему хорошему не приведет Чтобы избеж ать подобных конфликтов, D вводит понятие кл асса scope.

Экземпляры класса scope могут быть только объектами scope. О бъявле­ 298 Глава 6. Классы. Объектно-ориентированный стиль ния не scope-ссылки на объект компилятор не допустит. Для объявле­ ния класса scope нуж н о добавить в его объявление атрибут scope. Атри­ бут scope является транзитивным, то есть все подклассы класса scope так ж е являю тся классами scope.

Объявим абстрактный класс для нахож дения дайдж еста (хеш-суммы).

К онкретная реализация, произведенная от данного класса, может реа­ лизовывать алгоритм нахож дени я М Б5-дайдж еста, SHA-256 или любо­ го другого, но интерфейс от этого не изменится.

Import s t d. s t d i o ;

a b s t r a c t scope c l a s s Digest a b s t r a c t void update (v oid[] data);

a b s t r a c t ubyte[] r e s u l t ( ) ;

} c l a s s Md5: Digest { / / Тоже scope } Приведем пример ф ункции для вычисления МЮ5-суммы какого-то фай­ ла:

ubyte[] c a lc u la te H a sh (strin g filename) { scope d ig e s t = new Md5();

auto fd = File(filenam e, "r");

ubyte[] buff = new ubyte[4096];

ubyte[] readBuff;

do { readBuff = fd.rawRead(buff);

d i g e s t. u p d ate(readBuff);

} while (readB uff.leng th);

return d i g e s t. r e s u l t ( ) ;

} void m a in ( str in g [] args) ubyte[] hash = ca lc ulateH ash(args[1]);

} Однако д а ж е при использовании классов scope компилятор не может отследить сохранение ссылки на объект при передаче ее функции с не­ явным приведением типов:

scope c l a s s С { ) Object saved;

void save(Object с) { 6.17. Итоги saved = с;

void derp() { scope с = new С;

save(c);

} void main() { d e rp ();

/ / Куда сейчас указывает глобальная переменная saved?

Использование объектов scope м ож ет повысить производительность.

Если класс не переопределяет аллокатор по умолчанию, память под объект может быть выделена в стеке. Выделить память в стеке гораздо быстрее, чем динамическую память. Кроме того, объект scope использу­ ет память ровно столько, сколько нуж н о, автоматически освобож дая ее. В случае ж е со сборщиком мусора память будет освобож дена когда нибудь. Чем больше памяти будет выделяться и освобождаться не через сборщик мусора, тем чащ е будет производиться сбор. Во время сбора мусора приостанавливаются все потоки выполнения и производится поиск блоков памяти, на которые нет ни одной ссылки. Такие блоки па­ мяти освобождаются.

Класс памяти scope и классы scope считаются устаревш ими и небезопас­ ными, и их планируют убрать из самого языка. Вместо них в стандарт­ ную библиотеку была добавлена очень похож ая конструкция scoped!T:

import std.typecons;

unittest { c la s s А { i n t x;

} auto a1 = scoped!A();

auto a2 = scoped!A();

a1.x = 42;

a2.x = 53;

a s s e r t( a 1.x == 42);

} Конструкция scoped!T, которую мож но найти в модуле std.typecons, столь ж е небезопасна, но не засоряет очередной излиш ней возм ож но­ стью язык (осложняя этим ж изнь авторам компиляторов D).

6.17. Итоги Классы - это основное средство для реализации объектно-ориентиро­ ванных решений в D. Они повсюду используют ссылочную семантику и утилизирую тся сборщиком мусора.

Наследование позволяет использовать динамический полиморфизм. Р аз­ решено только одиночное наследование, но класс мож ет стать наслед 300 Глава б. Классы. О бъектно-ориентированный стиль ником нескольких интерфейсов. Интерфейсы не обладают собствен­ ным состоянием, но могут определять финальные методы.

П равила защ иты соответствуют правилам защ иты, принятым в опера­ ционной системе (каталоги и файлы).

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

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

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

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

Во-первых, классы подчиняю тся ссылочной семантике и из-за этого мо­ гут воплощать многие проектные реш ения не полностью или с ощ ути­ мыми накладными расходами. Н а практике трудно моделировать с по­ мощью класса такую простую сущ ность, как точка с двумя или тремя координатами, если таких точек больше нескольких миллионов: разра­ ботчик оказывается перед непростым выбором - хорош ая абстракция или приемлемое быстродействие. Кроме того, для линейной алгебры ссылочная семантика - больш ая морока. П опробуйте убедить матема­ тика или программиста-теоретика, что присваивание а = b долж н о де­ лать из матрицы а лишь псевдоним матрицы b, а не отдельную копию!

Д аж е такой простой тип, как массив, довольно накладно моделировать в виде класса в сравнении с мощной и лаконичной абстракцией массива, имеющейся в языке D (см. главу 4). Можно, конечно, сделать массивы «волшебными», но опыт то и дело показывает, что предоставлять мно­ жество «волшебных» типов, не воспроизводимых в пользовательском коде, - дурной тон и признак плохо спроектированного язы ка. Затраты на массив - всего два слова, а выделение памяти под экзем пляр класса и использование дополнительного косвенного обращ ения означают большие накладные расходы по памяти и времени для всех примитивов массива. Д аж е такой простой тип, как in t, нельзя выразить в виде клас­ са дешево и элегантно (причем речь не об удобстве оператора). У такого класса, как BigInt, та ж е проблема: а = b делает нечто совершенно иное, чем соответствующая операция присваивания для типа int.

302 Глава 7. Другие пользовательские типы Во-вторых, классы ж ивут вечно, а значит, с их помощью трудно моде­ лировать ресурсы с выраженным конечным временем ж изни (такие как дескрипторы файлов, дескрипторы графического контекста, мьютек­ сы, сокеты и т. д.). Работая с такими ресурсами как с классами, нужно постоянно быть начеку, чтобы не забыть своевременно освободить ин­ капсулированные ресурсы с помощью метода, вроде close или dispose.

В таких случаях обычно помогает инструкция scope (см. раздел 3.13), но лучш е, когда подобная контекстная семантика инкапсулирована в типе - раз и навсегда.

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

D не был бы настоящ им языком для системного программирования, ес­ ли бы предоставлял для выражения абстракций только классы. Кроме классов в запасе у D есть структуры (типы-значения, сравнимые с клас­ сами по мощ ности, но с семантикой значения и без полиморфизма), ти­ пы enum (легковесные перечисляемые типы и простые константы), объ­ единения (низкоуровневое хранилищ е с перекрыванием для разных типов) и вспомогательные механизмы определения типов, такие как a lia s. Все эти средства последовательно рассматриваются в этой главе.

7.1. Структуры Структуры позволяют определять простые, инкапсулированные типы значения. Удобная аналогия - тип int: значение типа int —это 4 байта, допускаю щ ие определенные операции. В int нет никакого скрытого со­ стояния и никаких косвенных обращений, и две переменные типа int всегда ссылаются на разные значения1. Соглашение о структурах ис­ ключает динамический полиморфизм, переопределение методов, насле­ дование и бесконечное время ж изни. Структура - это преувеличенный тип int.

Как вы помните, класс ведет себя как ссылка (см. раздел 6.2), то есть вы всегда манипулируете объектом посредством ссылки на него, причем ко­ пирование ссылок лиш ь увеличивает количество ссылок на тот ж е объ­ ект без дублирования самого объекта. А структура - это тип-значение, то есть, по сути, ведет себя «как int»: имя ж естко привязано к представ­ ленному им значению, а при копировании значения структуры на са­ мом деле копируется целый объект, а не только ссылка.

Определяют структуру так ж е, как класс, за исключением следую щ их моментов:

1 Н е с ч и т а я эк в и в а л е н т н ы х и м ен, со зд а в ае м ы х с пом ощ ью a lia s, о чем м ы ещ е п о г о в о р и м в э т о й г л а в е (с м. р а з д е л 7.4).

7.1. Структуры вместо ключевого слова c la s s используется ключевое слово s tru c t;

• • свойственное классам наследование и реализация интерфейсов за­ прещены, то есть в определении структуры нельзя указать :BaseType или :Interface, и очевидно, что внутри структуры не определена ссыл­ ка super;

• методы структуры нельзя переопределять - все методы являю тся финальными (вы мож ете указать в определении метода структуры ключевое слово fin a l, но это было бы совершенно излишне);

нельзя применять к структуре инструкцию synch ronized (см. главу 13);

• • структуре запрещ ается определять конструктор по умолчанию t h i s ( ) (см. раздел 7.1.3.1);

• структуре разреш ается определять конструктор копирования (post blit constructor) t h i s ( t h i s ) (см. раздел 7.1.3.4);

запрещен спецификатор доступа protected (иначе предполагалось бы • наличие структур-потомков).

Определим простую структуру:

s t r u c t Widget { / / Константа enum f u d g e F a c t o r = 0. 2 ;

/ / Разделяемое неизменяемое значение s t a t i c i m m u t a b l e d e f a u l t N a m e "Виджет";

= / / Некоторое состояние, память под которое выделяется / / для каждого экземпляра класса W i d g e t s t r i n g n a me = d e f a u l t N a m e ;

u in t w idth, h e i g h t ;

/ / Статический метод s t a t i c d o uble howFudgy() { return fudgeFactor;

} / / Метод vo id changeN am e(string another) { n a me = a n o t h e r ;

} 7.1.1. Семантика копирования Несколько заметных на глаз различий м еж ду структурами и классами есть следствие менее очевидных семантических различий. Повторим эксперимент, который мы у ж е проводили, обсуж дая классы в разде­ ле 6.2. На этот раз создадим структуру и объект с одинаковыми поля­ ми, а затем сравним поведение эти х типов при копировании:

class С { i n t x = 42;

double у = 3.14;

} 304 Глава 7. Другие пользовательские типы struct S { i n t x = 42;

do u ble у = 3.14;

} un ittest { С c 1 = n e w С;

S s1;

/ / Н и к а к о г о о п е р а т о р а new д л я S: п а м я т ь в ы д е л я е т с я в с т е к е a u t o c2 = c1;

a u to s 2 = s1;

c 2. x = 100;

s 2. x = 100;

a s s e r t ( c 1. x = = 1 0 0 ) ;

/ / c 1 и c 2 с с ы л а ю т с я н а о д и н и т о т же о б ъ е к т...

a s s e r t ( s 1. x == 4 2 ) ;

//. a s 2 - э т о н а с т о ящ а я копия s При работе со структурами нет никаких ссылок, которые можно привя­ зывать и перепривязывать с помощью операций инициализации и при­ сваивания. К аж дое имя экзем пляра структуры связано с отдельным значением. К ак у ж е говорилось, объект-структура ведет себя как зн а­ чение, а объект-класс - к а к ссы лка. На рис. 7.1 показано положение дел сразу после определения c2 и s2.

Рис. 7.1. И нст рукции auto c2 = с7;

для объекта-класса c1 и auto s2 = s1;

для объекта-структуры s1 действуют совершенно по-разному, поскольку класс по своей природе - ссылка, а структура - значение В отличие от имен c1 и c2, допускаю щ их привязку к любому объекту, имена s1 и s2 прочно привязаны к реальным объектам. Нет способа за­ ставить два имени ссылаться на один и тот ж е объект-структуру (кроме ключевого слова a l i a s, задаю щ его простую эквивалентность имен;

см.

раздел 7.4), и не бывает имени структуры без закрепленного за ним зна­ чения, так что сравнение s1 i s null бессмысленно и порождает ошибку во время ком пиляции.

7.1.2. Передача объекта-структуры в функцию П оскольку объект типа s t r u c t ведет себя как значение, он и передается в ф ункцию по значению.

struct S { i n t а, b, с;

7.1. Структуры d o u b le x, у, z;

) void fun(S s) { // fun п о л у ч а е т к о пи ю } Передать объект-структуру по ссылке мож но с помощью аргумента с ключевым словом ref (см. раздел 5.2.1):

void fun(ref S s) { // fun получает ссылку Раз у ж мы заговорили о ref, отметим, что t h i s передается по ссылке внутрь методов структуры S в виде скрытого параметра ref S.

7.1.3. Жизненный цикл объекта-структуры В отличие от объектов-классов, объектам-структурам не свойственно бесконечное время ж и зн и (lifetim e). Время ж и зн и для ни х четко огра­ ничено - так ж е как для временных (стековых) объектов ф ункций.

Чтобы создать объект-структуру, задайте им я нуж ного типа, как если бы вы вызывали функцию:

im port std.m ath ;

stru ct Test { double а = 0.4;

d o u b l e b;

} un ittest { / / Чт о б ы с о з д а т ь о б ь е к т, и с п о л ь з у й т е имя с т р у к т у р ы т а к, / / как и с п о л ь з у е т е функцию a u t o t = T e s t ( );

a s s e r t ( t. a == 0. 4 && I s N a N ( t. b ) ) ;

} Вызов Test() создает объект-структуру, все поля которого ин ициализи­ рованы по умолчанию. В наш ем случае это означает, что поле t. a при­ нимает значение 0.4, а t.b остается инициализированны м значением double.init.

Вызовы Test(1) и Test(1.5, 2.5) так ж е разреш ены и инициализирую т по­ ля объекта в порядке их объявления. П родолж им преды дущ ий пример:

un ittest { a u to t1 = T est(1);

assert(t1.a == 1 && I s N a N ( t 1. b ) ) ;

auto t2 = T est(1.5, 2.5);

assert(t2.a = = 1. 5 && t 2. b == 2. 5 ) ;

306 Глава 7. Другие пользовательские типы Поначалу мож ет раздраж ать разница в синтаксисе вы ражения, создаю­ щего объект-структуру Test ('аргументы), и вы ражения, создающего объ­ ект-класс new lest(apryMeHTbi). D мог бы отказаться от использования ключевого слова new при создании объектов-классов, но это new напоми­ нает программисту, что выполняется операция выделения памяти (то есть необычное действие).

7.1.3.1. Конструкторы Конструктор структуры определяется так ж е, как конструктор класса (см. раздел 6.3.1):

stru c t T e s t { d o u b le а = 0. 4 ;

d o u b le b;

th ls(d o u b le b) { t h i s.b = b;

} } u n ltte st { a u to t = T e s t ( 5 ) ;

} Присутствие хотя бы одного пользовательского конструктора блокирует все упомянуты е выше конструкторы, инициализирующ ие поля струк­ туры:

a u to t 1 = T e s t ( 1. 1, 1. 2 ) ;

Ошибка!

// // Нет к о н с т р у к т о р а, с о о т в е т с т в у ю щ е г о в ы з о в у T e s t ( d o u b l e, double) Есть важ ное исключение: компилятор всегда определяет конструктор без аргументов:

a u to t 2 = T e s t ( ) ;

/ / Все в порядке, с о з д а е т с я объект // с " н а ч и н к о й " по у м о л чан ию Кроме того, пользовательский код не мож ет определить собственный конструктор без аргументов:

stru ct T e st { d o u b le а = 0. 4 ;

d o u b le b;

t h i s ( ) { b = 0;

} / Ошибка!

/ // Структура не м о ж ет о п р е д е л и т ь к о н с т р у к т о р по умолчанию!

} Зачем нуж н о такое ограничение? Все из-за T.init - значения по умолча­ нию, определяемого каж ды м типом. Оно долж но быть статически из­ вестно, что противоречит сущ ествованию конструктора по умолчанию, выполняющего произвольный код. (Для классов T.init - это пустая ссылка null, а не объект, построенный по умолчанию.) Правило для всех структур: конструктор по умолчанию инициализирует все поля объек­ та-структуры значениями по умолчанию.

7.1. Структуры 7.1.3.2. Делегирование конструкторов Скопируем пример из раздела 6.3.2 с заменой ключевого слова cla ss на struct:

s t r u c t Widget { t h i s ( u i n t height) { th is ( 1, height);

/ / Положиться на другой конструктор } t h i s ( u i n t w idth, u in t height) { th is.w id th = width;

t h i s. height = h e ig h t;

} uint width, height;

Код запускается, не требуя внесения каких-либо других изменений.

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

7.1.3.3. Алгоритм построения Классу приходится заботиться о выделении динамической памяти и инициализации своего базового подобъекта (см. раздел 6.3.3). Со структурами все гораздо проще, поскольку выделение памяти - явный шаг алгоритма построения. Алгоритм построения объекта-структуры типа T по шагам:

1. Скопировать значение T.init в память, где будет размещ ен объект, путем копирования «сырой» памяти (а-ля memcpy).

2. Вызвать конструктор, если нуж но.

Если инициализация некоторых или всех полей структуры выглядит как = void, объем работ на первом ш аге мож но сократить, хотя и редко намного, зато такой маневр часто порож дает трудноуловимые ош ибки в вашем коде (тем не менее случай оправданного применения сокра­ щенной инициализации иллюстрирует пример с классом Transmogrifier в разделе 6.3.3).

7.1.3.4. Конструктор копирования this(this) Предположим, требуется определить объект, который содерж ит ло­ кальный (private) массив и предоставляет ограниченный A P I для ма­ нипуляции этим массивом:

s t r u c t Widget { priv ate i n t [ ] array;

t h i s ( u i n t length) { array = new in t[le n g th ];

} 308 Глава 7. Другие пользовательскиетипы in t g et(size_t o ffse t) { retu rn a r r a y [ o f f s e t ] ;

void s e t ( s i z e _ t o f f s e t, i n t value) { a r r a y [ o f f s e t ] = value;

} У класса Widget, определенного таким образом, есть проблема: при ко­ пировании объектов типа Widget м еж ду копиями создается отдаленная зависимость. Судите сами:

u n ittest { auto w1 = Widget(10);

auto w2 = w1;

w1.set(5, 100);

w2.set(5, 42);

/ / Также изменяет элемент w1.array[5]!

as se r t( w 1.g e t(5 ) == 100);

/ / Не проходит!?!

} В чем проблема? Копирование содержимого w в w2 «поверхностно», то есть оно выполняется поле за полем, без транзитивного копирования, на какую бы память косвенно ни ссылалось каж дое из полей. При ко­ пировании массива память под новый массив не выделяется;

копиру­ ются лиш ь границы массива (см. раздел 4.1.4). После копирования w и w2 действительно обладают различными полями с массивами, но ссы­ лаются эти поля на одну и ту ж е область памяти. Такой объект, являю­ щ ийся значением, но содерж ащ ий неявные разделяемые ссылки, мож­ но в ш утку назвать «клуктурой», то есть гибридом структуры (семанти­ ка значения) и класса (семантика ссылки)1.

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

s t r u c t Widget { p r iv a t e i n t [ ] array;

t h i s ( u i n t length) { a rra y = new i n t[ le n g th ] ;

/ / Конструктор копирования th ls (th is) { a rra y = array.dup;

) / / Как раньше i n t g e t ( s i z e _ t o f f s e t ) { return a r r a y [ o f f s e t] ;

) void s e t ( s i z e _ t o f f s e t, i n t value) { a r r a y [ o f f s e t] = value;

} } 1 Т ерм и н « к л у к т у р а » п р е д л о ж и л Б а р т о ш М ил евск и.

7.1. Структуры Конструктор копирования вступает в силу во время копирования объ­ екта. Чтобы инициализировать объект приемник с помощью объекта ис­ точник того ж е типа, компилятор долж ен выполнить следую щ ие шаги:

1. Скопировать участок «сырой» памяти объекта источник в участок «сырой» памяти объекта приемник.

2. Транзитивно для каж дого поля, содерж ащ его другие поля (то есть поля, содерж ащ его другое поле, содерж ащ ее третье поле,...), для ко­ торого определен метод t h i s ( t h i s ), вызвать эти конструкторы снизу вверх (начиная от наиболее глубоко вложенного поля).

3. Вызвать метод t h i s ( t h i s ) с объектом приемник.

Оригинальное название конструктора копирования «postblit con struc­ tor» происходит от «blit» - популярной аббревиатуры понятия «block transfer», означавшего копирование «сырой» памяти. Язык применяет «сырое» копирование при инициализации и разрешает сразу после это­ го воспользоваться ловушкой. В преды дущ ем примере конструктор ко­ пирования превращает только что полученный псевдоним массива в на­ стоящую, полномасштабную копию, гарантируя, что с этого момента м еж ду объектом-оригиналом и объектом-копией не будет ничего общ е­ го. Теперь, после добавления конструктора копирования, модуль легко проходит этот тест:

u n ittest { auto w1 = Widget(10);

auto w2 = w1;

/ / th is(th is) здесь вызывается с w w1.set(5, 100);

w2.set(5, 42);

a s sert(w 1.get(5) == 100);

/ / Пройдено } Вызов конструктора копирования вставляется в каж дом случае копи­ рования какого-либо объекта при явном или неявном создании новой переменной. Например, при передаче объекта типа Widget по значению в функцию так ж е создается копия:

void fun(Widget w) { / / Передать по значению w.set(2, 42);

} void gun(ref Widget w) { / / Передать по ссылке w.set(2, 42);

} u n ittest { auto w1 = Widget(10);

w1.set(2, 100);

fun(w1);

/ / Здесь создается копия a ssert(w 1.g et(2 ) == 100);

/ / Тест пройден gun(w1);

/ / А эдесь копирования нет 310 Глава 7. Другие пользовательские типы a s s e rt(w 1.g e t(2 ) == 42);

/ / Тест пройден Второй ш аг (часть с «транзитивным полем») процесса конструирования при копировании заслуж ивает особого внимания. Основанием для та­ кого поведения является инкапсуляция: конструктор копирования объекта-структуры долж ен быть вызван даж е тогда, когда эта структу­ ра встроена в другую. П редполож им, например, что мы решили сделать Widget членом другой структуры, которая в свою очередь является чле­ ном третьей структуры:

s t r u c t Widget2 { Widget wl;

i n t x;

} s t r u c t Widget3 { Widget2 w2;

s tr in g name;

th is(th is) { name = name ~ " (copy)'';

) Теперь, если потребуется копировать объекты, содерж ащ ие другие объ­ екты типа Widget, будет очень некстати, если компилятор забудет, как нуж н о копировать подобъекты типа Widget. Вот почему при копирова­ нии объектов типа Widget2 инициируется вызов конструктора th is(th is) для подобъекта w1, невзирая на то, что Widget2 вообще об этом ничего не знает. Кроме того, при копировании объектов типа Widget3 конструктор th is(th is) по-преж нем у вызывается применительно к полю w поля w2.

Внесем ясность:

u n ittest { Widget2 а;

a.w1 = Widget(10);

// Выделить память под данные auto b = а;

// t h i s ( t h i s ) вызывается для b.w a s s e r t( a.w 1.a r r a y ! i s b.w1.array);

/ / Тест пройден Widget3 с;

c.w2.w1 = Widget(20);

auto d = с;

/ / t h i s ( t h i s ) вызывается для d.w2.w assert(c.w 2.w 1.a rra y ! i s d.w2.w1.array);

/ / Тест пройден } Вкратце, если вы определите для некоторой структуры конструктор ко­ пирования th is(th is), компилятор позаботится о том, чтобы конструк­ тор копирования вызывался в каж дом случае копирования этого объ­ екта-структуры независимо от того, является ли он самостоятельным объектом или частью более крупного объекта-структуры.

7.1. Структуры 7.1.3.5. Аргументы в пользу this(this) Зачем был введен конструктор копирования? Ведь ничего подобного в других язы ках пока нет. Почему бы просто не передавать исходны й объект в будущ ую копию (как это делает С++)?

/ / Это не D stru ct S { th is (S another) { } / / Или t h i s ( r e f S another) { } } Опыт с С++ показал, что основная причина неэффективности программ на С++ - злоупотребление копированием объектов. Чтобы сократить по­ тери эффективности по этой причине, С ++ устанавливает ряд случаев, в которых компилятор мож ет пропускать вызов конструктора копиро­ вания (copy elision). Правила для этих случаев очень быстро услож ни­ лись, но все равно не охватывали все моменты, когда можно обойтись без конструирования, то есть проблема осталась не решенной. Развива­ ющийся стандарт С++ затрагивает эти вопросы, определяя новый тип «ссылка на r-значение», позволяющий пользователю управлять пропус­ ками вызова конструктора копирования, но плата за это - ещ е больш ее услож нение языка.

Благодаря конструктору копирования подход D становится простым и во многом автоматизируемым. Начнем с того, что объекты в D дол ж ­ ны быть перемещ аемыми, то есть не долж ны зависеть от своего располо­ жения: копирование «сырой» памяти позволяет переместить объект в другую область памяти, не наруш ая его целостность. Тем не менее это ограничение означает, что объект не м ож ет содержать так называемые внут ренние ук а за т ел и - адреса подобъектов, являю щ ихся его частя­ ми. Без этой техники мож но обойтись, так что D попросту ее исключает.

Создавать объекты с внутренними указателям и в D запрещ ается, и ком­ пилятор, как и подсистема времени исполнения, вправе предполагать, что это правило соблюдается. Перемещаемые объекты открывают для компилятора и подсистемы времени исполнения (например, для сбор­ щика мусора) большие возмож ности, позволяющ ие программам стать более быстрыми и компактными.

Благодаря перемещаемости объектов копирование объектов становится логическим продолжением перемещения объектов: конструктор копи­ рования th is(th is) делает копирование объектов эквивалентом переме­ щения с возможной последующей пользовательской обработкой. Таким образом, пользовательский код не м ож ет изменить поля исходного объ­ екта (что очень хорошо, поскольку копирование не должно затрагивать объект-источник), но зато мож ет корректировать поля, которые не дол ж ­ ны неявно разделять состояние с объектом-источником. Чтобы избеж ать лишнего копирования, компилятор вправе по собственному усмотре­ нию не вставлять вызов th is(th is), если мож ет доказать, что источник 312 Глава 7. Другие пользовательские типы копии не будет использован после заверш ения процесса копирования.

Рассмотрим, например, ф ункцию, возвращ ающую объект типа Widget (определенный выше) по значению:

Widget hun(uint x) return Widget(x * 2);

} unittest { auto w = hun(1000);

} Н аивный подход: просто создать объект типа Widget внутри функции hun, а затем скопировать его в переменную w, применив побитовое копи­ рование с последую щ им вызовом th is(th is). Но это было бы слишком расточительно: D полагается на перемещаемость объектов, так почему бы попросту не переместить в переменную wуж е отживш ий свое времен­ ный объект, созданны й функцией hun? Разницу никто не заметит, по­ скольку после того, как функция hun вернет результат, временный объ­ ект у ж е не нуж ен. Если в лесу упало дерево и никто этого не слышит, то легче переместить его, чем копировать. П охож ий (но не идентичный) случай:

Widget iu n (u in t x) { auto r e s u l t = Widget(x * 2);

return r e s u lt;

unittest { auto w = iun(1000);

} В этом случае переменная result тож е уходит в небытие сразу ж е после того, как iun вернет управление, поэтому в вызове th is(th is) необходи­ мости нет. Н аконец, ещ е более тонкий случай:

void jun(Widget w) { } unittest { auto w = Widget(1000);

.. / / «код, jun(w );

• • / / *код В этом случае слож нее выяснить, можно ли избавиться от вызова th is(th is). Вполне вероятно, что код2 продолж ает использовать w, и то­ 7.1. Структуры гда перемещение этого значения из u n ittest в jun было бы некоррект­ ным1.

Ввиду всех перечисленных соображ ений в D приняты следую щ ие пра­ вила пропуска вызова конструктора копирования:

• Все анонимные r-значения перемещ аются, а не копируются. Вызов конструктора копирования th is(th is) всегда пропускается, если ори­ гиналом является анонимное r-значение (то есть временный объект, как в функции hun выше).

• В случае именованных временных объектов, которые создаю тся внутри функции и располагаются в стеке, а затем возвращаются этой функцией в качестве результата, вызов конструктора копирования th is(th is) пропускается.

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

Но иногда требуется предписать компилятору выполнить перемещ е­ ние. Фактически это выполняет ф ункция move из модуля std.algorithm стандартной библиотеки:

import std.algorithm ;

void kun(Widget w) { ) unittest { auto w = Widget(1000);

... / / - код, • / / Вставлен вызов move kun(move(w));

assert(w == W id g et.in it);

/ / Пройдено •.. / / кодг ) Вызов функции move гарантирует, что w будет перемещена, а ее содерж и­ мое будет заменено пустым, сконструированным по умолчанию объек­ том типа Widget. Кстати, это один из тех случаев, где пригодится неизме­ няемый и не порождающ ий исключения конструктор по умолчанию Widget.init (см. раздел 7.1.3.1). Б ез него слож но было бы найти способ ос­ тавить источник перемещения в строго определенном пустом состоянии.


7.1.3.6. Уничтожение объекта и освобождение памяти Структура может определять деструктор с именем ^ t h i s ( ) :

import s td. s td i o ;

stru ct S { 1 Кроме того, код}может сохранить указатель на значение w которое исполь­, зует О 2'.

КД 314 Глава 7. Другие пользовательские типы int x = 42;

'th is() { S с содержимым ", x, " исчезает. Пока!");

w riteln (" C T p yK T ypa } ) void main() { writeln("Co3flanne объекта типа S.");

S object;

области видимости объекта ");

w r it e ln (" B H y T p n } writeln("BHe области видимости объекта");

} Эта программа гарантированно выведет на экран:

С о зд а н и е о б ъ е к т а т и п а S.

Внутри о б л а с т и в и ди м ости о б ъ е к т а С тр ук тур а S с содержимым 42 и с ч е з а е т. Пока!

Вне о б л а с т и в и ди м ости о б ъ е к т а.

К аж дая структура обладает временем ж изни в пределах области види­ м ост и (scoped lifetim e), то есть ее ж изнь действительно заканчивается с окончанием области видимости объекта. Подробнее:

• время ж и зн и нестатического объекта, определенного внутри функ­ ции, заканчивается в конце текущ ей области видимости (то есть контекста) до уничтож ения всех объектов-структур, определенных перед ним;

• время ж изни объекта, определенного в качестве члена другой струк­ туры, заканчивается непосредственно после окончания времени ж и з­ ни включающ его объекта;

• время ж и зн и объекта, определенного в контексте модуля, бесконеч­ но;

если вам нуж н о вызвать деструктор этого объекта, сделайте это в деструкторе модуля (см. раздел 11.3);

• время ж и зн и объекта, определенного в качестве члена класса, за­ канчивается в тот момент, когда сборщик мусора забирает память включающ его объекта.

Язы к гарантирует автоматический вызов деструктора ^this по оконча­ нии времени ж и зн и объекта-структуры, что очень удобно, если вы хо­ тите автоматически выполнять такие операции, как закрытие файлов и освобож дение всех важ ны х ресурсов.

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

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

7.1. Структуры 7.1.3.7. Алгоритм уничтожения структуры По умолчанию объекты-структуры уничтож аю тся в порядке, строго обратном порядку их создания. То есть первым уничтож ается объект структура, определенный в заданной области видимости последним:

import std.conv, std.stdio;

struct S { private string name;

t h is (str in g name) { writeln(name, " создан.");

th is.name = name;

^this() { writeln(name, " уничтожен.");

} } void main() { auto obj1 = ЭС'первый объект");

foreach (i;

0 3) { auto obj = S(text("o6beKT " i ) ) ;

} auto obj2 = ЭСпоследний объект");

) Эта программа выведет на экран:

п ер в ы й о б ъ е к т с о з д а н, объект 0 создан, объект 0 уничтож ен.

объект 1 создан, объект 1 уничтож ен.

объект 2 создан, объект 2 уничтож ен.

последний о б ъ ект с о з д а н.

последний о б ъ ект уничтож ен, п ер в ы й о б ъ е к т у н и ч т о ж е н.

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

Можно явно инициировать вызов деструктора объекта-структуры с по­ мощью инструкции с1еаг(объект);

. С ф ункцией clear мы у ж е познакоми­ лись в разделе 6.3.5. Тогда она оказалась полезной для уничтож ения состояния объекта-класса. Д ля объектов-структур ф ункция clear дела­ ет то ж е самое: вызывает деструктор, а затем копирует биты значения.in it в область памяти объекта. В результате получается правильно сконструированный объект, правда, без какого-либо интересного содер­ ж ания.

316 Глава 7. Другие пользовательские типы 7.1.4. Статические конструкторы и деструкторы Структура м ож ет определять лю бое число статических конструкторов и деструкторов. Это средство полностью идентично одноименному сред­ ству для классов, с которым мы у ж е встречались в разделе 6.3.6.

im port s t d. s t d i o ;

struct А { s t a t ic ^ th is() { w r i t e l n ( "Первый ст а т и ч ес к и й д е с т р у к т о р " );

} sta tic th is() { w r i t e l n ( " n e p B b m ст ат и ч ес к и й конструк тор ");

sta tic th is() { w r i t e l n ( " B T o p o n ст а т и ч ес к и й кон ст рук тор");

} sta tic 'th is () { w r i t e l n ( " B T o p o n ст ат и ч ес к и й д е с т р у к т о р " );

} } void m a in () { writeln("BHHMaHne, говор и т mai n");

Парность статических конструкторов и деструкторов не требуется. Под­ система поддерж ки времени исполнения не делает ничего интересно­ го —просто выполняет все статические конструкторы перед вычислени­ ем функции main в порядке их определения. По завершении выполне­ ния main подсистема поддерж ки времени исполнения так ж е скучно вы­ зывает все статические деструкторы в порядке, обратном порядку их определения. П реды дущ ая программа выведет на экран:

Первый с т а т и ч е с к и й к о н ст р у к т о р В торой с т а т и ч е с к и й к о н ст р у к т о р В н им ан и е, г о в о р и т m ain В торой с т а т и ч е с к и й д е с т р у к т о р Первый с т а т и ч е с к и й д е с т р у к т о р Порядок выполнения очевиден для статических конструкторов и де­ структоров, располож енны х внутри одного модуля, но в случае не­ скольких модулей не всегда все так ж е ясно. Порядок выполнения ста­ тических конструкторов и деструкторов из разны х модулей определен в разделе 6.3.6.

7.1. Структуры 7.1.5. Методы Структуры могут определять функции-члены, так ж е называемые мето­ дами. Поскольку в случае структур о наследовании и переопределении речи нет, методы структур лиш ь немногим больше, чем ф ункции.

Нестатические методы структуры S принимают скрытый параметр th is по ссылке (эквивалент параметра ref S). Поиск имен внутри методов структуры производится так ж е, как и внутри методов класса: парамет­ ры перекрывают одноименные внутренние элементы структуры, а им е­ на внутренних элементов структуры перекрывают те ж е имена, объяв­ ленные на уровне модуля.

void fu n (in t x) { a s s e r t( x != 0);

} / / Проиллюстрируем правила поиска имен stru ct S { i n t x = 1;

s t a t i c i n t у = 324;

void f u n (in t x) { a s s e r t( x == 0);

/ / Обратиться к параметру x a s s e r t ( t h i s. x == 1);

/ / Обратиться к внутреннему элементу x } void gun() { fun(0);

/ / Вызвать метод fun.fun(1);

/ / Вызвать функцию fun, / / определенную на уровне модуля } / / Тесты модуля могут быть внутренними элементами структуры u n ittest { S obj;

obj.gun();

a s s e r t( y == 324);

/ / Тесты модуля, являющиеся / / "внутренними элементами" / / видят статические данные Кроме того, в этом примере есть тест модуля, определенный внутри структуры. Такие тесты модуля, являю щ иеся «внутренними элемента­ ми», не наделены никаким особым статусом, но и х очень удобно встав­ лять после каж дого определения метода. К оду тела внутреннего теста модуля доступна та ж е область видимости, что и обычным статическим методам: например, тесту модуля в преды дущ ем примере не требуется снабжать статическое поле у префиксом S, как это не потребовалось бы любому методу структуры.

318 Глава 7. Другие пользовательские типы Некоторые особые методы заслуж иваю т более тщательного рассмотре­ ния. К ним относятся оператор присваивания opAssign, используемый оператором =, оператор равенства opEquals, используемый операторами == и !=, а так ж е упорядочиваю щ ий оператор opCmp, используемый опера­ торами, =, = и. Н а самом деле, эта тема относится к главе 12, так как затрагивает вопрос перегрузки операторов, но эти операторы особен­ ные: компилятор м ож ет сгенерировать их автоматически, со всем их особым поведением.

7.1.5.1. Оператор присваивания По умолчанию, если задать:

s t r u c t Widget { } / / Определен так же, как в разделе 7.1.3. Widget wl, w2;

w1 = w2;

то присваивание делается через копирование всех внутренних элемен­ тов по очереди. В случае типа Widget такой подход может вызвать про­ блемы, о которых говорилось в разделе 7.1.3.4. Если помните, структура Widget обладает внутренним локальным массивом типа in t[], и планиро­ валось, что он будет индивидуальны м для каж дого объекта типа Widget.

В ходе последовательного присваивания полей объекта w2 объекту w поле w2.array будет присвоено полю w1.array, но это будет только простое присваивание границ массива —в действительности, содержимое мас­ сива скопировано не будет. Этот момент необходимо подкорректиро­ вать, поскольку на самом деле мы хотим создать дубли кат массива ори­ гинальной структуры и присвоить его целевой структуре.

П ользовательский код м ож ет перехватить присваивание, определив метод opAssign. По сути, если lhs определяет opAssign с совместимой сиг­ натурой, присваивание lhs = гЬвтранслируетсяв lhs.opAssign(rhs), иначе если lhs и rhs имеют один и тот ж е тип, выполняется обычное присваи­ вание поле за полем. Д авайте определим метод Widget.opAssign:

s t r u c t Widget { p r iv a te i n t [ ] array;

/ / th is(u in t), th is (th is ), и т.д.

r e f Widget opAssign(ref Widget rhs) { array = rhs.a rray.dup ;

return th i s ;

} } Оператор присваивания возвращ ает ссылку на th is, тем самым позво­ ляя создавать цепочки присваиваний а-ля w = w2 = w3, которые компиля­ тор заменяет на w1.opAssign(w2.opAssign(w3)).

Осталась одна проблема. Рассмотрим присваивание:

7.1. Структуры Widget w;

w = Widget(50);

/ / Ошибка!

/ / Невозможно привязать r -значение типа Widget к ссылке ref Widget!

Проблема в том, что метод opAssign в таком виде, в каком он определен сейчас, ож идает аргумент типа ref Widget, то есть 1-значение типа Widget.


Чтобы помимо 1-значений можно было бы присваивать ещ е и г-значе­ ния, структура Widget долж на определять два оператора присваивания:

import std.algorithm ;

s t r u c t Widget { priv ate i n t [ ] array;

/ / th is (u in t), th is (th is ), и т.д.

ref Widget opAssign(ref Widget rhs) { array = rhs.array.dup;

return th is ;

} ref Widget opAssign(Widget rhs) { swap(array, rh s.a rra y );

return th is ;

В версии метода, принимающ ей r-значения, у ж е отсутствует обращ е­ ние к свойству.dup. Почему? Ну, r-значение (а с ним и его массив) - это практически собственность второго метода opAssign: оно было скопиро­ вано перед входом в функцию и будет уничтож ено сразу ж е после того, как функция вернет управление. Это означает, что больше нет нуж ды дублировать rhs.array, потому что его потерю никто не ощ утит. Доста­ точно лишь поменять местами rhs.array и this.array. Ф ункция opAssign возвращает результат, и rhs и старый массив объекта th is уходят в ни­ куда, а th is остается с массивом, ранее принадлеж авш им rhs, - совер­ шенное сохранение состояния.

Теперь можно совсем убрать первую перегруж енную версию оператора opAssign: та версия, что принимает rhs по значению, заботится обо всем сама (1-значения автоматически конвертируются в г-значения). Но оста­ вив версию с 1-значением, мы сохраняем точку, через которую мож но оп­ тимизировать работу оператора присваивания. Вместо того чтобы д у б ­ лировать структуру-оригинал с помощью свойства.dup, метод opAssign может проверять, достаточно ли в текущ ем массиве места для разм ещ е­ ния нового содержимого, и если да, то достаточно и записи поверх ста­ рого массива на том ж е месте.

/ / Внутри Widget ref Widget opAssign(ref Widget rhs) { i f (a rra y.le n g th r h s.a r r a y.le n g th ) { array = rhs.array.dup;

} e ls e { 320 Глава 7. Другие пользовательские типы / / Отрегулировать длину a r r a y.le n g th = r h s.a rr a y.le n g th ;

/ / Скопировать содержимое массива array (см. раздел 4.1.7) a r r a y [ ] = r h s.a r r a y [ ] ;

} return t h i s ;

} 7.1.5.2. Сравнение структур на равенство Средство для сравнения объектов-структур предоставляется «в комп­ лекте* - это операторы == и !=. Сравнение представляет собой поочеред­ ное сравнение внутренних элементов объектов и возвращает fa lse, если хотя бы два соответствую щ их друг другу элемента сравниваемых объ­ ектов не равны, иначе результатом сравнения является true.

s t r u c t Point { i n t x, у;

u n ittest { Point а, b;

a s s e r t ( a == b);

a.x = 1;

a s s e r t ( a ! = b);

} Чтобы определить собственный порядок сравнения, определите метод opEquals:

import std.math, s t d. s t d i o ;

s t r u c t Point { f l o a t x = 0, у = 0;

/ / Добавлено bool opEquals(ref const Point rhs) const { / / Выполнить приблизительное сравнение return approxEqual(x, rh s.x ) & approxEqual(y, rhs.y);

& ) } u n itte st { Point а, b;

a s s e r t ( a == b);

a.x = 1e-8;

a s s e r t ( a == b);

a.y = 1e-1;

a s s e r t ( a ! = b);

} По сравнению с методом opEquals для классов (см. раздел 6.8.3) метод opEquals для структур гораздо проще: ему не нуж но беспокоиться о кор­ ректности своих действий из-за наследования. Компилятор попросту 7.1. Структуры заменяет сравнение объектов-структур на вызов метода opEquals. К о­ нечно, применительно к структурам остается требование определять осмысленный метод opEquals: рефлексивный, симметричный и транзи­ тивный. Заметим, что хотя метод Point.opEquals выглядит довольно ос­ мысленно, он не проходит тест на транзитивность. Л учш им вариантом оператора сравнения на равенство было бы сравнение двух объектов ти­ па Point, значения координат которых предварительно усечены до сво­ их старших разрядов. Такую проверку было бы гораздо прощ е сделать транзитивной.

Если структура содерж ит внутренние элементы, определяю щ ие мето­ ды opEquals, а сама такой метод не определяет, при сравнении все равно будут вызваны сущ ествую щ ие методы opEquals внутренних элементов.

П родолжим работать с примером, содерж ащ им структуру Point:

s t r u c t Rectangle { Point leftBottom, rightTop;

} u n ittest { Rectangle а, b;

a s s e r t ( a == b);

a.leftB ottom.x = 1e-8;

a s s e r t ( a == b);

а. rightTop.у = 5;

a s s e r t( a != b);

} Для любых двух объектов а и b типа Rectangle вычисление а == b эквива­ лентно вычислению выражения a.leftB ottom == b.leftBottom & a.rightT op == b.rightTop & что в свою очередь мож но переписать так:

а. leftBottom.opEquals(b.leftBottom) && а. rightTop.opEquals(b.rightTop) Этот пример такж е показывает, что сравнение выполняется в порядке объявления полей (т. e. поле leftBottom проверяется до проверки right­ Top), и если встретились два неравных поля, сравнение заверш ается до того, как будут проверены все поля, благодаря сокращ енному вычисле­ нию логических связок, построенных с помощью оператора & (short & circuit evaluation).

7.1.6. Статические внутренние элементы Структура мож ет определять статические данны е и статические внут­ ренние функции. Помимо ограниченной видимости и подчинения пра­ вилам доступа (см. раздел 7.1.7) реж им работы статических внутренних функций ничем не отличается от реж им а работы обычных функций.

322 Глава 7. Другие пользовательские типы Нет скрытого параметра th is, не вовлечены никакие другие особые ме­ ханизмы.

Точно так ж е статические данны е схож и с глобальными данными, определенными на уровне модуля (см. раздел 5.2.4), во всем, кроме ви­ димости и ограничений доступа, налож енны х на эти статические дан­ ные родительской структурой.

in p o r t s t d. s t d i o ;

s t r u c t Point { p r iv a t e i n t x, у;

p r iv a t e s t a t i c s t r i n g formatSpec = "(%s %s)\n";

s t a t i c void setFormatSpec(string newSpec) { / / Проверить корректность спецификации формата formatSpec = newSpec;

} void p r i n t ( ) { writef(formatSpec, x, у);

} } void main() { auto pt1 = Point(1, 2);

p t1.p rin t();

/ / Вызвать статическую внутреннюю функцию, / / указывая ее принадлежность префиксом Point или pt Point.setFormatSpec("[%s, %s]\n");

auto pt2 = Point(5, 3);

/ / Новая спецификация действует на все обьекты типа Point p t1.p rin t();

p t2.p rin t();

} Эта программа выведет на экран:

(1 2 ) [1, 2 ] [5, 3] 7.1.7. Спецификаторы доступа Структуры подчиняю тся спецификаторам доступа private (см. раз­ дел 6.7.1), package (см. раздел 6.7.2), public (см. раздел 6.7.4) и export (см.

раздел 6.7.5) тем ж е образом, что и классы. Спецификатор protected применительно к структурам не имеет смысла, поскольку структуры не поддерживаю т наследование.

З а подробной информацией обратитесь к соответствующим разделам.

А здесь мы лиш ь вкратце напомним смысл спецификаторов:

7.1. Структуры stru ct S { priv a te i n t а;

// Доступен в пределах текущего файла и в методах S package i n t b;

// Доступен в пределах каталога текущего файла public i n t с;

// Доступен в пределах текущего приложения export i n t d;

// Доступен вне текущего приложения / / (там, где оно используется) } Заметим, что хотя ключевое слово export разреш ено везде, где синтак­ сис допускает применение спецификатора доступа, семантика этого ключевого слова зависит от реализации.

7.1.8. Вложенность структур и классов Часто бывает удобно вложить в структуру другую структуру или класс.

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

s t r u c t Tree { private:

c l a s s Node { i n t value;

a b s tr a c t Node l e f t ( ) ;

a b s tr a c t Node r ig h t( ) ;

c l a s s NonLeaf : Node { Node _ l e f t, _right;

override Node l e f t ( ) { return _ l e f t ;

} override Node r ig h t( ) { return _ r i g h t ;

} } c la s s Leaf : Node { override Node l e f t ( ) { return n u ll;

} override Node r i g h t () { return n u ll;

} } / / Данные Node root;

public:

void add (in t value) { } bool se a r c h ( in t value) { } } Аналогично структура м ож ет быть влож ена в другую структуру...

s t r u c t Widget { private:

s t r u c t Field { s tr in g name;

uint x, у;

} F ield [] f ie ld s ;

324 Глава 7. Другие пользовательские типы public:

...и наконец, структура м ож ет быть влож ена в класс.

c l a s s Window { s t r u c t Info { s t r i n g паше;

Window parent;

Window[] children;

} Info g e tIn f o ( );

) В отличие от классов, влож енны х в другие классы, вложенные струк­ туры и классы, вложенны е в другие структуры, не обладают никаким скрытым внутренним элементом outer - никакой специальный код не генерируется. Такие вложенны е типы определяются в основном со структурной целью - чтобы получить нуж ное управление доступом.

7.1.9. Структуры, вложенные в функции Вспомним, что говорилось в разделе 6.11.1: вложенные классы находят­ ся в привилегированном полож ении, ведь они обладают особыми, уни­ кальными свойствами. Влож енному классу доступны параметры и ло­ кальные переменные включающей функции. Если вы возвращаете вло­ ж енны й класс в качестве результата функции, компилятор даж е разме­ щает кадр ф ункции в динамической памяти, чтобы параметры и локальные переменные ф ункции вы ж или после того, как она вернет управление.

Д ля единообразия и согласованности D оказывает структурам, вложен­ ным в ф ункции, те ж е услуги, что и классам, вложенным в функции.

В лож енная структура м ож ет обращаться к параметрам и локальным переменным включающей функции:

void f u n ( in t а) { i n t b;

s t r u c t Local { i n t с;

i n t sum() { / / Обратиться к параметру, переменной / / и собственному внутреннему элементу структуры Local return а + b + с;

} Local obj;

i n t x = obj.sum();

/ / ( v o id * ).s iz e o f - размер указателя на окружение / / l n t. s i z e o f - размер единственного поля структуры 7.1. Структуры a s s e r t( L o c a l.s iz e o f == ( v o id * ).siz e o f + i n t. s i z e o f ) ;

} u n ittest { fun(5);

} Во вложенные структуры встраивается волшебный «указатель на кадр», с помощью которого они получают доступ к внеш ним значениям, та­ ким как а и b в этом примере. И з-за этого дополнительного состояния размер объекта Local не 4 байта, как мож но было ож идать, а 8 (на 32-раз рядной машине) - еще 4 байта заним ает указатель на кадр. Если хотите определить влож енную структуру без этого багаж а, просто добавьте в определение структуры Local ключевое слово s ta tic перед ключевым словом struct - тем самым вы превратите Local в обычную структуру, то есть закроете для нее доступ к а и b.

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

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

Атрибут @disable К структурам неприменимы наследование и полиморфизм, но этот тип данных по-прежнему поддерживает конструкцию a lia s th is, впервые представленную в разделе 6.13. С помощью a lia s th is м ож но сделать структуру подтипом любого другого типа. Определим, к примеру, про­ стой тип Final, поведением очень напоминаю щ ий ссылку на класс - во всем, кроме того что переменную типа Final невозмож но перепривязать!

Пример использования переменной Final:

import s td. s td i o ;

c l a s s Widget { void p r i n t ( ) { writeln("npnBeT, я объект класса Widget. Вот, пожалуй, и все обо мне.”);

I } u n ittest { auto а = Final!Widget(new Widget);

a.p rin t();

/ / Все в порядке, просто печатаем а auto b = а;

/ / Все в порядке, а и b привязаны / / к одному и тому же объекту типа Widget а = b;

/ / Ошибка!

/ / opAssign(Final!Widget) деактивиэирован!

326 Глава 7. Другие пользовательские типы а = new Widget;

/ / Ошибка!

/ / Невозможно присвоить значение г-эначению, / / возвращенному функцией g et()!

} П редназначение типа Final - быть особым видом ссылки на класс, раз и навсегда привязанной к одному объекту. Такие «преданные» ссылки полезны для реализации множ ества проектных идей.

Первый ш аг - избавиться от присваивания. Проблема в том, что опера­ тор присваивания генерируется автоматически, если не объявлен поль­ зователем, поэтому структура Final долж на вежливо указать компиля­ тору не делать этого. Д ля этого предназначен атрибут @disable:

s t r u c t Final(T) { / / Запретить присваивание © d i s a b le v o i d opAssign(Final);

С помощью атрибута @disable м ож но запретить и другие сгенерирован­ ные ф ункции, например сравнение.

Д о си х пор все шло хорошо. Чтобы реализовать Final!T, нуж но с помо­ щью конструкции a lia s th is сделать Final(T) подтипом T, но чтобы при этом полученны й тип не являлся 1-значением. Ошибочное решение вы­ глядит так:

/ / Ошибочное решение s t r u c t Final(T) { p r i v a t e T payload;

t h i s ( T bindTo) { payload = bindTo;

} / / Запретить присваивание e d i s a b l e v o i d opAssign(Final);

/ / Сделать Final(T) подклассом T a l i a s payload t h i s ;

} Структура Final хранит ссылку на себя в поле payload, которое инициа­ лизируется в конструкторе. Кроме того, объявив, но не определяя ме­ тод opAssign, структура эффективно «замораживает» присваивание. Та­ ким образом, клиентский код, пытающ ийся присвоить значение объек­ ту типа Final!T, или не см ож ет обратиться к payload (из-за private), или получит ош ибку во время компоновки.

Ошибка Final - в использовании инструкции a lia s payload th is;

. Этот тест модуля делает что-то непредусмотренное:

c la ss А { i n t value = 42;

t h i s ( i n t x) { value = x;

} } 7.1. Структуры unittest { auto v = Final!A(new A(42));

void sneaky(ref А ra) { ra = new A(4242);

I sneaky(v);

//Х м -м -м..

a s s e r t( v.v a lu e == 4242);

/ / Проходит?!?

} a lia s payload th is действует довольно просто: каж ды й раз, когда значе­ ние объект типа Final!T используется в недопустимом для этого типа кон­ тексте, компилятор вместо объект пиш ет oбъeкт.payload (то есть делает o6beKT.payload псевдонимом для объекта в соответствии с именем и син­ таксисом конструкции a lias). Но вы ражение объект.payload представля­ ет собой непосредственное обращ ение к полю объект, следовательно, яв­ ляется 1-значением. Это 1-значение привязано к переданному по ссылке параметру функции sneaky и, таким образом, позволяет sneaky напря­ мую изменять значение поля объекта v.

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

Так мы получим полную функциональность, но ссылка, сохраненная в payload, станет неприкосновенной. Очень просто осущ ествить привяз­ ку к г-значению с помощью свойства (объявленного с атрибутом @рго perty), возвращающего payload по значению:

s t r u c t Final(T) { priv ate T payload;

th is (T bindTo) { payload = bindTo;

} / / Запретить присваивание, оставив метод opAssign неопределенным priv ate void opAssign(Final);

/ / Сделать Final(T) подклассом T, / / не разрешив при этом перепривязывать payload eproperty T g e t() { return payload;

} a l i a s get th is ;

} Ключевой момент в новом определении структуры — то, что метод get возвращает значение типа T, а не ref Т. Конечно, объект, на который ссы­ лается payload, изменить мож но (если хотите избеж ать этого, ознакомь­ тесь с квалификаторами const и immutable;

см. главу 8). Но структура Final свои обязательства теперь выполняет. Во-первых, для любого ти­ па класса T справедливо, что Final! T ведет себя как Т. Во-вторых, одн аж ­ ды привязав переменную типа Final!T к некоторому объекту с помощью конструктора, вы не см ож ете ее перепривязать ни к какому другому объекту. В частности, тест модуля, из-за которого пришлось отказаться от предыдущего определения Final, больше не компилируется, посколь­ ку вызов sneaky(v) теперь некорректен: г-значение типа А (неявно полу­ ченное из v с помощью v.get) не мож ет быть привязано к ref А, как тре­ буется функции sneaky для ее черных дел.

328 Глава 7. Другие пользовательские типы В наш ей бочке меда осталась только одна лож ка дегтя (на самом деле, всего лишь чайная ложечка), от которой надо избавиться. Всякий раз, когдати п, подобный Final, использует конструкцию a lia s get th is, необ­ ходимо уделять особое внимание собственным идентификаторам Final, перекрывающим одноименные идентификаторы, определенные в типе, псевдонимом которого становится Final. П редположим, мы используем тип Final!Widget, а класс Widget и сам определяет свойство get:

c l a s s Widget { p r iv a te i n t x;

eproperty i n t g e t () { return x;

} } u n ittest { auto w = Final!Widget(new Widget);

auto x = w.get;

/ / Получает Widget из Final, а не in t из Widget } Чтобы избеж ать таких коллизий, воспользуемся соглашением об име­ новании. Д ля надеж ности будем просто добавлять к именам видимых свойств имя соответствующ его типа:

s t r u c t Final(T) { p r iv a t e T Final_payload;

th is ( T bindTo) Final_payload = bindTo;

} / / Запретить присваивание e d is a b le void opAssign(Final);

/ / Сделать Final(T) подтипом T, / / не разрешив при этом перепривязывать payload 0property T F in al_ g e t() { return Final_payload;

} a l i a s Final_get th i s ;

} Соблюдение такого соглаш ения сводит к м инимум у риск непредвиден­ ны х коллизий. (Конечно, иногда мож но намеренно перехватывать не­ которые методы, оставив вызовы к ним за перехватчиком.) 7.1.11. Взаимное расположение полей. Выравнивание К ак располагаю тся поля в объекте-структуре? D очень консервативен в отнош ении структур: он располагает элементы их содержимого в том ж е порядке, в каком они указаны в определении структуры, но сохра­ няет за собой право вставлять м еж ду полями от ст упы ^padding). Рас­ смотрим пример:

stru ct А { char а;

i n t b;

char с;

} 7.1. Структуры Если бы компилятор располагал поля в точном соответствии с размера­ ми, указанными в структуре А, то адресом поля b оказался бы адрес объекта А плюс 1 (поскольку поле а типа char заним ает ровно 1 байт). Но такое расположение проблематично, ведь современные компьютерные системы извлекают данные только блоками по 4 или 8 байт, то есть мо­ гут извлекать только данные, расположенные по адресам, кратным и 8 соответственно. П редполож им, объект типа А расположен по «хоро­ шему» адресу, например кратному 8. Тогда адрес поля b точно окаж ется не в лучшем районе города. Чтобы извлечь b, процессору придется пово­ зиться, ведь нуж но будет «склеивать» значение b, собирая его из кусоч­ ков размером в байт. Усугубляет ситуацию то, что в зависимости от компилятора и низкоуровневой архитектуры аппаратного обеспечения эта операция сборки мож ет быть выполнена лиш ь в ответ на прерыва­ ние ядра «обращение к невыровненным данным», обработка которого требует своих (и немалых) накладны х расходов [28]. А это вам не семеч­ ки щелкать: такая дополнительная гимнастика легко сниж ает скорость доступа на несколько рорядков.



Pages:     | 1 |   ...   | 7 | 8 || 10 | 11 |   ...   | 15 |
 





 
© 2013 www.libed.ru - «Бесплатная библиотека научно-практических конференций»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.