четверг, 2 октября 2014 г.

Коротко. Про контроль типов

По мотивам:

Коротко. Про IStorage
Коротко. "Почему нужны тесты"

Я уже привык к тому, что выступаю в роли "капитана очевидность", но всё же - не могу не написать.

В Delphi - всё неплохо с контролем типов, но если только эти типы не атомарные.

А вот с атомарными типами - есть "шероховатости".

(Оговорюсь сразу - пишу не про "коня в вакууме", а "с колёс", про реальные проблемы, выявленные в процессе отладки и рефакторинга)

Попробую пояснить - что я имею в виду.

Пусть у нас есть объект-менеджер, который умеет распределять два типа ресурсов (в пределе - N).

Например этот объект выделяет блоки (двух разных типов) на файловой системе (потому и Int64).

И пусть он выглядит так:

type
 TmyResource = Int64;

 TmyAllocator = class
  public
   class function AllocResource1: TmyResource; 
   class function AllocResource2: TmyResource; 
   class procedure FreeResource1(var theResource: TmyResource); 
   class procedure FreeResource2(var theResource: TmyResource); 
 end;//TmyAllocator

-- в чём тут потенциальная проблема?

А в том, что можно написать так:

var
 l_Res : TmyResource;
...
begin
 l_Res := TmyAllocator.AllocResource1;
 ...
 TmyAllocator.FreeResource2(l_Res);
end;

-- Т.е. распределили ресурс одного типа, а освобождаем - ресурс другого типа.

И компилятор тут нам - "не помощник".

И про проблемы мы можем узнать лишь по "косвенным признакам". Типа AV в Run-time или в лучшем случае - Assert.

Что можно сделать?

Можно попытаться написать так:

type
 TmyResource1 = Int64;
 TmyResource2 = Int64;

 TmyAllocator = class
  public
   class function AllocResource1: TmyResource1; 
   class function AllocResource2: TmyResource2; 
   class procedure FreeResource1(var theResource: TmyResource1); 
   class procedure FreeResource2(var theResource: TmyResource2); 
 end;//TmyAllocator

var
 l_Res : TmyResource1;
...
begin
 l_Res := TmyAllocator.AllocResource1;
 ...
 TmyAllocator.FreeResource2(l_Res);
end;

-- но и тут - компилятор - нам не поможет.

Можно даже попытаться написать так:

type
 TmyResource1 = type Int64;
 TmyResource2 = type Int64;

 TmyAllocator = class
  public
   class function AllocResource1: TmyResource1; 
   class function AllocResource2: TmyResource2; 
   class procedure FreeResource1(var theResource: TmyResource1); 
   class procedure FreeResource2(var theResource: TmyResource2); 
 end;//TmyAllocator

var
 l_Res : TmyResource1;
...
begin
 l_Res := TmyAllocator.AllocResource1;
 ...
 TmyAllocator.FreeResource2(l_Res);
end;

-- но и тут - компилятор - нам не поможет.

Что делать?

"Мой ответ" - избавится от "хакерства" и перейти от атомарных типов к неатомарным.

Как?

Ну банально например вот так:

type
 TmyResource1 = record
  public
   rPosition : Int64;
   constructor Create(aPosition: Int64);
 end;//TmyResource1

 TmyResource2 = record
  public
   rPosition : Int64;
   constructor Create(aPosition: Int64);
 end;//TmyResource2

 TmyAllocator = class
  public
   class function AllocResource1: TmyResource1; 
   class function AllocResource2: TmyResource2; 
   class procedure FreeResource1(var theResource: TmyResource1); 
   class procedure FreeResource2(var theResource: TmyResource2); 
 end;//TmyAllocator
...
constructor TmyResource1.Create(aPosition: Int64);
begin
 rPosition := aPosition;
end;

constructor TmyResource2.Create(aPosition: Int64);
begin
 rPosition := aPosition;
end;
...
class function TmyAllocator.AllocResource1: TmyResource1;
begin
 Result := TmyResource1.Create(Self.AllocPos1);
end;

class function TmyAllocator.AllocResource2: TmyResource1;
begin
 Result := TmyResource2.Create(Self.AllocPos2);
end;

var
 l_Res : TmyResource1;
...
begin
 l_Res := TmyAllocator.AllocResource1;
 ...
 TmyAllocator.FreeResource2(l_Res);
 // - Вот тут компилятор - ЗАРУГАЕТСЯ
end;

- т.е. в данном случае - компилятор нам - помощник.

Почему записи, а не объекты?

Ну чтобы избежать "накладных расходов".

Хотя объекты - конечно - предпочтительнее.

Как только "бизнес-логика" становится "сложной" и начинает "упаковываться в экземпляры ресурсов".

Капитан-очевидность.

Но!

Ещё одна ремарка - пусть у нас есть даже не ресурсы, а просто "идентификаторы":

type
 TUserID = Integer;
 TGroupID = Integer;

-- как их "случайно не перепутать"?

А всё так же:

type
 TUserID = record
  public
   rID : Integer;
   constructor Create(anID: Integer);
 end;//TUserID

 TGroupID = record
  public
   rID : Integer;
   constructor Create(anID: Integer);
 end;//TGroupID

Ну вот собственно и всё.

Может быть кому-нибудь понравится.

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

  1. Дженерики никак не помогут разве?

    ОтветитьУдалить
  2. Попробуйте ответить на вопрос - "для чего они должны помочь".
    Для чего-то - точно могут :-)

    ОтветитьУдалить
  3. угловые скобки стираются, гадство :(

    ОтветитьУдалить
  4. type
    TmyResource1 = type Int64;
    TmyResource2 = type Int64;

    TmyAllocato{T} = class
    public
    class function AllocResource: T;
    class procedure FreeResource(var theResource: T);
    end;//TmyAllocator

    var
    l_Res: TmyResource1
    ...
    l_Res := TmyAllocator{TmyResource1}.AllocResource;
    ...
    TmyAllocator{TmyResource1}.FreeResource(l_Res); //тут уже не перепутает
    TmyAllocator{TmyResource2}.FreeResource(l_Res); //тут ругнется компилятор

    ОтветитьУдалить
    Ответы
    1. А вы пробовали? Это хотя бы компилировать... По-моему - нет.

      Удалить
  5. понятно что это не рабочий код, я лишь сказал, что дженерики упрощают контроль типов (собственно заметка же об
    этом), или Вам важен именно рабочий код? Можно придумать и рабочий... Но зачем?

    ОтветитьУдалить
  6. Дженерики НЕ УПРОЩАЮТ контроль типов. Особенно атомарных. И если уж вы взялись "придумывать", то да - "придумывайте реальный код". И боюсь что про дженерики у вас странное представление. Могу конечно ошибаться.

    ОтветитьУдалить
    Ответы
    1. Мысль была в чём?

      А вот тут:

      type
      TmyResource1 = type Int64;
      TmyResource2 = type Int64;

      -- дело в том, что TmyResource1 и TmyResource2 - для компилятора - НЕ РАЗЛИЧИМЫ.Что с дженериками, что БЕЗ НИХ. Собственно мысль была ТОЛЬКО В ЭТОМ.

      Приведу более "тривиальный" пример - TCaption и TFileName. Они ведь:
      type
      TCaption = type String;
      TFileName = type String;

      Они ведь СЕМАНТИЧЕСКИ - РАЗНЫЕ, но СИНТАКСИЧЕСКИ - совместимы друг с другом.

      Что ведёт к потенциальным ошибкам.

      А вот:

      type
      TCaption = record
      rValue : String;
      end;//TCaption

      TFileName = record
      rValue : String;
      end;//TFileName

      -- что СЕМАНТИЧЕСКИ, что СИНТАКСИЧЕСКИ - НЕСОВМЕСТИМЫ. И это - "хорошо".

      Тут - "компилятор помощник".

      Так понятно? Или "я что-то придумываю"?

      Удалить