четверг, 3 сентября 2009 г.

Параметры типа открытый массив и "array of const"

Это перевод Open array parameters and array of const. Автор: Rudy Velthuis.

Эта статья описывает синтаксис и использование параметров функций типа открытый массив (open array parameters), и использование типа параметров "array of const". Она также описывает внутреннее устройство этих двух похожих типов, обсуждает время их жизни, приводит решение типичных проблем. Также тут есть краткое обсуждение часто возникающей путаницы между открытыми и динамическими массивами и между вариантными массивами (variant array) и открытыми массивами из вариантов.

Содержание

НаверхОткрытые массивы

Параметры типа открытый массив являются специальными параметрами, которые позволяют вам написать процедуру или функцию (я буду использовать слово подпрограмма для обозначения обоих), которые могут принимать любой массив с таким же базовым типом, независимо от его размера. Чтобы объявить такой параметр, вам нужно использовать такой синтаксис:
procedure ListAllIntegers(const AnArray: array of Integer);
var
  I: Integer;
begin
  for I := Low(AnArray) to High(AnArray) do
    WriteLn('Integer at ', I, ' is ', AnArray[I]);
end;
Вы можете вызвать эту процедуру с любым одномерным массивом из Integer, так что вы можете вызвать её для array[0..1] of Integer, а также для array[42..937] of Integer или даже для динамического массива array of Integer.

Код также демонстрирует, как вы можете определить размер переданного вам массива в подпрограмме. Delphi знает псевдо-функции Low и High. Это не настоящие функции, а просто синтаксические элементы в Delphi, которые выглядят и используются как функции, но в реальности они работают с помощью магии компилятора, который заменяет их на код. Low возвращает нижнюю границу массива, а High – верхнюю. Вы также можете использовать функцию Length, которая возвращает число элементов в массиве.

Но если вы вызовите код с массивом, которые не начинается с нуля (not zero-based), как в следующем (бессмысленном) примере,
var
  NonZero: array[7..9] of Integer;
begin
  NonZero[7] := 17;
  NonZero[8] := 325;
  NonZero[9] := 11;
  ListAllIntegers(NonZero);
end.
то вы увидите такой вывод:
Integer at 0 is 17
Integer at 1 is 325
Integer at 2 is 11
Это потому что внутри подпрограммы массив всегда виден как массив, начинающийся с нуля (zero-based). Поэтому для параметров типа открытый массив, Low всегда равна 0, а High корректируется соответственно (заметьте, что это не обязательно так для других использований High и Low, т.е. при использовании их не для открытых массивов). Для открытых массивов, Length всегда равна High + 1.

НаверхОтрезка массива (slice)

Если вы не хотите использовать весь массив, а только его часть, вы можете сделать это с использованием псевдо-функции Slice. Её можно использовать только для обрезки параметра, передаваемого как открытый массив. Она используется следующим образом:
const
  Months: array[1..12] of Integer = (31, 28, 31, 30, 31, 30, 
                                     31, 31, 30, 31, 30, 31);
begin
  ListAllIntegers(Slice(Months, 6));
end;
Этот код покажет только 6 значений, а не все 12 (прим.пер.: очень удобно использовать эту псевдо-функцию для случаев с Capacity в массиве – т.е. когда длина (Length) массива не отражает реальное число активных в нём элементов, которое хранится отдельно).

НаверхВнутреннее устройство

Но как это работает, как функция узнаёт размер массива? Это довольно просто. Параметр типа открытый массив на самом деле является комбинацией двух параметров: указателя, который содержит адрес начала массива (передаваемых данных), и Integer, который содержит значение High (с учётом отсчёта от 0). Так что, фактически, настоящий список параметров процедуры выглядит так:
procedure ListAllIntegers(const AnArray: PInteger; High: Integer);
Каждый раз, когда вы передаёте массив параметром типа открытый массив, компилятор, который знает размер массива, просто передаст подпрограмме указатель на данные и их количество (размер) – скорректированное значение High. Для статических массивов (вроде array[7..9] of Integer) он просто использует размер, указанный при объявлении массива; для динамических массивов, он получает значение High массива во время выполнения программы.

Обычно вы можете передавать открытые массивы как const параметры. Открытые массивы, которые не передаются как const будут полностью скопированы в локальную область переменных подпрограммы. Массив всегда передаётся по ссылке, но если он не был объявлен как const, то магия компилятора выделит место в стеке и скопирует туда массив целиком. Для больших массивов это может быть очень не эффективно. Так что если вам не нужна локальная модификация элементов массива – объявляйте открытый массив с модификатором const.

НаверхКонструкторы открытых массивов

Иногда вы не хотите объявлять и заполнять массив только для того, чтобы передать его в параметр типа открытого массива. К счастью, Delphi позволяет вам объявить массив прямо по месту вызова, используя синтаксис, так называемого, конструктора открытого массива, который использует [ и ] для определения массива. Пример выше с массивом NonZero можно было также очень компактно записать вот так:
ListAllIntegers([17, 325, 11]);
Здесь мы объявляем массив прямо по месту вызова, как [17, 325, 11]. Компилятор гарантирует, что массив будет существовать во время всего вызова подпрограммы, и он также передаёт корректное значение High. Этот способ вызова полностью прозрачен для вызываемой подпрограммы. Сам массив будет удалён сразу после выхода из подпрограммы.

НаверхПутаница

Хотя синтаксис, к сожалению, очень похож, но вам не следует путать параметры типа открытого массива с динамическими массивами Delphi. Динамический массив – это массив, который обслуживается RTL Delphi во время выполнения программы, он размещается в динамической памяти (куче) и вы можете менять его размеры с помощью SetLength (см. также статью про динамические типы данных). Он объявляется так:
type
  TIntegerArray = array of Integer;
К сожалению, это выглядит в точности как синтаксис для параметров типа открытый массив. Но они не являются одним и тем же. Параметр типа открытый массив примет любой массив: динамический, вроде array of Month, или статический, вроде array[0..11] of Month. Поэтому внутри подпрограммы с таким параметром вы не можете вызвать SetLength для параметра. Если же вы хотите работать только с динамическими массивами – вам придётся объявить их тип отдельно, и использовать имя типа при указании типа параметра подпрограммы.
type
  TMonthArray = array of Month;

procedure AllKinds(const Arr: array of Month);
procedure OnlyDyn(Arr: TMonthArray);
Процедура AllKinds примет и статический массив и динамический массив, поэтому вы не сможете использовать SetLength, ведь статические массивы не могут менять свой размер. Процедура же OnlyDyn примет только динамический массив, поэтому вы сможете использовать SetLength (однако она будет работать с копией массива; если же вы хотите изменить оригинальный массив, используйте в объявлении var Arr: TMonthArray).
Информация
Вам не следует забывать, что в Delphi параметры могут быть объявлены только с указанием типа, а не с объявлением типа. Поэтому следующие формальные параметры, которые являются объявлениями типов, не допустимы:
procedure Sum(const Items: array[1..7] of Integer);
function MoveTo(Spot: record X, Y: Integer; end);
Вы должны сперва объявить тип, и только потом использовать его имя для указания типа при объявлении параметра:
type
  TWeek = array[1..7] of Integer;
  TSpot = record
    X, Y: Integer;
  end; 
    
procedure Sum(const Items: TWeek);
function MoveTo(Spot: TSpot);
Вот почему array of Something в списке параметров подпрограммы не может быть объявлением динамического массива. Это объявление параметра типа открытый массив.

НаверхTArray<T>

В версиях Delphi с поддержкой генериков (generics) есть тип TArray<T>, объявленный так:
type
  TArray<T> = array of T;
Этот тип является генерик-типом, т.е. он становится настоящим типом только при указании конкретного типа для T, например:
procedure OnlyDyn(Arr: TArray<Month>);
Подобные конструкции начинают использоваться всё чаще и чаще, поскольку генерик-типы могут обходить классическое правило совместимости типов: совместимости типов не зависит от формы объявления, но зависит от места объявления. Т.е. не все массивы из Month совместимы друг с другом по присваиванию, даже хотя они объявлены одинаково. Но при этом любые массивы вида TArray<Month> совместимы по присваиванию вне зависимости от места их объявления, даже если они объявляются прямо в месте объявления другого типа (подпрограммы). Это - исключение к правилу, которое я объяснял чуть выше в разделе путаницы синтаксисов. TArray<T> может быть использован и как параметр и как возвращаемый тип:
constructor Create(Limbs: TArray; Negative: Boolean);
function ToByteArray: TArray;

НаверхАссемблер

Чтобы использовать открытый массив из ассемблера, вам нужно не забывать, что открытый массив, на самом деле, является комбинацией двух параметров. Первый параметр является указателем на начало массива, второй - индекс последнего элемента (с коррекцией начала массива на индекс 0). Как правило открытые массивы передаются как const или var. Во всех случаях массив передаётся по ссылке

Вот пример функции суммирования всех чисел в массиве:
function Sum(const Data: array of Integer): Integer;
// EAX: адрес массива (первый параметр)
// EDX: значение High (второй параметр)
asm
        MOV     ECX,EAX                 // P := PInteger(Addr(Data));
        XOR     EAX,EAX                 // Result := 0;
        OR      EDX,EDX
        JS      @@Exit                  // if High(Data) < 0 then Exit;
@@Next:                                 // repeat
        ADD     EAX,[ECX]               //   Result := Result + P^;
        ADD     ECX,TYPE Integer        //   Inc(PInteger(P));
        DEC     EDX                     //   Dec(EDX);
        JNS     @@Next                  // until EDX < 0;
@@Exit:
end;
Этот пример использует соглашение вызова register. Если ваша подпрограмма имеет иное соглашение вызова - вам нужно обращаться к параметру массива иным образом, но всё равно - трактуя его как комбинацию двух параметров. В частности, если подпрограмма является методом, то её первым параметром (EAX) будет неявный параметр Self, а массив Data в этом случае будет передан во втором и третьем параметрах: EDX и ECX.

НаверхArray of const

Array of const является особым случаем открытых массивов. Вместо передачи элементов только одного типа, вы можете передавать несколько разных типов. Посмотрите на объявление функции Format в Delphi:
function Format(const Format: string; const Args: array of const): string;
(Вообще-то, есть и вторая, перегруженная функция, в некоторых версиях Delphi, но я просто проигнорирую её тут)

Первый параметр является строкой, которая показывает, как интерпретировать значения в массиве, а второй параметр является array of const, так что вы можете передавать набор значений, используя такой же синтаксис, как для конструкторов открытых массивов. Например, вы можете вызвать функцию примерно так:
var
  Res: string;
  Int: Integer;
  Dub: Double;
  Str: string;
begin
  Int := Random(1000);
  Dub := Random * 1000;
  Str := 'Teletubbies';
  Res := Format('%4d %8.3f %s', [Int, Dub, Str]);
end;
Информация
Официальное название для параметров array of const – это параметры типа вариантного открытого массива (variant open array parameters). Его не нужно путать с типом Variant в Delphi, и с массивами, которые он может хранить. Они несколько различны, даже хотя TVarRec (см. ниже) немного похожа на внутреннее устройство Variant. Даже имя записи внутреннего представления для Variant выглядит очень похоже: TVarData.

НаверхВнутреннее устройство

Внутренне, array of const является открытым массивом из записей TVarRec. Объявление TVarRec дано в online справке Delphi. Этот тип представляет собой вариантную запись (её также не надо путать с типом Variant), которая содержит поле VType, и перекрывающиеся (overlay) поля разных типов, некоторые из которых являются просто указателями. Компилятор создаёт по TVarRec для каждого элемента в конструкторе открытого массива, заполняет поле VType типом элемента, и размещает значение или указатель на него в соответствующем поле записи. После этого весь массив из TVarRec передаётся функции.

Поскольку каждая TVarRec содержит информацию о типе элемента, функция Format может использовать её для проверки соответствия типа элемента массива спецификатору в строке. Вот почему вы можете получить сообщение об ошибке в runtime, если вы передадите неверный тип данных. Вы можете указать компилятору, что вы хотите передавать значение как другой тип, с помощью приведения (casting) к желаемому типу. Если требуемый вами тип не имеет аналога в TVarRec, компилятор попробует сконвертировать значение в совместимый тип, так что, например, если вы передадите Double, компилятор сконвертирует его в Extended. Конечно же, есть разумные ограничения на возможности компилятора по подбору типов, так что, например, передача объекта не сработает.

Внутри функции или процедуры, вы можете работать непосредственно с членами TVarRec. Справка Delphi показывает примеры, как это сделать.

НаверхВопросы жизненного цикла

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

Если вам нужно скопировать TVarRec в массив или переменную вне подпрограммы (например, в var-параметр), то убедитесь, что вы делаете копию (т.е. в куче) значения, и заменяете указатель в TVarRec на указателю на копию. Вам также придётся позаботиться об освобождении памяти копии, когда она будет не нужна. Вот пример: Скачать (ссылка на сайт автора – прим.пер.)
type
  TConstArray = array of TVarRec;
                                
// Копирует TVarRec и его содержимое. Если содержимое передаётся по ссылке,
// то значение будет скопировано, а ссылка обновлена.

function CopyVarRec(const Item: TVarRec): TVarRec;
var
  W: WideString;
begin
  // Сперва копируем TVarRec целиком.
  Result := Item;
      
  // Теперь обрабатываем специальные случаи.
  case Item.VType of
    vtExtended:
      begin
        New(Result.VExtended);
        Result.VExtended^ := Item.VExtended^;
      end;
    vtString:
      begin
        GetMem(Result.VString, Length(Item.VString^) + 1);
        Result.VString^ := Item.VString^;
      end;
    vtPChar:
      Result.VPChar := StrNew(Item.VPChar);
    vtPWideChar:
      begin
        W := Item.VPWideChar;
        GetMem(Result.VPWideChar,
               (Length(W) + 1) * SizeOf(WideChar));
        Move(PWideChar(W)^, Result.VPWideChar^,
             (Length(W) + 1) * SizeOf(WideChar));
      end;
    vtAnsiString:
      begin
        Result.VAnsiString := nil;
        AnsiString(Result.VAnsiString) := AnsiString(Item.VAnsiString);
      end;
    vtCurrency:
      begin
        New(Result.VCurrency);
        Result.VCurrency^ := Item.VCurrency^;
      end;
    vtVariant:
      begin
        New(Result.VVariant);
        Result.VVariant^ := Item.VVariant^;
      end;
    vtInterface:
      begin
        Result.VInterface := nil;
        IInterface(Result.VInterface) := IInterface(Item.VInterface);
      end;
    vtWideString:
      begin
        Result.VWideString := nil;
        WideString(Result.VWideString) := WideString(Item.VWideString);
      end;
    vtInt64:
      begin
        New(Result.VInt64);
        Result.VInt64^ := Item.VInt64^;
      end;
    vtUnicodeString:
      begin
        Result.VUnicodeString := nil;
        UnicodeString(Result.VUnicodeString) := UnicodeString(Item.VUnicodeString);
      end;
  end;
end;

// Создаёт TConstArray из переданных значений. Использует CopyVarRec
// для создания копий каждого отдельного элемента.
function CreateConstArray(const Elements: array of const): TConstArray;
var
  I: Integer;
begin
  SetLength(Result, Length(Elements));
  for I := Low(Elements) to High(Elements) do
    Result[I] := CopyVarRec(Elements[I]);
end;
     
// TVarRec, создаваемые CopyVarRec должны быть удалены этой функцией.
// Вы не должны использовать её для других TVarRec.
procedure FinalizeVarRec(var Item: TVarRec);
begin
  case Item.VType of
    vtExtended: Dispose(Item.VExtended);
    vtString: Dispose(Item.VString);
    vtPChar: StrDispose(Item.VPChar);
    vtPWideChar: FreeMem(Item.VPWideChar);
    vtAnsiString: AnsiString(Item.VAnsiString) := '';
    vtCurrency: Dispose(Item.VCurrency);
    vtVariant: Dispose(Item.VVariant);
    vtInterface: IInterface(Item.VInterface) := nil;
    vtWideString: WideString(Item.VWideString) := '';
    vtInt64: Dispose(Item.VInt64);
    vtUnicodeString: UnicodeString(Item.VUnicodeString) := '';
  end;
  Item.VInteger := 0;
end;  

// Массив TConstArray, содержащий TVarRec, должен быть финализирован. 
// Эта функция удаляет все элементы массива по-одному.
procedure FinalizeConstArray(var Arr: TConstArray);
var
  I: Integer;
begin
  for I := Low(Arr) to High(Arr) do
    FinalizeVarRec(Arr[I]);
  Arr := nil;
end;
Функции выше помогут вам управлять TVarRec вне подпрограммы, для которых они создавались. Вы даже можете использовать TConstArray, где был объявлен открытый массив. Следующая небольшая программа
program VarRecTest;

{$APPTYPE CONSOLE}

uses
  SysUtils,
  VarRecUtils in 'VarRecUtils.pas';

var
  ConstArray: TConstArray;

begin
  ConstArray := CreateConstArray([1, 'Hello', 7.9, IntToStr(1234)]);
  try
    WriteLn(Format('%d --- %s --- %0.2f --- %s', ConstArray));
    Writeln(Format('%s --- %0.2f', Copy(ConstArray, 1, 2)));
  finally
    FinalizeConstArray(ConstArray);
  end;  
  ReadLn;
end.
выведет ожидаемый, но не очень возбуждающий результат:
1 --- Hello --- 7.90 --- 1234
Hello --- 7.90
Небольшая программа выше также демонстрирует, как вы можете использовать Copy для использования только части массива TConstArray. Copy создаст копию части динамического массива, но при этом она не копирует содержимое (в смысле CreateConstArray), поэтому вы не должны вызывать FinalizeConstArray на эту копию. В программе выше, копия части элементов массива будет удалена автоматически, т.к. жизнью динамических массивов управляет компилятор.

НаверхЗаключение

Открытые массивы и arrays of const являются мощными возможностями языка, но в них есть несколько подводных камней. Я надеюсь, что мне удалось успешно показать некоторые из них и способы их преодоления.

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

  1. Клёво, не знал про Slice!

    ОтветитьУдалить
  2. Спасибо за статью (перевод)! Наконец-то расставила для меня точки над i про передачу массивов "подпрограммам", и открыла новые моменты Delphi.

    ОтветитьУдалить
  3. Процедура же OnlyDyn примет только динамический массив, поэтому вы сможете использовать SetLength (однако она будет работать с копией массива; если же вы хотите изменить оригинальный массив, используйте в объявлении var Arr: TMonthArray).

    Delphi 2007 не снимает копию с тела массива. Только с заголовка, где хранит длину, указатель и счётчик ссылок.

    Например, такой код вывалится с ошибкой на ShowMessage.

    //------------------------
    procedure TForm1.FormCreate(Sender: TObject);
    var
    i: Integer;
    begin
    SetLength(FArray1, 10);
    for i := 0 to 9 do
    begin
    FArray1[i] := TMyObject.Create(i);
    end;
    end;

    procedure TForm1.Test(Arr: ObjArray);
    begin
    Arr[Length(Arr) - 1] := nil;
    SetLength(Arr, Length(Arr) - 1);
    end;

    procedure TForm1.btnOooopsClick(Sender: TObject);
    begin
    Test(FArray1);
    ShowMessage(IntToStr(TMyObject(FArray1[Length(FArray1) - 1]).Id));
    end;

    //------------------------

    То же самое относится и к обычному присвоению, в итоге которого мы имеем две разные переменные, части значений которых лежат в одной и той же области памяти. Это баг, ИМХО.

    ОтветитьУдалить
  4. When an Open Array Parameter isn't so Open - пример особого случая для array of Char.

    ОтветитьУдалить
  5. подскажите, почему вываливается ошибка
    [Pascal Error] Proga4.dpr(73): E2010 Incompatible types: 'Ñ' and 'dynamic array'

    program Proga4;

    {$APPTYPE CONSOLE}
    {Задание 4

    Вычислить сумму элементов каждой из матриц А(15,15) и В(30,30) без учета элементов главной диагонали. Задачу решить с использованием подпрограммы. }


    {$APPTYPE CONSOLE}
    uses
    SysUtils,
    Russian;

    const
    n=15; //Число строк и столбцов матрицы А.
    m=30; //Число строк и столбцов матрицы В.
    type
    С = array of array of integer;
    var
    A,B:array of array of integer; //Массивы для хранения матриц.
    i, j : integer;




    function summa(D : С; p:integer ):integer; //Считает сумму элементов матрицы C без учёта главной диагонали

    var
    i, j : integer;
    begin
    SetLength(D, p,p);

    Result:=0; //Обнуляем сумму
    for i:=0 to High(D) do //Цикл по строкам
    begin
    for j:=0 to High(D[0]) do //Проходим в цикле по элементам строки
    begin
    if i<>j then //Исключаем главную диагональ
    Result:=Result+D[i,j]; //Суммируем элеметы матрицы
    end;
    end;
    D:=nil;
    end;

    begin // Начало основной программы.
    SetLength(A,n,n);
    SetLength(B,m,m);
    randomize;

    writeln (Rus(' Исходная матрица A'));
    for i:=0 to High(A) do //Создание исходной матрицы А.
    begin
    for j:=0 to High(A[0]) do
    begin
    A[i,j]:=25-Random(50);
    Write (A[i,j]:5);
    end;
    WriteLn;
    end;

    writeln (Rus(' Исходная матрица B'));
    for i:=0 to High(B) do //Создание исходной матрицы B.
    begin
    for j:=0 to High(B[0]) do
    begin
    B[i,j]:=25-Random(50);
    Write (B[i,j]:5);
    end;
    WriteLn;
    end;


    Writeln (summa(A,n));
    Writeln (summa(B,m));

    A:=nil;
    B:=nil;
    Writeln(' press Enter for exit, please...');
    readln;
    end.

    ОтветитьУдалить
  6. Простите, а как-то возможно ли сделать такое? Вот так хочу вызывать функцию в коде:

    TestFunc(
    [
    [1, 'test'],
    [2, 213, 332, Button1],
    [Panel3, 'day', 'month', 'head', 220],
    ]
    );

    У меня не выходит, как тока ни пробовал, ругается по-всякому...

    ОтветитьУдалить
    Ответы
    1. А вы читали статью? В частности, про то, как устроены открытые массивы. Очевидно же, что двумерный открытый массив невозможен.

      Используйте динамические массивы. Посмотрите.

      Удалить
    2. Конечно читал, кстати там "логическая опечатка" - в последнем участке кода есть FinalizeConstArray и в послесловии упоминается, но во всём предыдущем тексте нету такой подстроки. Вероятно имелась ввиду FinalizeVarRecArray? Ну и ещё перед одним из комментов не хватает обратного слеша.

      Но там нету именно интересующего меня случая. Сейчас пытаюсь намонстрячить такое:

      TestFunc(
      {*} CreateConstArray([
      {*} {*} CreateConstArray([1, 'test']),
      {*} {*} CreateConstArray([2, 213, 332, Button1]),
      {*} {*} CreateConstArray([Panel3, 'day', 'month', 'head', 220])
      {*} ])
      );

      Даже вроде работает, тока как мне корректно теперь освободить не уверен...

      Удалить
    3. > Str := 'Teletubbies';
      о_____О
      Матерь-перематерь... Изыдьте, изыдьте нечистые...

      Удалить
    4. Пока передаю параметр в TestFunc() как написал выше, уже в самой TestFunc() удаляю переданное.
      При удалении предполагаю что если VType равен vtPointer то это вложенный TConstArray и удаляю рекурсивно. Кажется работает.
      Только не понял надо ли что-то делать при VType равном vtWideString, у меня тут Делфи новая, в отладчике посмотрел строки только такие попадают в TVarRec. Пытался по аналогии пихнуть в Dispose(), но тока вылет получаю.

      Вот я объявляю TestFunc(Rules: TConstArray) - как мне пометить var или const или никак? Если как var, то оно не позволяет передавать сразу CreateConstArray а только переменную, а если как const оно не даёт передать параметр в FinalizeVarRecArray так как там объявлено как var.
      Если я никак не помечу он как я понял сделает копию? Лично мне всё равно копия или нет, там будут относительно небольшие данные, вот только не понимаю оригинал при этом корректно освободится? Или всё же лучше передавать в TestFunc() исключительно в виде переменной? Это в моём случае кажется слегка неудобно.

      Извините что забросал глупыми вопросами... Начинающий я. Изобретаю как сделать свою задумку удобнее, без лестницы if'ов и прочего.

      Удалить
    5. Короче хотя первые тестовые попытки были очень успешны - но когда начал применять на практике... Когда стал вызывать TestFunc() несколько раз подряд и/или в циклах - то всё внезапно навернулось, сперва вылетает AV по странному адресу, а потом даже при закрытии приложения почему-то выдаёт ещё пачку других странных AV.
      Отладчиком поковырял, пишет мол Rules[1].VString есьм "Inaccessible value", хотя передаю я туда абсолютно точно константную строку. Короче не вышел каменный цветок. :С
      А жаль, вышло бы красиво.

      Удалить

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

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

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

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

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