четверг, 29 апреля 2010 г.

Absolute(но) для начинающих

Это перевод Absolute (for) Beginners. Блог: Te Waka o Delphi.

Сегодня я случайно предложил использование ключевого слова "absolute" в ответе на вопрос в списке рассылок NZ DUG. Я сразу забыл об этом, но потом кто-то упомянул, что он не видел, как использовали это ключевое слово, вот уже много лет. Поэтому я подумал, что, может быть, будет целесообразным довести его до более широкого внимания.

absolute - это одна из тех возможностей языка, которые, я уверен, поделят всех на два лагеря: тех, кто считают его полезным инструментом и тех, кто считает, что его нужно избегать в современном мире (вроде как goto). Лично я твёрдо состою в первом лагере.

absolute позволяет вам указать компилятору рассматривать одну или несколько переменных как существующие в одном месте в памяти, вроде вариантных записей, когда одна переменная перекрывает другую.

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

Наверное, пример сможет указать, что я имел тут ввиду.

Представьте обработчик события, который отвечает за изменения во множестве элементов управления, когда точный тип элемента управления не важен, но реакция обработчика зависит от него. Это довольно обычный шаблон, когда вы не хотите заводить множество обработчиков событий. Давайте, для примера, все редактируемые элементы управления на форме должны вводить текст только в верхнем регистре (пожалуйста, имейте ввиду, что это просто пример; не надо к нему придираться):
procedure TMyForm.EditableChange(Sender: TObject);
begin
  if Sender is TEdit then
  begin
    TEdit(Sender).Text := Uppercase(TEdit(Sender).Text);
  end
  else if Sender is TMemo then
  begin
    TMemo(Sender).Text := Uppercase(TMemo(Sender).Text);
  end
  else if (Sender is TComboBox) and (TComboBox(Sender).Style = csDropDown) then
  begin
    TComboBox(Sender).Text := Uppercase(TComboBox(Sender).Text);
  end;

  SetDirtyFlag(TRUE);
end;
Итак, первым способом улучшить этот код могло бы быть:
procedure TMyForm.EditableChange(Sender: TObject);
var
  edit: TEdit;
  memo: TMemo;
  combo: TComboBox;
begin
  if Sender is TEdit then
  begin
    edit := TEdit(Sender);
    edit.Text := Uppercase(edit.Text);
  end
  else if Sender is TMemo then
  begin
    memo := TMemo(Sender);
    memo.Text := Uppercase(memo.Text);
  end
  else if (Sender is TComboBox) then
  begin
    combo := TComboBox(Sender);
    if (combo.Style = csDropDown) then
    begin
      combo.Text := Uppercase(combo.Text);
    end;
  end;

  SetDirtyFlag(TRUE);
end;
Я не знаю, как вы, но я думаю, что в этом случае лекарство оказалось хуже болезни. Тогда давайте теперь посмотрим, как "absolute" сделает эту попытку улучшения намного более успешной:
procedure TMyForm.EditableChange(Sender: TObject);
var
  control: TObject;
  edit: TEdit absolute control;
  memo: TMemo absolute control;
  combo: TComboBox absolute control;
begin
  control := Sender;
  if Sender is TEdit then
  begin
    edit.Text := Uppercase(edit.Text);
  end
  else if Sender is TMemo then
  begin
    memo.Text := Uppercase(memo.Text);
  end
  else if (Sender is TComboBox) and (combo.Style = csDropDown) then
  begin
    combo.Text := Uppercase(combo.Text);
  end;

  SetDirtyFlag(TRUE);
end;
Второй вариант настолько же типобезопасен, как и первый - проверка типов производится в тех же местах. Отличие в том, что приведение типов теперь выполняется при самом объявлении переменных, поэтому вам не нужно его делать руками в коде.

Ключевое слово absolute говорит компилятору, чтобы он трактовал переменные edit, memo и combo просто как разные виды одной и той же переменной control.

Но тут есть что-то большее - лично для меня, это также является чётким указанием на то, как эти переменные будут использоваться в теле процедуры (причём, прямо в объявлении). В то время как в первом примере этого не видно, без изучения кода метода.

Мне нравится код, который объясняет сам себя - это означает, что мне не нужно писать много в комментариях!

Теперь, фактически, мы можем сделать всё прозрачнее для понимания и более компактным, поскольку, равно как и "перекрывать" переменные, absolute может "перекрывать" и параметры процедуры, так что мы можем избавится от отвлекающей переменной control:
procedure TMyForm.EditableChange(Sender: TObject);
var
  edit: TEdit absolute Sender;
  memo: TMemo absolute Sender;
  combo: TComboBox absolute Sender;
begin
  if Sender is TEdit then
    edit.Text := Uppercase(edit.Text);
  else if Sender is TMemo then
    memo.Text := Uppercase(memo.Text);
  else if (Sender is TComboBox) and (combo.Style = csDropDown) then
    combo.Text := Uppercase(combo.Text);

  SetDirtyFlag(TRUE);
end;
Как я уже сказал, это не очень красивый и очень надуманный пример, но я хотел использовать что-то простое для представления идеи, прежде чем немного усложнять вещи.

Изменение сущности шизофреничных типов


Если вам когда-либо приходилось работать с ресурсами в своих приложениях - иконками, растровыми изображениями и т.д. - вы, несомненно, знакомы с MAKEINTRESOURCE(). Эта маленькая функция преобразует 16-битный идентификатор ресурса в значение, которое может быть использовано вместо указателя на название ресурса в некоторых функциях.

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

Возможно, это неудобно, но absolute позволяет легко и, опять же, прозрачно указать цель вашего кода, без необходимости явно выполнять код для выполнения преобразования:
function GetResourceIDAsString(const aID: PChar): String;
var
  id: Integer absolute aID;
begin
  if HiWord(aID) = 0 then
    Result := IntToStr(id)
  else
    Result := aID;
end;
Заметьте: проверка старших 16-ти битов параметра может быть записана в следующей эквивалентной форме:
if id <= MAXWORD then
Заметьте, как эта техника обеспечивает безопасность типов для передаваемого параметра (идентификатор ресурса, передаваемого как PChar) без привлечения преобразований типов в коде процедуры. Для сравнения: вот обычный подход к этой проблеме без использования absolute:
function GetResourceIDAsString(const aID: PChar): String;
var
  dw: DWORD;
begin
  dw := DWORD(aID);
  if HiWord(dw) = 0 then
    Result := IntToStr(LoWord(dw))
  else
    Result := aID;
end;
Ну, может разница и не слишком велика (хотя на этот раз это пример из реальной жизни), но мы всё же объявили переменную и использовали преобразование типов без особой на то причины (в смысле назначения функции), что просто позволило нам работать с переданным значением другим способом. Я думаю, что у вас может быть желание убрать переменную и просто добавить ещё больше преобразований типов:
function GetResourceIDAsString(const aID: PChar): String;
begin
  if HiWord(DWORD(aID)) = 0 then
    Result := IntToStr(LoWord(DWORD(aID)))
  else
    Result := aID;
end;
Что несколько короче и экономнее, но... видимо, дело в том, что некоторые люди просто любят такой код, с множеством повторений и нагромождением скобок. Лично я - нет.

К концу начала

Как я уже сказал вначале, всё это началось, когда я ответил на вопрос в списке рассылки NZ DUG. Вопрос был о том, как воспользоваться преимуществами языковой конструкции for in, представленной в Delphi 2007. Тот случай касался попытки использование for in для перебора содержания экземпляра класса-наследника от TObjectList - то есть свой энумератор для класса ещё не был создан (и имейте в виду, что дженерики не были доступны, потому что Delphi версия в вопросе не имела их поддержки в Win32). Человек в вопросе спрашивал почему следующий код даёт ошибки компиляции и как их избежать (я немного изменил и упростил код):
procedure PrintFormats( aFormatList: TAddressFormatList );
var
  addrFormat: TAddressFormat;
begin
  for addrFormat in addrFormats do
    WriteLn( addrFormat.Description );
end;
Поскольку TAddressFormatList наследуется от TObjectList, то этот код не компилируется из-за ошибок несовместимости типов, поскольку перечислитель для TObjectList возвращает Pointer (прим. пер.: наследуется от TList), а не TAddressFormat. Вопрос был задан человеком, который видел демонстрацию этой возможности языка, где цикл for in был написан для обработки содержимого экземпляра TStringList, но он при этом не понял механизм, который делает это возможным, не понял, что для этого нужна инфраструктура, которую кто-то должен написать, чтобы for in работал (которая, конечно же, в случае TStringList и некоторых других типов, уже написана авторами RTL/VCL). Вы, конечно, можете делать так, как требует от вас TObjectList: делать перечисление, используя переменную типа Pointer, приводя её тип к нужному объекту в теле цикла:
procedure PrintFormats(aFormatList: TAddressFormatList);
var
  addrFormat: Pointer;
begin
  for addrFormat in aFormatList do
    WriteLn(TAddressFormat(addrFormat).Description);
end;
Но если вам нужно в теле цикла обращаться к addrFormat несколько раз, то это весьма хлопотно. Поэтому вы захотите ввести другую, строго типизированную переменную, так что вы сделаете преобразование типа только один раз при присвоении и в дальнейшем будете использовать только вторую переменную:
procedure PrintFormats(aFormatList: TAddressFormatList);
var
  addrFormatEnum: Pointer;
  addrFormat: TAddressFormat;
begin
  for addrFormatEnum in aFormatList do
  begin
    addrFormat := TAddressFormat(addrFormatEnum);
    WriteLn(addrFormat.Description);
  end;
end;
Но мы уже видели, как absolute может помочь с упрощением таких ситуаций, делая отношение между двумя переменными чётко выраженными при их объявлении:
procedure PrintFormats(aFormatList: TAddressFormatList);
var
  addrFormatEnum: Pointer;
  addrFormat: TAddressFormat absolute addrFormatEnum;
begin
  for addrFormatEnum in aFormatList do
    WriteLn(addrFormat.Description);
end;
Вот и всё. Как я уже сказал, ожидайте, что к этой заметке будут комментарии, указывающие на опасности использования этой техники, но я считаю, что эти опасности не превышают (а иногда и меньше) тех, которые вы получаете, используя все эти безусловные приведения типов. К преимуществам absolute также относится уменьшение "шума" в коде, а также оно делает назначение переменных более ясным, без необходимости чтения кода. absolute, хотя бы, может стать еще одним инструментом в вашем ящике с инструментами. Или, оно может быть инструментом, о котором вы даже не знали, что он у вас есть.

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

Примечание переводчика: в случае с for in использование absolute является лишь обходным решением. Если вам нужно делать for in во многих местах, то правильным решением будет написание энумератора для класса, а не использовать каждый раз доп. переменную.

5 комментариев:

  1. Спасибо за перевод. Использование absolute в for..in выглядит привлекательно :) Я создавал классы, единственным назначением которых было создание Enumerator-а для дальнейших for..in

    ОтветитьУдалить
  2. Классная статья. С удовольствием читаю все ваши переводы.

    ОтветитьУдалить
  3. Отличная статья. Спасибо!

    ОтветитьУдалить
  4. абсолютный контроль!!! (absolute control x3)

    ОтветитьУдалить

Можно использовать некоторые HTML-теги, например:

<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>

Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку (поддерживается OpenID).

Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.

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