среда, 20 августа 2014 г.

Коротко. О возбуждении исключений

Ни для кого наверное не открою Америку, но всё же напишу.

Можно сделать так:

type
 EmyException = class(Exception)
 end;//EmyException
...
if not Condition1 then
 raise EMyException.Create('Some string1');
...
if not Condition2 then
 raise EMyException.Create('Some string2');

А можно так:

type
 EmyException = class(Exception)
  public
   class procedure Check(aCondition: Boolean; 
                         const aMessage: String);
 end;//EmyException
...
class procedure EmyException.Check(aCondition: Boolean; 
                                   const aMessage: String);
begin
 if not aCondition then
  raise Self.Create(aMessage);
end;
...
EMyException.Check(Condition1, 'Some string1');
...
EMyException.Check(Condition2, 'Some string2');

Вроде "то же на то же", и оба варианта - одинаковые.

Но по мне - второй вариант - "вкуснее". Да и читабельнее.

И в отладке - полезнее.

Почему в отладке полезнее? Потому, что можно поставить один break-point в EmyException.Check, а не множество по коду.

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

И его понятное дело - можно расширять и усовершенствовать.

Например - не передавать строку, а "генерировать" её внутри Check. Ну в общем - вариантов масса.

Ну и ещё отмечу - я наблюдал достаточное количество людей, которые забывали raise.

Т.е. писали так:

 Exception.Create('aMessage');

а не так:

 raise Exception.Create('aMessage');

Ну и про "расширения".

Можно например так:

type
 TMyPredicate = reference to function (aData: Integer): Boolean;
 EmyException = class(Exception)
  public
   class procedure Check(aCondition: Boolean; 
                         const aMessage: String); overload;
   class procedure Check(aCondition: TMyPredicate; 
                         const aMessage: String; 
                         aData: Integer); overload;
 end;//EmyException
...
class procedure EmyException.Check(aCondition: Boolean; 
                                   const aMessage: String);
begin
 if not aCondition then
  raise Self.Create(aMessage);
end;

class procedure EmyException.Check(aCondition: TMyPredicate; 
                                   const aMessage: String; 
                                   aData: Integer);
begin
 if not aCondition(aData) then
  raise Self.Create('InvalidData: ' + IntToStr(aData) + aMessage);
end;

...
EMyException.Check(Condition1, 'Some string1');
...
EMyException.Check(
 function (aData: Integer): Boolean; 
 begin 
  Result := IsValid(aData); 
 end;, 
 'Some string2', 
 SomeComplexExpression);

-- ну понятное, дело, что это "макет", а не реально рабочий код.

Чем этот "макет" хорош? Тем, что SomeComplexExpression - вычислится один раз.

Понятное дело, что можно и "локальной переменной" обойтись.
Ну это в простейших случаях. 

Да и потом, ведь можно написать и так:

var
 SomeLocalData : Integer;

EMyException.Check(
 function (aData: Integer): Boolean; 
 begin 
  Result := (aData = SomeLocalData); 
 end;, 
 'Some string2', 
 SomeComplexExpression);

Update. 

По мотивам комментария - http://programmingmindstream.blogspot.ru/2014/08/blog-post_85.html?showComment=1408566592655#c1216957169742679866

Про "просто процедуру" - мы конечно же тоже ими пользовались:

function Ht(ID : LongInt) : LongInt;
{var
 nDosError : SmallInt; // Сюда занесут код, возвращенный ДОС
 nOperation: SmallInt; // Сюда занесут код операции, приведшей к ошибке
 lErrstr : array[0..1000] of AnsiChar;
 lErrstr2 : PAnsiChar;
}
begin
 Result := ID;

 if lNeedStackOut_ErrNum <> 0 then
 begin
  l3System.Stack2Log(Format('HTERROR = %d STACK OUT', [lNeedStackOut_ErrNum]));
  lNeedStackOut_ErrNum := 0;
 end;

{ if ID = -1 then
  lErrstr2 := htExtError(nDosError, nOperation, @lErrstr[0]);
}
 if ID < 0 then
  raise EHtErrors.CreateInt(ID);
end;
....
   Ht(htOpenResults(Masks,ROPEN_READ,@FldArr,FldCount));
....
     Ht(htDeleteRecords(TmpList));
....
     Ht(htOpenResults(ValList,ROPEN_READ,nil,0));

И ещё.

Что случится, если мы напишем так:

type
 EmyException2 = class(EmyException)
 end;//EmyException2

...
EmyException2.Check(aCondition, aMessage);

-- исключение какого класса возбудится?

EmyException или EmyException2?

EmyException2 :-) что и "следовало ожидать".

Это к вопросу - "почему метод класса, а не просто функция".

Update.

(В некотором роде в ответ на - http://programmingmindstream.blogspot.ru/2014/08/blog-post_85.html?showComment=1408649403323#c8271514992700352647)

Вот кстати пример, того что написано в самом начале:

Em3InvalidStreamPos.Check(Self.IsValidPosition,
                          aHeader.f_Name,
                          l_Pos);
Em3InvalidStreamSize.Check(Self.IsValidPosition,
                           aHeader.f_Name,
                           aHeader.f_TOCItemData.rBody.rRealSize);
Em3InvalidStreamPos.Check(Self.IsValidLink,
                          aHeader.f_Name,
                          aHeader.f_TOCItemData.rBody.RTOCBuffRootPosition);
Em3InvalidStreamPos.Check(Self.IsValidLink,
                          aHeader.f_Name,
                          aHeader.f_TOCItemData.rBody.RTOCItemListPosition);
Em3InvalidStreamPos.Check(Self.IsValidLink,
                          aHeader.f_Name,
                          aHeader.f_TOCItemData.RNextPosition);

- IsValidPosition и IsValidLink это предикаты.

Т.е. function (aData: Int64): Boolean of object;

Check в данном случае выглядит так:

type
 TInt64Predicate = function (aData: Int64): Boolean of object;
...
class procedure Em3InvalidStreamData.Check(aCondition: TInt64Predicate;
                                           aName : String;
                                           aData : Int64);
begin
 if not aCondition(aData) then
  raise Self.CreateFmt('Invalid data %d in file %s', [aName, aData]);
end; 
...
Em3InvalidStreamPos = class(Em3InvalidStreamData);

Em3InvalidStreamSize = class(Em3InvalidStreamData);

-- зачем ещё это нужно?

Ну чтобы с форматками в функции Format не напутать.

И не отлаживать "наведённый" exception который вылезет у пользователя, но "не расскажет правду".

Понятное дело, что пример кода - далеко не самый "красивый", но я специально ничего не рафинировал.

Конечно - на вкус и цвет - "все фломастеры разные", но мне лично это нравится больше, чем с "вязанкой" raise.

Попробуйте написать с raise и покажите - что он "будет короче". Я буду рад поучиться.

И кстати да - вывод стека в лог - помогает идентифицировать строку - откуда полетело исключение. Но про это я может быть ещё когда-нибудь напишу отдельно.

(Пока вот тут - идёт некоторая дискуссия)

Update.

Да и оговорюсь.

Можно было бы конечно "сочинить" что-то вроде:

Em3InvalidStreamPos.Check(Self.IsValidPosition,
                          aHeader.f_Name,
                          [l_Pos,
                           aHeader.f_TOCItemData.rBody.rRealSize,
                           aHeader.f_TOCItemData.rBody.RTOCBuffRootPosition,
                           aHeader.f_TOCItemData.rBody.RTOCItemListPosition,
                           aHeader.f_TOCItemData.RNextPosition]);

-- но я осознанно этого не делаю.

Т.к. конечно запись короче и "читабельнее".

Но! В таком варианте - сложнее искать реальный источник ошибки.

Ну и with конечно тоже можно было бы написать. Я знаю. Но я лично - "идеологический противник" with.

Особенно в варианте Delphi, где нет "обратной устойчивости".

Слова "обратная устойчивость" понятны? Или я опять "выдумываю свои термины"?

На всякий случай написал вот что - Коротко. Об "обратной устойчивости"

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

  1. Хорошая штука. Мне понравилось. Как-то не приходил в голову такой подход.

    ОтветитьУдалить
    Ответы
    1. ;-) ну надо же! :-) понравилось. Пользуйся на здоровье :-)

      Удалить
    2. Ром, а я ведь сомневался - писать или нет. Боялся показаться банальным. :-)

      Удалить
  2. Классовая процедура - это хорошо, в последнее время я им отдаю предпочтение. А касательно исключений - идея очень хорошая, в сторонних библиотеках я такого не встречал, обычно используется просто процедура...

    ОтветитьУдалить
    Ответы
    1. "обычно используется просто процедура..."

      :-) это мы тоже "проходили"

      понятно ведь, чем "просто процедура" хуже?

      тем, что наследования нет :-)

      Удалить
    2. "Классовая процедура - это хорошо, в последнее время я им отдаю предпочтение"

      -- я тоже. Ведь "классовая процедура" это по сути "фабричный метод" :-)

      Удалить
    3. http://18delphi.blogspot.ru/2013/04/blog-post_7483.html

      Удалить
    4. "Да и кеширование в фабричный метод всегда можно потом безболезненно воткнуть, или логирование, ну или бизнес-логику какую. Или например граничные условия обработать."

      Удалить
  3. Круто, я про Exception никогда не думал, так его расширять.

    ОтветитьУдалить
  4. Единообразие нарушается. Везде raise, а тут вдруг Check().

    ОтветитьУдалить
  5. Отличная идея!

    И кстати, принимая во внимание комментарий pda, я б назвал этот метод raiseIfNotTrue или raise (в идеале), но не факт что компилятор позволит.

    ОтветитьУдалить
  6. Насчёт "единообразия" верно заметил один мой знакомый - "если довести до абсурда - все функции нарушают единообразие, так как вместо везде используемых ключевых слов языка начинаем использовать какие-то пользовательские функции". И я с этим - полностью согласен.

    ОтветитьУдалить
  7. Всё ниже - моё скромное IMHO.
    Никого не критикую, пытаюсь понять, как мог бы сам это использовать...
    <code title='Вариант A'>
    EMyException.Check(
      function (aData: Integer): Boolean; 
      begin
        Result := IsValid(aData); 
      end;, 
      'Some string2', 
      SomeComplexExpression);
    </code>
    vs
    <code title='Вариант B'>
      if IsValid(SomeComplexExpression) then
        raise EmyException.Create('Some string2');
    </code>
    Вариант B представляется более компактным (в три раза) и, как минимум, не проигрывает в наглядности.
    Интересно понять стремление к группировке различных ситуаций в один класс исключения.
    Классический подход подразумевает идентифицируемость исключений, например, для удобства их обработки. Блок except при обработке исключений, в частности, заточен под это, позволяя фильтровать исключения по типам с учётом их иерархии.
    В идеале (т. е. я следую этому не всегда) это означает, что каждой исключительной ситуации в коде сопоставлен соответствующий тип исключения. Мне всегда казалось, что следование этой схеме не сулит неудобств.
    Здесь же, как мне показалось, предлагается следовать обратной схеме: исключение одного типа возбуждается в различных контекстах. Да, и здесь можно обеспечить идентификацию (с помощью «кода ошибки» в EMyException, например), но при стандартном подходе это уже есть «из коробки».

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

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