Использование универсальных контейнеров в Delphi XE - всегда?

Общие контейнеры могут быть экономией времени при наличии элемента и строго типизированным списком этих элементов. Он сохраняет повторяющееся кодирование создания нового класса с, возможно, внутренней переменной TList, и набирает методы типа "Добавить/удалить" среди других преимуществ (таких как все новые функции, предоставляемые классами классов Generic.)

Однако рекомендуется ли всегда использовать универсальные контейнеры для строго типизированных списков? Каковы конкретные недостатки этого? (Если не беспокоиться о обратной совместимости кода.) Вчера я писал серверное приложение и имел список элементов, которые я создал "старым способом", и собирался заменить его общим списком, но решил сохранить его наклонным, но в основном по привычке. (Должны ли мы нарушать привычку и начинать новую, всегда используя дженерики?)

Ответ 1

В Delphi XE нет оснований не использовать общие контейнеры.

Переключение с старого метода на кастинг даст вам:

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

Ответ 2

Это было вызвано ответом Дельтика, я хотел предоставить контрпример, подтверждающий, что вы можете использовать дженерики для обычной кормления животных. (т.е. Полиморфный общий список)

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

TBaseList = class
  // Some code to actually make this a list
end

TSpecificList = class(TBaseList)
  // Code that reintroduces the Add and GetItem routines to turn TSpecificList
  // into a type-safe list of a different type, compatible with the TBaseList
end

Это не работает с дженериками, потому что вы обычно будете иметь это:

TDogList = TList<TDog>
end

TCatList = TList<TCat>
end

... и единственным "общим предком" для обоих списков является TObject - совсем не полезно. Но мы можем определить новый общий тип списка, который принимает два аргумента класса: a TAnimal и a TSpecificAnimal, создавая безопасный для типа список TSpecificAnimal, совместимый с общим списком TAnimal. Здесь основное определение типа:

TCompatibleList<T1:class;T2:class> = class(TObjectList<T1>)
private
  function GetItem(i: Integer): T2;
public
  procedure Add(A:T2);
  property Item[i:Integer]:T2 read GetItem;default;
end;

Используя это, мы можем сделать:

TAnimal = class; 
TDog = class(TAnimal); 
TCat = class(TAnimal);

TDogList = TCompatibleList<TAnimal, TDog>;
TCatList = TCompatibleList<TAnimal, TCat>;

Таким образом, как TDogList, так и TCatList фактически наследуются от TObjectList<TAnimal>, поэтому теперь у нас есть полиморфный общий список!

Здесь представлено полное консольное приложение, которое показывает эту концепцию в действии. И этот класс теперь входит в мою ClassLibrary для повторного использования в будущем!

program Project23;

{$APPTYPE CONSOLE}

uses
  SysUtils, Generics.Collections;

type

  TAnimal = class
  end;

  TDog = class(TAnimal)
  end;

  TCat = class(TAnimal)
  end;

  TCompatibleList<T1:class;T2:class> = class(TObjectList<T1>)
  private
    function GetItem(i: Integer): T2;
  public
    procedure Add(A:T2);
    property Item[i:Integer]:T2 read GetItem;default;
  end;

{ TX<T1, T2> }

procedure TCompatibleList<T1, T2>.Add(A: T2);
begin
  inherited Add(T1(TObject(A)));
end;

function TCompatibleList<T1, T2>.GetItem(i: Integer): T2;
begin
  Result := T2(TObject(inherited Items[i]));
end;

procedure FeedTheAnimals(L: TObjectList<TAnimal>);
var A: TAnimal;
begin
  for A in L do
    Writeln('Feeding a ' + A.ClassName);
end;

var Dogs: TCompatibleList<TAnimal, TDog>;
    Cats: TCompatibleList<TAnimal, TCat>;
    Mixed: TObjectList<TAnimal>;

begin
  try
    // Feed some dogs
    Dogs := TCompatibleList<TAnimal, TDog>.Create;
    try
      Dogs.Add(TDog.Create);
      FeedTheAnimals(Dogs);
    finally Dogs.Free;
    end;
    // Feed some cats
    Cats := TCompatibleList<TAnimal, TCat>.Create;
    try
      Cats.Add(TCat.Create);
      FeedTheAnimals(Cats);
    finally Cats.Free;
    end;
    // Feed a mixed lot
    Mixed := TObjectList<TAnimal>.Create;
    try
      Mixed.Add(TDog.Create);
      Mixed.Add(TCat.Create);
      FeedTheAnimals(Mixed);
    finally Mixed.Free;
    end;
    Readln;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

Ответ 3

Должны ли мы нарушать привычку и начинать новую, всегда используя дженерики? Да

Ответ 4

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

Ответ 5

В духе ответа Cosmin, по сути, ответ на Deltic answer, вот как исправить Deltic code:

type
  TAnimal = class
  end;

  TDog = class(TAnimal)
  end;

  TAnimalList<T:TAnimal> = class(TList<T>)
    procedure Feed;
  end;
  TDogList = TAnimalList<TDog>;

Теперь вы можете написать:

var
  Dogs: TDogList;
...
  Dogs.Feed;

Ответ 6

Вы писали о обратной совместимости... это моя самая большая проблема, если (например, я) вы пишете библиотеки, которые лучше компилировать с наиболее распространенными версиями Delphi.

Даже если вы используете только XE для закрытого проекта, вы, вероятно, создаете собственные собственные собственные библиотеки, даже если вы никогда не публикуете этот код. У всех нас есть такие любимые юниты, просто доступные, чтобы не изобретать велосипед для каждого проекта.

В будущем назначении вам может потребоваться сохранить старый код без возможности обновления до более новой версии Delphi (нет денег на миграцию и просмотр 1 000 000 строк кода). В этом случае вы можете пропустить свои библиотеки только для XE, с блестящими родовыми списками...

Но для 100% -ного приложения "private", если вы уверены, что вам никогда не придется поддерживать старый код Delphi, я не вижу причин не использовать дженерики. Моя единственная проблема - проблема с дублированным кодом (как цитирует Мейсон): кэш ЦП может быть заполнен ненужным кодом, поэтому скорость выполнения может пострадать. Но в реальном приложении, я думаю, вы не увидите никакой разницы.

Примечание. Я добавил некоторые новые функции в мою оболочку TDynArray. Я попытался подражать пример кода из docwiki EMB. Таким образом, у вас могут быть общие функции, с хорошими старыми версиями Delphi... Конечно, дженерики лучше работают с классами, но с некоторыми массивами и записями он просто качается!

Ответ 7

Если вам нужны полиморфные списки, то дженерики - это помеха, а не помощь. Это даже не компилируется, например, потому что вы не можете использовать TDogList, где требуется TAnimalList:

  uses
    Generics.Collections;

  type
    TAnimal = class
    end;

    TDog = class(TAnimal)
    end;

    TAnimalList = TList<TAnimal>;
    TDogList = TList<TDog>;


  procedure FeedTheAnimals(const aList: TAnimalList);
  begin
    // Blah blah blah
  end;


  var
    dogs: TDogList;
  begin
    dogs := TDogList.Create;
    try
      FeedTheAnimals(dogs);

    finally
      dogs.Free;
    end;
  end;

Причины этого достаточно ясны и легко объясняются, но они так же стойки против интуиции.

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

Если мне нужен TAnimalList, тогда мне нужны или могут воспользоваться дополнительными TAnimal конкретными методами в этом классе списка, которые я бы хотел наследовать в TDogList, что, в свою очередь, может ввести дополнительные конкретные элементы, относящиеся к ним.

(Животные и Собака используются только для иллюстративных целей, конечно. На самом деле я не работаю над ветеринарным кодом - LOL)

Проблема в том, что вы не всегда знаете это в начале.

Оборонительные принципы программирования предполагают (для меня, ymmv), что рисование себя в угол ради экономии времени, скорее всего, в конечном итоге будет стоить много в будущем. И если это не так, дополнительная "стоимость" несоблюдения начального сохранения сама по себе незначительна.

Кроме того, ваш код более совместим с пользователями более старых версий Delphi, если вы склонны быть такими щедрыми.

:)