Spread the love

RAD Studio 10.2 Tokyo — Release 3

Подробности: List of new features and customer reported issues fixed in RAD Studio 10.2 Tokyo Release 3 10.2 Tokyo — Release 3 Скачать: RAD Studio,

Читать на сайте автора.

Когда программисту скучно

Читать на сайте автора.

Интерпретируйте результаты экспериментов правильно

К моему переводу статьи Руди о String и PChar добавили замечательный комментарий, на который мне хотелось бы ответить, но объём заблуждений не позволяет это сделать простым ответом в комментарии.

Текст из статьи, реакцией на который был написан комментарий, видимо, такой:

Это означает, что вы не должны использовать PChar для указания на строки, а затем изменять строку. Лучше всего избегать таких вещей:

// ПРЕДУПРЕЖДЕНИЕ: ПЛОХОЙ ПРИМЕР
var
S: string;
P: PChar;
begin
S := ParamStr(0); // например, нам вернули 'C:Test.exe';
P := PChar(S);
S := 'Something else';

Если S изменяется на 'Something else', указатель P не будет изменён и будет продолжать указывать на 'C:Test.exe'. Поскольку P не является строковой (в смысле String) ссылкой на этот текст, и у нас нет никакой другой переменной, ссылающейся на него, то его счётчик ссылок станет равным 0, и текст будет удалён из памяти. Это означает, что P теперь указывает на недоступную память.

Сам же комментарий:

Michael Shamrovsky
По факту, команда PChar(S), где S: String ничего плохого не сделает, даже если мы после присвоения переменной типа PChar, поменяем строку. Наша куча символов не измениться, и будет указывать на живую страницу.

var
S: String;
P: PChar;
begin
S := 'Hallo!';
P := PChar(S);
ShowMessage(P);
Delete(S, 2, 1);
ShowMessage(P);
end;

Результат — на экране дважды Hello!, хотя по Вашим словам будет ЖОПА!

Учим мат часть! В начале выделяется новая страница, в ней образуется копия строки но без лидирующих 4х байт, затем дается ссылка.

PChar это не просто преобразователь типов, но и копировщик строки!!!

Если вы хотите, вы можете самостоятельно выяснить, что не так с этим экспериментом. Считайте это очередной задачкой. Либо листайте ниже для ответа.

Оставим сладкое («дважды Hello!«) на конец, а пока давайте начнём со слона в комнате: «PChar это не просто преобразователь типов, но и копировщик строки!!!«. Это весьма сильное утверждение, которое легко проверить: достаточно посмотреть исходный код подпрограммы преобразования String к PChar. Для этого достаточно запустить программу, остановиться на строке присваивания и нажать F7 (вход в подпрограмму) — при условии, конечно, что у вас включены отладочные DCU для системного кода (Use Debug DCUs):

Как видим, ни о каком копировании здесь речи не идёт: происходит прямое копирование указателя. Для надёжности можно проверить в машинном отладчике, что никакого иного кода (кроме вызова _UStrToPWChar) в строке присвоения нет:

Упражнение: почему в машинном отладчике процедура называется @UStrToPWChar, а не _UStrToPWChar?

Хм, но если строка не копируется, а P указывает на S, то почему-же при изменении S не меняется P?

Ошибка в рассуждениях состоит в том, что предполагается, что операция изменения (в данном случае — Delete) произведёт операцию с самой строкой. Но на деле Delete сделает копию строки и изменит её. Оригинальная строка при этом не изменится. Соответственно, P продолжит указывать на оригинальную (не изменённую) строку.

Как я узнал, что Delete делает копию? Отложим этот вопрос на потом, я подробно покажу это, а пока поверьте мне на слово.

Постойте, но почему Delete делает копию? И что, в таком случае, будет являться оригиналом строки? В какой переменной он хранится, этот оригинал? Неужели, в P и Михаил был прав?

Наводящий вопрос: а где, собственно, хранится сам текст 'Hallo!'? В строку он же должен попасть откуда-то.

Возможно, я открою тайну, но: сложные константы хранятся в специальном сегменте данных программы. Поскольку константы не должны меняться, то этот сегмент имеет атрибуты «только для чтения» (read only). Это означает, что любая попытка записать что-то в этот участок памяти приведёт к возбуждению исключения EAccessViolation. Вот почему Delete делает копию строки — потому что оригинальную строку изменить нельзя.

Наверное, к этому моменту возникает много вопросов: не слишком ли много я делаю допущений? И что Delete делает копию? И что строка хранится в блоке констант, а не в переменной (т.е. не копируется при присваивании)?

Ну, давайте посмотрим, как мы можем это подтвердить. Вспомним, что у строк типа String есть счётчик ссылок. Он увеличивается на 1 при копировании строк и уменьшается при выходе переменной за область видимости. Свежесозданные строки имеют счётчик ссылок равный 1, а удаляются из памяти, когда счётчик опускается до нуля. Соответственно, если счётчик равен 1, то на строку ссылается одна переменная (и тогда её можно спокойно модифицировать), а если счётчик больше 1, то на строку ссылается две или более переменных (и тогда строку модифицировать нельзя, надо сначала сделать копию).

Как мы можем узнать счётчик ссылок?

Если у вас относительно новая Delphi, то в ней должна быть функция StringRefCount, а если очень старая — то нужно вспомнить внутреннее устройство строк. Строка является указателем, который указывает на начало данных строки (символов). Непосредственно перед этими данными лежит служебный заголовок — в котором, помимо всего прочего, хранится счётчик ссылок строки. Служебный заголовок представлен записью (record) TStrRec в модуле System.

Таким образом, если у нас есть строка S, то, чтобы узнать её счётчик ссылок, нужно преобразовать её в указатель (например: PAnsiChar(S) в старых Delphi или PByte(S) — в новых; PAnsiChar/PByte здесь нужен только чтобы иметь адресную арифметику), затем отступить на размер заголовка ( - SizeOf(TStrRec)), разыменовать указатель (Pointer(...)^), обозначить, что это — заголовок (TStrRec(...)) и, наконец, прочитать счётчик ссылок (.refCnt). Итого, вы можете добавить в Watches такое выражение:

TStrRec(Pointer(PByte(S) - SizeOf(TStrRec))^).refCnt

где S — это ваша переменная типа String.

Примечание: если вы захотите использовать просто StringRefCount вместо этого страшного выражения, то вам нужно будет вставить вызов этой функции в любое место — этого будет достаточно, чтобы Delphi поместила машинный код функции в вашу программу. По умолчанию эта функция нигде не вызывается, её код в вашу программу не попадает, соответственно, вызывать то, чего нет, — нельзя. Ах, да, галку «Allow side effects and function calls» надо будет ещё установить в свойствах Watch-а.

Посмотрим же, чему будет равен счётчик ссылок. Запустим программу. Сначала строка не инициализирована, т.е. указывает на nil, и счётчика ссылок у неё просто нет.

Но после присвоения:

Счётчик ссылок стал равен… минус единице. Несложно сообразить, что -1 в данном случае является специальным флагом, который указывает, что память для строки не была выделена в куче (в противном случае счётчик ссылок имел бы значение просто 1), т.е. что строка является константой. Именно на этот признак смотрит Delete, когда принимает решение о необходимости копировать строку.

Пройдёмся дальше по коду. До вызова Delete всё скучно, нет никаких изменений, счётчик ссылок по-прежнему равен -1. А вот после вызова Delete:

Счётчик становится равным 1 — именно потому, что Delete сделала копию строки. Т.е. выделила в куче новый блок памяти, счётчик ссылок которого будет равен 1. Чтобы он мог опуститься до 0, когда S выйдет из поля видимости. И при этом память, которую выделила Delete, будет освобождена.

А что же P? А P мы не меняли. Она по-прежнему указывает на «старую S«, которая, как мы помним является константой. Таким образом, в памяти программы будет две строки: одна — 'Hallo!' (в области памяти для констант, на неё указывает P), а другая — 'Hllo!' (в динамической куче, на неё указывает S). Как видим, никакого копирования памяти P не выполняет. Михаил ошибся.

Вот ещё пара экспериментов, которые можно провести: во-первых, попробуйте записать что-то в P:

var
S: String;
P: PChar;
begin
S := 'Hallo!';
P := PChar(S);
ShowMessage(P);
Delete(S, 2, 1);
ShowMessage(P);
P^ := 'A'; // - добавили; должно изменить первый символ строки
ShowMessage(P); // - добавили
end;

Если вы запустите этот код, то получите исключение EAccessViolation при попытке записи в P. Происходит это именно потому, что P указывает на read-only область памяти констант, куда невозможно произвести запись. Если бы P указывала на строку S (которая размещена в read-write памяти кучи), то операция записи в P была бы успешна.

Во-вторых, вы можете зайти в Delete по F7:

Как вы можете видеть, Delete первым действием проверяет, нужно ли копировать строку:

И строка копируется, если её счётчик ссылок отличен от 1 (т.е. в том числе — когда он равен минус единице).

Вот, собственно, и всё. Правильно трактуйте результаты своих экспериментов.

P.S.
Возможно, если бы Михаил дочитал статью до конца, он бы и сам догадался:

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

P.P.S.
Упражнение: чем отличается модифицирование строки указанными способами?

var
S: String;
P: PChar;
begin
S := { ... };
P := S;

// Способ 1:
S[1] := 'A';

// Способ 2:
P^ := 'A';
end;

Читать на сайте автора.

Кастомизированный PageControl как аналог RadioGroup

Читать на сайте автора.

Delphi: как измерить точное время выполнения операции?

Точное время измерения выполнения операции в Delphi может пригодится во многих случаях, начиная от самого простого — показать пользователю время, затраченное на выполнение длительной операции

Читать на сайте автора.

Разъяснение по отмене Upgrade SKU

22 января этого года, в блоге генерального директора Embarcadero Technologies Атанаса Попова было объявлено об изменении коммерческих правил для обновления на последние версии продуктов нашей

Читать на сайте автора.

Копирование ссылок с кириллицей в браузере

При копировании в браузерах ссылки из адресной строки содержащей кириллицу ссылки копируются в виде

https://ru.wikipedia.org/wiki/%D0%91%D1%80%D0%B0%D1%83%D0%B7%D0%B5%D1%80

При отправке сообщения с такой ссылкой непонятно куда она ведет.

Читать на сайте автора.

Данные из Dataset в формате JSON для поддержки JavaScript-клиентов

Поддержка JSON-представления данных появилась в RAD Studio очень давно и с тех пор сильно видоизменялась и расширялась. Начиная с обеспечения разработок для DataSnap в DBX,

Читать на сайте автора.

По итогам вебинара «Что нового в Tokyo 10.2 Release 2»

Приятной неожиданностью стало большое количество слушателей на внеочередном вебинаре Embarcadero на русском языке, который был посвящен новым возможностям последнего релиза RAD Studio Tokyo 10.2 Release

Читать на сайте автора.

Компактный режим Диспетчера задач Windows

Читать на сайте автора.