Глава 19. Наборы
Программисты, работающие на языке Паскаль, обычно тратят
очень много времени на создание кода по манипулированию и обеспе-
чению структур данных, таких как связанные списки и массивы с ди-
намической установкой размеров. И очень часто один и тот же код
имеет тенденцию к повторному переписыванию и отладке.
Что касается традиционного языка Паскаль, он лишь предостав-
ляет вам встроенные типы записи и массива. Все другие структуры
остаются на ваше усмотрение. Например, если вы собираетесь хра-
нить данные в массиве, то обычно вам нужно написать код для соз-
дания массива, импорта данных в массив, получение данных массива
для обработки, и, возможно, вывода данных на устройство ввода-вы-
вода. Позднее, когда потребуется новый тип элемента массива, вы
начинаете все сначала.
Было бы замечательно, если бы тип массива поставлялся вместе
с кодом, обрабатывающего бы многие из тех операций, которые обыч-
но выполняются с массивом. Это был бы тип массива, который можно
было бы расширять без нарушения первоначального кода. Все это яв-
ляется целью создания типа ObjectWindows TCollection. Это объект,
который хранит наборы указателей и обладает набором методов по
манипулированию ими.
Объекты наборов
-----------------------------------------------------------------
Будучи объектами и тем самым имея встроенные методы, наборы
обладают двумя дополнительными чертами, которые имеют отношение к
обычным массивам языка Паскаль - это динамическое установка раз-
меров и полиморфизм.
Динамическая установка размеров наборов
-----------------------------------------------------------------
Размер стандартного массива в стандартном Паскале фиксирует-
ся во время компиляции. Хорошо, если вы точно знаете, какой раз-
мер должен иметь ваш массив, но это может быть не столь хорошо к
тому моменту, когда кто-нибудь будет запускать на вашу программу.
Изменение размера массива требует изменения исходного кода и пе-
рекомпиляции.
Однако, для наборов вы устанавливаете только их начальный
размер, который динамически увеличивается в процессе работы прог-
раммы, для размещения в нем всех нужных данных. Это делает ваше
приложение в его скомпилированном виде значительно более гибким.
Тем не менее, следует иметь в виду, что набор не может сжиматься,
поэтому следует быть аккуратным и не делать его неоправданно
большим.
Полиморфизм наборов
-----------------------------------------------------------------
Второй аспект, по которому массивы могут ограничивать ваше
приложение, состоит в том, что каждый элемент массива должен
иметь один и тот же тип, и этот тип должен быть определен при
компиляции кода.
Наборы обходят это ограничение использованием нетипизирован-
ных указателей. Это сказывается не только на быстроте и эффектив-
ности, но наборы могут состоять из объектов (и даже не из объек-
тов) разного типа и размера. Набору не нужно знать что-либо об
объектах, которые он обрабатывает. Он просто организует связь с
ними в случае необходимости.
Проверка типа и наборы
-----------------------------------------------------------------
Наборы ограничивают традиционную мощную проверку типа языка
Паскаль. Это означает, что можете поместить нечто в набор и когда
запрашиваете это назад, компилятор уже не может проверить ваших
предположений относительно объекта. Вы можете поместить нечто как
PHedgehog (еж), а прочитать назад как PSheep (овца), и набор ни-
как не сможет насторожить вас.
Как программист, работающий на языке Паскаль, вы вполне оп-
равданно будете нервничать по поводу результатов. Проверка типов
языка Паскаль в конце концов сберегает несколько часов при поиске
некоторых достаточно иллюзорных ошибок. Поэтому вы должны принять
во внимание следующее предупреждение. Вы даже представить себе не
можете насколько трудно бывает искать ошибки несоответствия типа,
поскольку обычно эту работу за вас выполнял компилятор! Однако,
если вы обнаружите, что ваша программа сбивается или зацикливает-
ся, тщательно проверьте хранимые типы объектов и считываемые из
наборов.
Объединение в набор элементов, не являющихся объектами
-----------------------------------------------------------------
Вы даже можете добавить в набор нечто, что вообще не являет-
ся объектом, но это также может явиться серьезным предметом оза-
боченности. Наборы ожидают получения нетипизированных указателей
незаданного типа на нечто. Но некоторые методы TCollection пред-
назначены специально для работы с наборами элементов, производных
от TObject. Это касается методов доступа к потоку PutItem и
GetItem, и стандартной процедуры FreeItem.
Например, это означает, что вы можете хранить PChar в набо-
ре, но при попытке послать этот набор в поток, результаты будут
не столь успешными, если вы не перепишете стандартные методы на-
бора GetItem и PutItem. Аналогично, при попытке освобождения на-
бора будет сделана попытка удаления каждого элемента с помощью
FreeItem. Например, это делает TStrCollection.
Если вам удастся преодолеть все эти трудности, вы обнаружи-
те, что наборы (и построенные вами производные наборов) являются
быстрыми, гибкими и надежными структурами данных.
Создание набора
-----------------------------------------------------------------
Создание набора столь же просто, как и создание типа данных,
которые вы хотите в нем хранить. Предположим, что вы - консуль-
тант, и вам нужно хранить и искать номер счета, фамилию и номер
телефона каждого из ваших клиентов. Сначала определим тип объекта
клиента (TClient), который будет хранится в наборе (не забудьте
определить тип указателя для каждого нового типа объекта):
type
PClient=^TClient;
TClient=object(TObject)
Account, Name, Phone: PChar;
constructor Init(NewAccount, NewName, NewPhone: PChar);
destructor Done; virtual;
procedure Print; virtual;
end;
Затем реализуем методы Init и Done для размещения и удаления
данных о клиенте и метод Print для отображения данных о клиенте в
виде таблицы. Обратите внимание, что поля объекта имеют тип
PChar, поэтому память выделяется только для той части строки, ко-
торая действительно используется. Функции StrNew и StrDispose
очень эффективно обрабатывают динамические строки.
constructor TClient.Init(NewAccount, NewName,
NewPhone: PChar);
begin
Account := StrNew(NewAccount);
Name := StrNew(NewName);
Phone := StrNew(NewPhone);
end;
destructor TClientDone;
begin
StrDispose(Account);
StrDispose(Name);
StrDispose(Phone);
end;
procedure TClient.Print;
begin
Writeln( ' ',
Account, '':10 - StrLen(Account),
Name, '':20 - StrLen(Name),
Phone, '':16 - StrLen(Phone));
end;
TClient.Done будет автоматически вызываться для каждого кли-
ента при удалении всего набора. Сейчас вы просто инициируете на-
бор для хранения ваших клиентов и вставляете в него записи о кли-
ентах. Головное тело программы (COLLECT1.PAS) будет выглядеть
следующим образом:
var
ClientList: PCollection;
begin
ClientList:=New(PCollection, Init(10,5));
with ClientList^ do
begin
Insert(New(PClient, Init('91-100', 'Anders, Smitty',
'(406) 111-2222')));
Insert(New(PClient, Init('90-167', 'Smith, Zelda',
'(800) 555-1212')));
Insert(New(PClient, Init('90-177', 'Smitty, John',
'(406) 987-4321')));
Insert(New(PClient, Init('90-160', 'Johnson, Agatha',
'(302) 139-8913')));
end;
PrintAll(ClientList);
SearchPhone(ClientList, '(406)');
Dispose(ClientList, Done);
end.
Примечание: Процедуры PrintAll и SearchPhone будут
рассмотрены позднее.
Обратите внимание, насколько просто было построить набор.
Первый оператор размещает новый экземпляр TCollection с именем
ClientList с начальным размером на 10 клиентов. В случае необхо-
димости размещения более 10 клиентов в ClientList, его размер бу-
дет увеличиваться каждый раз на 5 клиентов. Следующие два опера-
тора создают новый объект клиента и вставляют его в набор. Вызов
Dispose в конце операции освобождает весь набор клиентов.
Нигде не нужно было сообщать набору, какой вид данных пред-
полагается хранить - для этого просто используется указатель.
Методы итератора
-----------------------------------------------------------------
Вставка и удаление элемента не являются единственными общими
операторами набора. Очень часто вы будете писать циклы for для
просмотра всех объектов набора с целью отображения данных или вы-
полнения некоторых вычислений. В других случаях вы будете искать
первый или последний элемент набора, который удовлетворяет неко-
торому критерию поиска. Для этих целей у наборов имеется три ме-
тода итератора: ForEach, FirstThat и LastThat. Каждый из них
воспринимает указатель на процедуру или функцию в качестве своего
единственного параметра.
Итератор ForEach
-----------------------------------------------------------------
ForEach воспринимает указатель на процедуру. Процедура имеет
один параметр, который является указателем на хранимый в наборе
элемент. Для каждого элемента набора ForEach вызывает процедуру
один раз, в той последовательности, в которой элементы появляются
в наборе. Процедура PrintAll в Collect1 показывает пример итера-
тора FoeEach.
procedure PrintAll(C: PCollection);
procedure CallPrint(P: PClient); far;
begin
P^.Print; {Вызов метода Print}
end;
begin {Print}
Writeln;
Writeln;
Writeln('Client list:');
C^.ForEach(@CallPrint); { распечатка для каждого клиента }
end;
Для каждого элемента набора, переданного в качестве парамет-
ра в PrintAll, вызывается вложенная процедура CallPrint.
CallPrint просто распечатывает информацию об объекте клиента в
отформатированных колонках.
Примечание: Итераторы должны вызывать локальные проце-
дуры far.
Вам нужно быть аккуратным с сортировкой процедур, которые вы
вызываете итераторами. Для того, чтобы быть вызванной итератором,
процедура (в данном примере, CallPrint) должна:
* Быть процедурой - она не может быть функцией или методом
объекта, хотя данный пример показывает, что процедура мо-
жет вызвать метод.
* Быть локальной (вложенной) относительно вызывающей ее прог-
раммы.
* Описываться как дальняя процедура директивой far или ди-
рективой компилятора {$F+}.
* Воспринимать указатель на элемент набора в качестве своего
единственного параметра.
Итераторы FirstThat и LastThat
-----------------------------------------------------------------
Кроме возможности приложения процедуры к каждому элементу
набора, часто бывает очень нужно найти конкретный элемент набора
на основании некоторого критерия. Это является предназначением
итераторов FirstThat и LastThat. Как это следует из их имен, они
просматривают набор в противоположных направлениях до момента на-
хождения первого элемента набора, который удовлетворяет критерию
булевской функции, переданной в качестве элемента.
FirstThat и LastThat возвращают указатель на первый (или
последний) элемент, который удовлетворяет условию поиска. Предпо-
ложим, что в приведенном ранее примере списка клиентов, вы не мо-
жете вспомнить номер счета клиента или не помните точно написание
имени клиента. К счастью, вы точно помните, что это был ваш пер-
вый клиент из штата Монтана. Следовательно, вы можете организо-
вать поиск первого клиента с кодом штата 406 (поскольку ваш спи-
сок клиентов ведется хронологически). Данная процедура использует
метод FirstThat, который и сделает всю работу:
procedure SearchPhone(C: PCollection; PhoneToFind: PChar);
function PhoneMatch(Client: PClient: PClient): Boolean;
far;
begin
PhoneMatch := StrPos(Client^.Phone, PhoneToFind) <> nil;
end;
var
FoundClient: PClient;
begin { SearchPhone }
Writeln;
FoundClient := C^.FirstThat(@PhoneMatch);
if FoundClient = nil then
Writeln('Такому требованию не отвечает ни один клиент')
else
begin
Writeln('Найден клиент:');
FoundClient^.Print;
end;
end;
Снова обратите внимание на то, что PhoneMatch вложена и ис-
пользует удаленную модель вызова. В этом случае эта функция возв-
ращает True только при совпадении номера телефона клиента и за-
данного образца поиска. Если в наборе нет объекта, который соот-
ветствовал бы критерию поиска, FirstThat возвращает указатель
nil.
Запомните: ForEach вызывает определенную пользователем про-
цедуру, а FirstThat и LastThat каждая вызывает определенную поль-
зователем булевскую функцию. В любом случае определенная пользо-
вателем процедура или функция передают указатель на объект набо-
ра.
Отсортированные наборы
-----------------------------------------------------------------
Иногда вам бывает нужно, чтобы ваши данные были определенным
образом отсортированы. ObjectWindows имеет специальный тип набо-
ра, который позволяет вам упорядочить ваши данные произвольным
образом. Это тип TSortedCollection.
TSortedCollection является производным от TCollection и ав-
томатически сортирует задаваемые ему объекты. При добавлении но-
вого элемента он автоматически проверяет набор на дублирование
ключей. Булевское поле Duplicates контролирует разрешение дубли-
рования ключей. Если для поля Duplicates установлено значение
False (по умолчанию), то новый элемент добавляется к набору, за-
меняя существующий член с тем же самым ключом. Если Duplicates
имеет значение True, то новый член просто вставляется в набор.
TSortedCollection - это набор абстрактного типа. Для его ис-
пользования вы должны сначала решить, какой тип данных вы собира-
етесь собирать и определить два метода, отвечающих вашим конкрет-
ным требованиям сортировки. Для этого вам нужно создать новый
тип, производный от TSortedCollection. В данном случае назовем
его TClientCollection. Ваш TClientCollection уже знает, как де-
лать всю реальную работу с набором. Он может вставить (Insert)
запись о новом клиенте и удалять (Delete) существующие записи -
он унаследовал эти основные черты поведения от TCollection. Все
что нужно сделать - это научить TClientCollection, какое поле ис-
пользовать в качестве ключа сортировки и как сравнивать двух кли-
ентов при решении вопроса о том, какой из них должен стоять в на-
боре выше другого. Это делается переписыванием методов KeyOf и
Compare и реализации их следующим образом:
PClientCollection = ^TClientCollection;
TClientCollection = object(TSortedCollection)
function KeyOf(Item: Pointer): Pointer; virtual;
function Compare(Key1, Key2: Pointer): Integer; virtual;
end;
function TClientCollection.KeyOf(Item: Pointer): Pointer;
begin
KeyOf := PClient(Item)^.Account;
end;
function TClientCollection.Compare(Key1, Key2: Pointer):
Integer;
begin
Compare := StrIComp(PChar(Key1), PChar(Key2));
end;
Примечание: Так как ключи являются нетипизированными
указателями, для них нужно выполнять приведение типа.
KeyOf определяет, какое поле или поля используются в качест-
ве ключей сортировки. В данном случае это поле клиента Account.
Compare воспринимает два ключа сортировки и определяет, какой из
них должен идти первым в соответствии с правилами сортировки.
Compare возвращает -1, 0 или 1 в зависимости от того, Key1 мень-
ше, равен или больше Key2, соответственно. В данном примере ис-
пользуется сортировка по алфавиту (для букв верхнего и нижнего
регистра) ключевой строки (Account) путем вызова модуля Strings
функции StrIComp. Вы можете легко сортировать набор по именам,
вместо номера счета, если замените возвращаемое KeyOf поле на
Name.
Обратите внимание на то, что ключи, возвращаемые KeyOf и пе-
редаваемые в Compare являются нетипизированными указателями, поэ-
тому до их разыменования и передачи в StrIComp в данном примере
вы должны привести их тип к PChar.
Это практически все, что вам нужно определить! Теперь, если
вы переопределите ClientList как PClientCollection вместо
PCollection (сменив объявление var и вызов New), то легко сможете
распечатать ваших клиентов в алфавитном порядке (COLLECT2.PAS):
var
ClientList: PClientCollection;
.
.
begin
ClientList:=New(PClientCollection, Init(10,5));
.
.
end.
Обратите внимание и на то, как легко будет сменить сортиров-
ку списка клиентов по номеру счета на сортировку по имени. Все
что вам нужно сделать, это сменить метод KeyOf на возврат поля
Account на поле Name.
Наборы строк
-----------------------------------------------------------------
Многим программам требуется работать с отсортированными
строками. Для этих целей ObjectWindows предоставляет набор специ-
ального назначения TStrCollection (он совпадает с типом
TStringCollection, определенным для хранения строк Паскаля). Об-
ратите внимание, что элементы TStrCollection - это не объекты.
Они представляют собой указатели на строки, заканчивающиеся ну-
лем. Поскольку наборы строк происходят от TSortedCollection, мож-
но хранить и дублированные строки.
Использовать наборы строк несложно. Просто определяется пе-
ременная указателя для хранения набора строк. Разместим набор,
задав его начальный размер и приращение для роста при добавлении
новых строк (см. COLLECT3.PAS):
var
WordList: PCollection;
WordRead: PChar;
.
.
.
begin
WordList:=New(PStrCollection, Init(10,5));
.
.
.
WordList первоначально рассчитан для хранения 10 строк с
последующим приращением по 5 строк. Все что вам нужно сделать -
это вставить несколько строк в набор. В данном примере слова счи-
тываются из текстового файла и вставляются в набор:
repeat
.
.
.
if GetWord(WordRead, WordFile)^ <> #0 then
WordList^.Insert(StrNew(WordRead));
.
.
.
until WordRead[0]=#0;
.
.
.
Dispose(WordList, Done);
Обратите внимание, что функция StrNew используется для копи-
рования считанных слов, и адрес скопированной строки передается в
набор. При использовании набора вы всегда передаете ему контроль
над данными набора. Он позаботится об освобождении данных после
работы. Он при этом делает то, что происходит при вызове Dispose:
удаляется каждый элемент набора, и затем удаляется сам набор
WordList.
Пересмотренные итераторы
-----------------------------------------------------------------
Метод ForEach просматривает весь набор, элемент за элемен-
том, и выполняет над каждым из них заданную процедуру. В предыду-
щем примере процедуре PrintWord передавался указатель строки для
ее отображения. Обратите внимание, что процедура PrintWord вло-
женная (или локальная). Она работает в другой процедуре, Print,
которой передается указатель на TstrCollection. Print использует
метод итератора ForEach для передачи каждого элемента своего на-
бора в процедуру PrintWord.
procedure Print(C: PCollection);
procedure PrintWord(P: PChar); far;
begin
Writeln(P); { вывести строку }
end;
begin {Print}
Writeln;
Writeln;
C^.ForEach(@PrintWord); { вызов PrintWord }
end;
PrintWord должен выглядеть как уже знакомая процедура. Она
просто берет указатель строки и передает его значение Writeln.
Обратите внимание на директиву far после описания PrintWord.
PrintWord не может быть методом, это просто процедура. Кроме того
это должна быть вложенная процедура. Print надо рассматривать как
некую оболочку вокруг процедуры, которая выполняет некоторую ра-
боту над каждым элементом набора (может быть отображает или моди-
фицирует данные). Вы можете иметь несколько аналогичных PrintWord
процедур, но каждая из них должна быть вложена в Print и должна
быть дальней процедурой (использовать директиву far или {$F+}).
Нахождение элемента
Отсортированные наборы (и следовательно наборы строк) имеют
метод Search, который возвращает индекс элемента с конкретным
значением ключа. Но как найти элемент в неотсортированном наборе?
Или когда критерий поиска не использует сам ключ? Конечно же,
следует использовать FirstThat и LastThat. Вы просто определяете
булевскую функцию для проверки нужного вам критерия и вызываете
FirstThat.
Полиморфические наборы
-----------------------------------------------------------------
Как вы уже видели, что наборы могут динамически хранить лю-
бой тип данных, и они обладают множеством методов, которые помо-
гают вам организовывать эффективный доступ к данным. В действи-
тельности сам TCollection определяет 23 метода. Когда вы исполь-
зуете наборы в ваших программах, вы будете удивлены скоростью их
работы: они разработаны с максимальной гибкостью и реализованы
для использования с максимальной скоростью.
Теперь пришло время рассмотреть реальные возможности набо-
ров, элементы могут обрабатываться полиморфически. Это значит,
что вы не просто можете хранить определенный тип объекта в набо-
ре; вы можете хранить несколько разных типов объектов, взятых
произвольно из вашей иерархии объектов.
Если вы рассмотрите приведенные примеры наборов, вы можете
заметить, что все элементы каждого набора были одно и того же ти-
па. Мы имели дело со списком строк в котором каждый элемент был
строкой. Мы также занимались списком клиентов. Но наборы могут
хранить любые производные от TObject объекты, и вы можете произ-
вольно смешивать эти объекты. Естественно, что вы желаете, чтобы
эти объекты имели нечто общее. На самом деле вам нужно, чтобы у
них был общий абстрактный объект-предок.
В качестве примера рассмотрим программу, которая помещает в
набор три различных графических объекта. Затем итератор ForEach
используется для просмотра набора и отображения каждого объекта.
В отличие от других примеров данной главы данный пример
(Collect4) использует функции Windows для рисования в окне. Обя-
зательно включите WinProcs и WinTypes в uses данного примера.
Сначала определяется абстрактный объект-предок (см.
COLLECT4.PAS).
type
PGraphObject = ^TGraphObject;
TGraphObject = object(TObject)
Rect: TRect;
constructor Init(Bounds: TRect);
procedure Draw(DC: HDC); virtual;
end;
Из этого объявления вы можете видеть, что каждый графический
объект может инициализировать себя (Init) и отобразить себя на
графическом экране (Draw). Теперь определим эллипс, прямоугольник
и сектор как производные от этого общего предка:
PGraphEllipse = ^TGraphEllipse;
TGraphEllipse = object(TGraphObject)
procedure Draw(DC: HDC); virtual;
end;
PGraphRect=^TGraphRect;
TGraphRect=object(TGraphObject)
procedure Draw(DC: HDC); virtual;
end;
PGraphPie = ^TGraphPie;
TGraphPie = object(TGraphObject)
ArcStart, ArcEnd: TPoint;
constructor Init(Bounds: TRect);
procedure Draw(DC: HDC); virtual;
end;
Все эти три типа объекта наследуют поле Rect из
TGraphObject, но все они разного размера. TGraphEllipse и
TGraphRect нужно только добавить их новые методы рисования, т.к.
их методам рисования нужны только размеры и расположение, а
TGraphPie нужны дополнительные поля и другой конструктор для их
корректного представления. Приведем исходный код для помещения
этих фигур в набор:
.
.
.
GraphicsList := New(PCollection, Init(10,5));
{ создать набор }
for I := 1 to NumToDraw do
begin
case I mod 3 of { создать объект }
0: P := New(GraphRect, Init(Bounds));
1: P := New(GraphEllipse, Init(Bounds));
2: P := New(GraphPie, Init(Bounds));
end;
GraphicsList^.Insert(P); { добавить в набор }
end;
.
.
Как вы можете видеть цикл, for вставляет графические объекты
в набор GraphicsList. Вы знаете только то, что каждый объект в
GraphicsList представляет собой некоторый вид TGraphObject. После
помещения в набор у вас уже нет информации о том, является ли
элемент набора прямоугольником, эллипсом или сектором. Благодаря
полиморфизму, вам этого и не нужно знать, поскольку каждый объект
содержит все данные и код (Draw), который ему нужен. Просмотрим
набор с использованием итеративного метода и каждый набор будет
сам отображать себя:
procedure DrawAll(C: PCollection);
procedure CallDraw(P: PGraphObject); far;
begin
P^.Draw(PaintDC); { вызов метода Draw }
end;
begin {DrawAll}
C^.ForEach(@CallDraw); { прорисовать каждый объект }
end;
var
GraphicsList: PCollection;
begin
.
.
.
if GraphicsList <> nil then DrawAll(GraphicsList);
.
.
.
end.
Способность наборов хранить разные, но связанные объекты ос-
новывается на мощном краеугольном камне объектно-ориентированного
программирования. В следующей главе вы увидите тот же принцип по-
лиморфизма, примененный к потокам с равными приоритетами.
Наборы и управление памятью
-----------------------------------------------------------------
TCollection может динамически расти от начального размера,
установленного Init, до максимального размера в 16380 элементов.
ObjectWindows хранит максимальный размер набора в переменной
MaxCollectionSize. Каждый добавляемый в набор элемент занимает
четыре байта памяти, т.к. он хранится в виде указателя.
Ни одна библиотека динамических структур данных не будет
полной, если она не снабжена средствами обнаружения ошибок. Если
для инициализации набора не хватает памяти, то возвращается ука-
затель nil.
Если не хватает памяти при добавлении элемента в набор, то
вызывается метод TCollection.Error, и возникает ошибка этапа вы-
полнения в динамически распределяемой области памяти. Вы можете
переписать TCollection.Error для организации собственного метода
информирования или исправления ошибки.
Вам следует уделить особое внимание доступности динамической
области памяти, поскольку у пользователя имеет значительно боль-
ший контроль над программой ObjectWinodws, чем над обычной прог-
раммой языка Паскаль. Если добавлением объектов в набор управляет
пользователь (например, открывая новое окно), то ошибку динами-
ческой области памяти не так то легко предсказать. Вы можете
предпринять некоторые шаги по защите пользователя от фатальной
ошибки при выполнении программы либо проверяя память при исполь-
зовании набора, либо обрабатывая сбой выполняемой программы таким
образом, чтобы избежать прекращения ее работы.
Назад | Содержание | Вперед