Инициализировать результат строковой функции?

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

function TMyObject.GenerateInfo: string;

        procedure AppendInfo(const AppendStr: string);
        begin
          if(Result > '') then
            Result := Result + #13;
          Result := Result + AppendStr;
        end;

begin
  if(ACondition) then
    AppendInfo('Some Text');
end;

Многократный вызов этой функции привел к:

"Some Text"

первый раз,

"Some Text"
"Some Text"

второй раз,

"Some Text"
"Some Text"
"Some Text"

третий раз и т. д.

Чтобы исправить это, мне пришлось инициализировать Результат:

begin
  Result := '';
  if(ACondition) then
    AppendInfo('Some Text');
end;

Обязательно ли инициализировать результат строковой функции? Почему (технически)? Почему компилятор не выдает предупреждение «W1035 Возвращаемое значение функции 'xxx' может быть неопределенным» для строковых функций? Нужно ли мне просматривать весь мой код, чтобы убедиться, что значение установлено, поскольку ненадежно ожидать от функции пустой строки, если результат не задан явно?

Я протестировал это в новом тестовом приложении, и результат тот же.

procedure TForm1.Button1Click(Sender: TObject);
var
  i: integer;
  S: string;
begin
  for i := 1 to 5 do
    S := GenerateInfo;
  ShowMessage(S); // 5 lines!
end;

person avenmore    schedule 14.07.2010    source источник
comment
Я испытал нечто подобное с функцией, возвращающей интерфейс. Похоже, существует общая проблема с типами данных, требующими инициализации. Я не пробовал динамические массивы, но думаю, что это также выявило бы эту проблему.   -  person dummzeuch    schedule 15.07.2010


Ответы (7)


Это не ошибка, но "функция":

Для строки, динамического массива, указателя метода или варианта результата эффекты такие же, как если результат функции был объявлен как дополнительный параметр var после объявленных параметров. Другими словами, вызывающий передает дополнительный 32-битный указатель, указывающий на переменную, в которой должен быть возвращен результат функции.

Т.е. ваш

function TMyObject.GenerateInfo: string;

Неужели это:

procedure TMyObject.GenerateInfo(var Result: string);

Обратите внимание на префикс «var» (а не «out», как вы могли ожидать!).

Это ТАКОЕ неинтуитивно понятно, поэтому приводит ко всевозможным проблемам в коде. Рассматриваемый код - всего лишь один пример результатов этой функции.

Просмотрите и проголосуйте за этот запрос.

person Alex    schedule 14.07.2010
comment
Нельзя добросовестно называть это функцией. Возможно, неправильная функция: catb.org/jargon/html/M/misfeature.html. Но однозначный +1 за ссылку на документы. - person Joe White; 15.07.2010
comment
Я не думаю, что какие-то последствия не интуитивны. Я всегда рассматривал result как локальную переменную, и их обычно (целые, действительные, логические и т. Д.) Нужно инициализировать вручную. Поэтому я довольно сильно реагирую на код OP, в котором отсутствует result := '' первая строка. - person Andreas Rejbrand; 15.07.2010
comment
›Я всегда рассматривал результат как локальную переменную, а это не. Результат - выходной аргумент функции. Это не локальная переменная. - person Alex; 15.07.2010
comment
@Alexander: Интересно, почему кто-то проголосовал против ... Ваш ответ определенно лучше моего, по крайней мере ... - person Andreas Rejbrand; 15.07.2010
comment
@Andreas Rejbrand Что ж, может быть, есть люди, которые на самом деле используют эту функцию как функцию, и они не хотят, чтобы она была исправлена. - person Alex; 15.07.2010
comment
Возможно, это было более интуитивно понятно в старые времена, когда у нас не было переменной Result, но вместо этого приходилось назначать значение результата имени функции (например, GenerateInfo: = 'Some Text' вместо Result: = 'Some Text'). Таким образом, вы могли смотреть на функцию как на расширенную глобальную переменную с соответствующим блоком кода, и вам приходилось явно определять локальные переменные, которые вам нужны в функции. Если вы попытаетесь использовать код GenerateInfo: = GenerateInfo + 'Some Text', это не имеет смысла (фактически он создает бесконечный рекурсивный цикл). +1 к пункту Андреаса: напишите чистый код, который имеет смысл. - person Jørn E. Angeltveit; 15.07.2010
comment
@Alexander: Даже лучшие могут неправильно понять концепцию переменной Result. См. Зарко Гаджич на странице delphi.about.com/od/beginners/a/subroutines. htm: поскольку каждая функция неявно имеет локальную переменную. Результат [...] - person Jørn E. Angeltveit; 15.07.2010
comment
@ Jørn E. Angeltveit Хорошо, значит, вы (и Андреас) говорите переменные инициализации перед использованием, верно? Для меня нет проблем, это имеет смысл. Теперь попробуйте применить это правило к функции, которая возвращает запись со строкой. Как мы обычно инициализируем запись? Конечно, с помощью FillChar! Угадайте, что в этом случае будет делать FillChar? Поскольку Result на самом деле является переменной input, ваш код, написанный по хорошим правилам, уничтожит существующий строковый указатель. Не красиво, правда? Таким образом, небезопасно думать о Result как о чем-либо, кроме того, что он есть: аргумент in / out. - person Alex; 16.07.2010
comment
P.S. Обратите внимание, что использование FillChar для реальной локальной переменной будет работать. Это потому, что локальные переменные не инициализируются. Результат не является локальной переменной, фактически он инициализирован и может содержать непустое значение. Это совершенно неожиданно. Итак, попытка использовать FillChar не удалась. - person Alex; 16.07.2010
comment
P.P.S. Когда мы говорим об инициализации для локальных записей, это может быть немного нечетко. Локальные записи не инициализируются в том смысле, что они не заполняются нулями. Локальные записи инициализируются в том смысле, что они содержат обнуленные поля управляемых типов (строки, интерфейсы и т. Д.). Надеюсь, вы понимаете, что я пытаюсь сказать. - person Alex; 16.07.2010
comment
@Alexander: Ну, я не говорю, что вы должны инициализировать все (в смысле обнуления каждого бита в переменной), прежде чем использовать это каким-либо образом. Я говорю, что вы должны писать чистый код, который ничего не предполагает. Если функция просто присваивает значение каждому полю в записи, это будет достаточно хорошей инициализацией. Итак, как мне инициализировать запись? Создайте функцию инициализации, которая инициализирует каждое поле в записи предпочтительными значениями по умолчанию, и вызовите эту функцию вместо FillChar. Для этого в Delphi 2006 были введены конструкторы записей. - person Jørn E. Angeltveit; 16.07.2010
comment
Пара вопросов: всегда ли один и тот же указатель повторно используется для передачи функции из цикла вызывающей процедуры? Если компилятор изменит параметр на out, все ли функции всегда будут выдавать предупреждение, если результат не инициализирован? - person avenmore; 16.07.2010
comment
Первый вопрос: я думаю, что на это нет никаких гарантий. Второй вопрос: если Result когда-нибудь выйдет - да, должно быть предупреждение, как сейчас, скажем, для Integer. - person Alex; 18.07.2010
comment
Итак, я вижу, что QC был закрыт без каких-либо исправлений и без объяснения причин, по которым он был закрыт? - person Wodzu; 21.10.2014
comment
Отчет @Wodzu QC был написан как отчет об ошибке. Это было до открытия документации о var vs. out. Поскольку это задокументировано, такое поведение не является ошибкой. Поэтому отчет закрыт. На самом деле это должен быть отчет о проблеме дизайна. Т.е. просьба изменить это поведение. Я добавил ссылку - надеялся, что она сработает как единое целое, но не вышло ... - person Alex; 21.10.2014
comment
Да, это больше похоже на проблему дизайна, и мы будем попадаться в эту ловушку время от времени. - person Wodzu; 21.10.2014
comment
Это кажется правильным, по крайней мере, при рекурсивном вызове внутренних функций и функций, но это не все, поскольку есть примеры, которые работают так, как вы интуитивно ожидали. Смотрите мой "ответ" - person Clint Good; 31.03.2017

Мы сталкивались с этим раньше, я думаю, может быть, еще в Delphi 6 или 7. Да, даже если компилятор не беспокоится о том, чтобы дать вам предупреждение, вам нужно инициализировать свои строковые переменные Result именно по причине вы столкнулись. Строковая переменная инициализируется - она ​​не начинается как ссылка на мусор - но, похоже, не инициализируется re, когда вы этого ожидаете.

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

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

person Joe White    schedule 14.07.2010
comment
Вызывающий не выделяет место, но передает действительную ссылку на инициализированную переменную, по крайней мере, общая концепция такова, как я ее понимаю. У него есть отчеты: qc.embarcadero.com/wc/qcmain.aspx?d= 894, qc.embarcadero.com/wc/qcmain.aspx? d = 32556, возможно, и другие. - person Sertac Akyuz; 15.07.2010
comment
Это не ошибка - см. Мой ответ. - person Alex; 15.07.2010

Это не ошибка. По определению никакая переменная внутри функции не инициализируется, включая Результат.

Таким образом, ваш Результат - undefind при первом вызове, и может содержать что угодно. Как это реализовано в компиляторе, не имеет значения, и вы можете получить разные результаты в разных компиляторах.

person dmajkic    schedule 15.07.2010

Похоже, что ваша функция должна быть упрощена следующим образом:

function TMyObject.GenerateInfo: string;
begin
  if(ACondition) then
    Result := 'Some Text'
  else
    Result := '';
end;

Обычно вы не хотите использовать Result справа от присваивания в функции.

В любом случае, строго в иллюстративных целях, вы также можете сделать это, хотя и не рекомендуется:

procedure TForm1.Button1Click(Sender: TObject);
var
  i: integer;
  S: string;
begin
  for i := 1 to 5 do
  begin
    S := ''; // Clear before you call
    S := GenerateInfo;
  end;
  ShowMessage(S); // 5 lines!
end;
person Marcus Adams    schedule 15.07.2010

Похоже на ошибку в D2007. Я только что протестировал его в Delphi 2010 и получил ожидаемое поведение. (1 строка вместо 5.)

person Mason Wheeler    schedule 14.07.2010
comment
Я только что протестировал его в Delphi 2009 и получил все пять строк. - person Andreas Rejbrand; 15.07.2010
comment
Это не ошибка - см. Мой ответ. - person Alex; 15.07.2010
comment
Но тогда это баг с D2010? - person Sertac Akyuz; 15.07.2010
comment
@Sertac Akyuz: Я думаю, что Мейсон Уиллер просто не тестировал это правильно. (Я сделал ту же ошибку при первом тестировании кода - не осознавал, что мне нужно было вызывать GenerateInfo несколько раз.) - person Andreas Rejbrand; 15.07.2010
comment
Да, я согласен: я думаю, что Мейсон Уиллер не тестировал это должным образом. Эта проблема существует во всех версиях Delphi (проверено в D3-D2010). - person Alex; 15.07.2010
comment
Ваш QC утверждает, что D3 и D4 не были затронуты, а в D5 появилась ошибка. Вот расскажи про D3 в качестве отправной точки? - person Arioch 'The; 15.08.2012

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

Во многих местах строки передаются по ссылке, передаются по значению, но все эти строки ожидают строки VALID, в которых счетчик управления памятью имеет какое-то допустимое, а не мусорное значение. Поэтому для того, чтобы строки оставались действительными, единственное, что можно сказать наверняка, - это то, что они должны быть инициализированы при первом введении. Например, для любой строки локальной переменной это необходимо, поскольку именно здесь вводится строка. Любое другое использование строк, включая function (): string (которая на самом деле процедура (var Result: string), как правильно указал Александр), ожидает только действительные строки в стеке, а не инициализированные . И действительность здесь исходит из того факта, что конструкция (var Result: string) говорит, что «я жду действительной переменной, которая определенно была введена ранее». ОБНОВЛЕНИЕ: из-за этого фактическое содержимое Result является неожиданным, но из-за той же логики, если это единственный вызов этой функции, который имеет локальную переменную слева, пустота строки в этом случае гарантируется.

person Maksee    schedule 15.07.2010

Ответ Алекса почти всегда правильный, и он объясняет, почему я наблюдал такое странное поведение, но это еще не все.

Следующее, скомпилированное без оптимизации, дает ожидаемый результат, что sTemp является пустой строкой. Если вы замените функцию вызовом процедуры, вы получите другой результат.

Похоже, что для фактического программного модуля существует другое правило.

По общему признанию, это угловой случай.

program Project1;

{$APPTYPE CONSOLE}

uses System.SysUtils;

  function PointlessFunction: string;
  begin
  end;

  procedure PointlessProcedure(var AString: string);
  begin
  end;

var
  sTemp: string;
begin
  sTemp := '1234';
  sTemp := PointlessFunction;
  //PointlessProcedure(sTemp);
  WriteLn('Result:' + sTemp);
  ReadLn;
end.
person Clint Good    schedule 31.03.2017