четверг, 22 января 2015 г.

Как хранить числа в объектах TStringList

Как хранить числа в объектах TStringList прекрасно известно: MyStrings.AddObject( MyString, TObject( MyNumber ) )...казалось бы. Однако, мне (да вот - дошла очередь и до меня) пришлось убедиться, что к словам "прекрасно известно" следует добавить слово "было" - прекрасно известно было. Увы, двадцать лет работавший в Win32 и даже в Win64 способ теперь уже не работает. Точнее, не работает на мобильных платформах, т.е. там, где действует незримая власть коварного AUTOREFCOUNT aka ARC.

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


Цвет идёт к справочнику в виде строки #rrggbb - тут StringToAlphaColor() сразу в руки. Получаем бинарное значение TAlphaColor, которое на самом деле = type Cardinal, т.е. то, что отличнейшим образом укладывается в AddObject() рядом со строчкой из справочника. Теперь, получая данные, я легко в справочнике найду соответствующий цвет: 'критично'-#ff0000, 'неважно'-#cccccc, и т.д. Один раз цвета распознал, а потом пользуйся сколько влезет. Только обязательно надо не забыть включить для цвета канал Alpha (например, Result := TAlphaColorRec.Alpha + AColor), а то нарисованное будет настолько прозрачным, что его станет совершенно невозможно разглядеть.

Связка справочников - тоже штука банальная. У меня в одном из двух справочников дано дефолтное значение из второго. Надо при заполнении TStringList просто бежать по первому списку и искать в предварительно заполненном втором IndexOf() и класть в объекты первого списка найденные индексы, а текущий счётчик класть, разумеется, в объекты второго списка с этими индексами. Тут, правда, есть нюанс: нулевое значение nil, которым инициализируются объекты списков, будет в виде целого числа равно нулю, что означает не пустое значение, а индекс первого элемента. Надо быть внимательнее - ставить -1 везде, где только можно, или при сохранении найденных индексов добавлять единицу, а потом об этом надо не забывать, и оперировать уже с отниманием единицы. Я лично предпочитаю второй способ - точно не нарвёшься на случайный ноль.


Не долго думая, я сделал прототип на Win32. И всё у меня прекрасно заработало: метки красились нужным цветом, словари дружно связывались. Ура! Осталось только скомпилить под Андроид, который требовался Заказчику, и ... Вот тут-то и обнаружилось, что связки не связываются, а цвета не цветут. Как же так?

Чтобы не рвать куски из проекта, возьмём надуманный пример. Создадим проект пустого мобильного приложения, добавим в дереве проекта платформу Win32 и вставим на событие OnCreate главной и единственной формы код, что дан ниже (имя формы только подкорректируйте под себя). Ах, да - не забудем ещё бросить на форму TMemo c Align = alClient ( все эти манипуляции читатель может делать мысленно ).

const
  Labels: array [1..5] of String = ('Lorem','ipsum','dolor','sit','amet');
 
procedure TForm6.FormCreate(Sender: TObject);
var
  I: Integer;
begin
  with TStringList.Create do
  try
    for I := Low(Labels) to High(Labels) do
      AddObject( Labels[ I ], TObject( I ) );
    Sort;
    for I := 0 to Count-1 do
      Strings[ I ] := Format( '%s=%d', [ Strings[ I ], Integer( Objects[ I ] ) ] );
    Memo1.Lines.Text := Text;
  finally Free;
  end;
end;

Ожидаемый результат получен:


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

А у меня в проекте этот фокус прошёл. Я поназаполнял словарей, развесил там индексы и цвета и пошёл дальше читать уже данные. И только когда стал доставать из закромов все свои богатства, то обнаружил сплошные nil. И сильно удивился - мыши съели?! - Так нет здесь мышей! Ну да говорят, что удивляться - это даже полезно.

Что делать? Проект - вон он уже дышит и играет мускулами! Не будешь же перетрахивать (© Лукашенко А.Г.) его из-за какой-то мелочи. Сейчас дадим таблеточку, и будет как здоровый!

type
  TData<T> = class
    FValue: T;
    constructor Create( const AValue: T );
  end;
 
const
  Labels: array [1..5] of String = ('Lorem','ipsum','dolor','sit','amet');
 
procedure TForm6.FormCreate(Sender: TObject);
var
  I: Integer;
begin
  with TStringList.Create( True ) do
  try
    for I := Low(Labels) to High(Labels) do
      AddObject( Labels[ I ], TData<Integer>.Create( I ) );
    Sort;
    for I := 0 to Count-1 do
      Strings[ I ] := Format( '%s=%d', [ Strings[ I ], TData<Integer>( Objects[ I ] ).FValue ] );
 
    Memo1.Lines.Text := Text;
  finally Free;
  end;
end;
 
{ TData<T> }
 
constructor TData<T>.Create(const AValue: T);
begin
  FValue := AValue;
end;

Просто? Да! Мне даже как-то понравилось - появилось ещё "пустое значение": nil=Null. Так что и игры с плюс-минус единицей для индексов можно выкинуть. А если, например, нужно Альфа-канал цвета устанавливать по месту, то уже ни за что не спутаешь чёрный цвет с прозрачным - 0 уже не равен nil! И кода лишнего - с гулькин нос. Главное - при создании TStringList включить  параметр OwnsObjects, чтобы объекты прибивались самим списком, и всё. Красота!


...............

Конечно, старики в стойбище ворчат, что на новой мягкой земле уже нельзя просто оставить пойманную рыбу, как раньше на льду, а надо держать в садке, привязанном верёвкой к колышку, чтобы рыба не стухла, или не стащил кто. И чтобы оттуда достать рыбу, надо теперь этот садок вытягивать из воды, лезть в него рукой и там шарить. Как раньше хорошо было! А теперь таскайся с этой штукой. Это же, конечно, ещё и денег каких-то стоит!  Ох уж эти новые края с их климатом! Как тут другие люди живут?...