Константные строковые параметры и компилятор Delphi XE5 для Android

Надеюсь, я просто пропустил что-то очевидное, но я, кажется, обнаруживаю, что постоянные строковые аргументы повреждаются при использовании компилятора Delphi XE5 для Android. Тестовый код:

1) Создайте новый проект пустых мобильных приложений.

2) Добавьте TButton в форму и создайте для OnClick обработчик OnClick .

3) Заполните обработчик следующим образом:

 procedure TForm1.Button1Click(Sender: TObject); begin GoToDirectory(PathDelim + 'alpha' + PathDelim + 'beta'); GoToDirectory(FParentDir); end; 

4) В объявлении класса формы добавьте два поля и один из них:

 FCurrentPath, FParentDir: string; procedure GoToDirectory(const Dir: string); 

5) Реализовать Foo и GoToDirectory следующим образом:

 function Foo(const S: string): Boolean; begin Result := (Now <> 0); end; procedure TForm1.GoToDirectory(const Dir: string); begin FCurrentPath := IncludeTrailingPathDelimiter(Dir); FParentDir := ExcludeTrailingPathDelimiter(ExtractFilePath(Dir)); ShowMessageFmt('Prior to calling Foo, Dir is "%s"', [Dir]); Foo(FParentDir); ShowMessageFmt('After calling Foo, Dir is "%s"', [Dir]); end; 

6) Скомпилируйте и запустите на устройстве.

Когда я это делаю, первые два окна сообщений не указывают ничего плохого, однако Dir затем очищается между третьим и четвертым приглашениями. Кто-нибудь еще это понимает, или я просто делаю что-то глупое? (Нет ничего неблагоприятного, если я нацелен на Win32 для тестирования).

Обновить

Для версии без FMX снова создайте новое пустое мобильное приложение, но на этот раз удалите форму из проекта. Затем перейдите в исходный код проекта и добавьте следующий код:

 program Project1; uses System.SysUtils, Androidapi.Log; type TTest = class private FCurrentPath, FParentDir: string; procedure GoToDirectory(const Dir: string); public procedure Execute; end; function Foo(const S: string): Boolean; begin Result := (Now <> 0); end; procedure TTest.GoToDirectory(const Dir: string); var M: TMarshaller; begin FCurrentPath := IncludeTrailingPathDelimiter(Dir); FParentDir := ExcludeTrailingPathDelimiter(ExtractFilePath(Dir)); LOGE(M.AsUtf8(Format('Prior to calling Foo, Dir is "%s"', [Dir])).ToPointer); Foo(FParentDir); LOGE(M.AsUtf8(Format('After to calling Foo, Dir is "%s"', [Dir])).ToPointer); end; procedure TTest.Execute; begin GoToDirectory(PathDelim + 'alpha' + PathDelim + 'beta'); GoToDirectory(FParentDir); end; var Test: TTest; begin Test := TTest.Create; Test.Execute; end. 

Чтобы увидеть результат, сначала запустите monitor.bat в папке tools Android SDK; Видеть дерево через деревья, фильтровать только за ошибки, учитывая, что я использовал вызовы LOGE . Хотя не каждый раз, когда я запускаю это переработанное тестовое приложение, аргумент развращается, он все еще иногда … что указывает на довольно неприятную ошибку компилятора …

Обновление 2

Что касается второго теста, то я убеждаю себя еще больше, поэтому я зарегистрировал его как QC 121312 .

Обновление 3

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

 program CanaryInCoalmine; {$APPTYPE CONSOLE} uses System.SysUtils; type ICanary = interface function GetName: string; property Name: string read GetName; end; TCanary = class(TInterfacedObject, ICanary) strict private FName: string; function GetName: string; public constructor Create(const AName: string); destructor Destroy; override; end; TCoalmine = class private FCanary: ICanary; procedure ChangeCanary(const Arg: ICanary); public procedure Dig; end; constructor TCanary.Create(const AName: string); begin inherited Create; FName := AName; WriteLn(FName + ' is born!'); end; destructor TCanary.Destroy; begin WriteLn(FName + ' has tweeted its last song'); inherited; end; function TCanary.GetName: string; begin Result := FName; end; procedure TCoalmine.ChangeCanary(const Arg: ICanary); var OldName: string; begin Writeln('Start of ChangeCanary - reassigning FCanary...'); OldName := Arg.Name; FCanary := TCanary.Create('Yellow Meanie'); Writeln('FCanary reassigned - is ' + OldName + ' still alive...?'); Writeln('Exiting ChangeCanary...'); end; procedure TCoalmine.Dig; begin FCanary := TCanary.Create('Tweety Pie'); ChangeCanary(FCanary); end; var Coalmine: TCoalmine; begin Coalmine := TCoalmine.Create; Coalmine.Dig; ReadLn; end. 

Вывод:

 Tweety Pie is born! Start of ChangeCanary - reassigning FCanary... Yellow Meanie is born! Tweety Pie has tweeted its last song FCanary reassigned - is Tweety Pie still alive...? Exiting ChangeCanary... 

Таким образом, переназначение поля уменьшает счетчик ссылок на предыдущий объект, что указывает на отсутствие другой сильной ссылки на него, уничтожает его там и тогда до ChangeCanary процедуры ChangeCanary .

Solutions Collecting From Web of "Константные строковые параметры и компилятор Delphi XE5 для Android"

Мы провели некоторые внутренние исследования, и, оказывается, это зависит от того, как написан код, и компилятор действительно ничего не может с этим поделать. Он немного сложный, но, короче, метод GoToDirectory принимает параметр const string (Dir), который ссылается на строку. Однако в коде метода заменяется строка новой (которая может быть одинаковой или в другой ячейке памяти). Учитывая, что параметр const не увеличивает счетчик ссылок, если вы уменьшаете количество ссылок той же строки в коде, строка удаляется. Таким образом, у вас есть параметр, указывающий на неопределенное расположение памяти, и фактический вывод является случайным. Такая же проблема происходит (может случиться) на всех платформах, а не на мобильных устройствах.

Существует много обходных решений:

1) не имеет параметра const (поэтому число ссылок больше, вы изменяете строку с привязкой, но параметр теперь является ссылкой на отдельную строку

2) Передайте псевдоним строки:

  Tmp := FParentDir; GoToDirectory(Tmp); 

3) Назначьте параметр «const String» временной переменной:

 procedure TForm1.GoToDirectory(const Dir: string); var TmpDir: String; begin TmpDir := Dir; 

Я знаю, что это далеко не чистое описание, и мне пришлось несколько раз его красить, но это сценарий, который компилятор не может обрабатывать автоматически, поэтому мы собираемся закрыть отчет об ошибке «как запроектировано».

Чтобы немного расширить комментарий к Марко, это явное падение использования параметра const по параметру было в Delphi с момента введения параметров const и не является ошибкой, а скорее особенностью, которая является примером вашего примера Он не должен использоваться.

Константный модификатор является обещанием вызывающему, что нет способа передать переменную в качестве параметра в качестве побочного эффекта вызова. Самый простой способ гарантировать это – никогда не изменять глобально доступную переменную в функции или процедуре с параметром const . Это позволяет вызываемому полагаться на подсчет ссылок вызывающего абонента, исключать семантику копирования и т. Д. Другими словами, он сообщает компилятору, что если значение более эффективно передается как var и его можно рассматривать как параметр var (то есть, Имеет lvalue), то передайте его как var вместо значения. Если это управляемый тип, как строка, он также может полагаться на ссылку вызывающего, чтобы поддерживать память в памяти.

Этот контракт нарушен GoToDirectory когда он изменяет глобальную доступную строку (любой доступ к куче должен считаться глобальным, в этом контексте, даже если это поле объекта). GoToDirectory не должен иметь параметр const поскольку он нарушает контракт, подразумеваемый const .

Обратите внимание, что это значительно отличается от контракта, подразумеваемого const на других языках, таких как C ++. К сожалению, в то время не было лучшего слова. Фактически это говорит о том, что функция или процедура являются чистыми относительно переменных, совместимых с формальным типом аргумента const , а не для того, чтобы не изменять аргумент. Легче просто помнить, не применяйте const к любому параметру функции или процедуры, которая имеет побочный эффект.

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

FWIW, я не могу воспроизвести проблему локально с помощью XE5 Update 2, Android 4.4.2, на Nexus 7 с вашей версией, отличной от FMX. Проект был построен с использованием ваших пошаговых инструкций (копирование / вставка кода) и запуск в режиме отладки на устройстве. Выход журнала:

Захват окна Android Debug Monitor

Чтобы быть уверенным, что я не смог воспроизвести его, я несколько раз создавал и запускал приложение с теми же результатами.

Однако версия FMX имеет непоследовательные результаты. В первый раз, когда я побежал и построил его, он получил нарушение прав доступа после третьего ShowMessageFmt и должен был быть остановлен. Затем я снова построил его, запустил и смог увидеть все четыре диалоговые окна ShowMessageFmt , но последний отобразил неверное значение:

 Prior to calling foo, Dir is "/alpha/beta" After to calling foo, Dir is "/alpha/beta" Prior to calling foo, Dir is "/alpha" After to calling foo, Dir is "" 

Третий и четвертый повторы сборки и запуска дали тот же результат, что и второй.

Я бы сказал, что это ошибка. Он открыт, и команда R & D в Embarcadero будет исследовать его.