Проблемы, возвращающие self как результат функции

у меня есть очень простое определение класса для 3D-векторов, TVector3D и несколько методов, используемых для реализации . Если я пройду Normalise функция вектор, который уже нормализован, я хочу, чтобы он вернул вектор, который я его передал. Здесь я использовал Result := Self но у меня есть некоторые сумасшедшие возвращения.

консольное приложение:

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils;

type
  TVector3D = Class
  public
    x : Single;
    y : Single;
    z : Single;
    constructor Create(x : Single;
                       y : Single;
                       z : Single);
    function GetMagnitude() : Single;
    function IsUnitVector() : Boolean;
    function Normalise() : TVector3D;
  end;

  constructor TVector3D.Create(x : Single;
                               y : Single;
                               z : Single);
  begin
    Self.x := x;
    Self.y := y;
    Self.z := z;
  end;

  function TVector3D.GetMagnitude;
  begin
    Result := Sqrt(Sqr(Self.x) + Sqr(Self.y) + Sqr(Self.z));
  end;

  function TVector3D.IsUnitVector;
  begin
    if Self.GetMagnitude = 1 then
      Result := True
    else
      Result := False;
  end;

  function TVector3D.Normalise;
  var
    x : Single;
    y : Single;
    z : Single;
    MagnitudeFactor : Single;
  begin
    if IsUnitVector then
      Result := Self
    else
      MagnitudeFactor := 1/(Self.GetMagnitude);
      x := Self.x*MagnitudeFactor;
      y := Self.y*MagnitudeFactor;
      z := Self.z*MagnitudeFactor;
      Result := TVector3D.Create(x,
                                 y,
                                 z);
  end;

  procedure TestNormalise;
  var
    nonUnitVector : TVector3D;
    unitVector : TVector3D;
    nUVNormed : TVector3D;
    uVNormed : TVector3D;
  begin
  //Setup Vectors for Test
    nonUnitVector := TVector3D.Create(1,
                                      1,
                                      1);
    unitVector := TVector3D.Create(1,
                                   0,
                                   0);
  //Normalise Vectors & Free Memory
    nUVNormed := nonUnitVector.Normalise;
    nonUnitVector.Free;
    uVNormed := unitVector.Normalise;
    unitVector.Free;
  //Print Output & Free Memory
    WriteLn('nUVNormed = (' + FloatToStr(nUVNormed.x) + ', ' + FloatToStr(nUVNormed.y) + ', ' + FloatToStr(nUVNormed.z) + ')');
    nUVNormed.Free;
    WriteLn('uVNormed = (' + FloatToStr(uVNormed.x) + ', ' + FloatToStr(uVNormed.y) + ', ' + FloatToStr(uVNormed.z) + ')');
    uVNormed.Free;
  end;

begin
  try
    TestNormalise;
    Sleep(10000);
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

Normalise работает нормально для не-устройство vecors, т. е. IsUnitVector возвращает false. Но для единичных векторов, таких как (1,0,0), вместо того, чтобы возвращаться, я получаю результат с очень низкими ненулевыми числами, где раньше было ненулевое значение, например (8.47122...E-38,0,0).

если я запускаю это через отладчик с точкой останова в строке Result := Self установить для оценки себя, Self is (1,0,0) пока результат становится (Very Low Number,0,0). Где Very Low Number изменения каждый раз, когда я запускаю программу, но всегда кажется, чтобы быть вокруг!--13-->.

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

3 ответов


текущего TVector3D.Normalise реализация имеет несколько вопросов:

  • последние 4 строчки всегда выполнена, потому что вы не использовали begin-end блок после else,
  • таким образом, рутина никогда не возвращается Self, но всегда новый экземпляр,
  • память возвращаемого экземпляра вероятно просочилась, потому что вы потеряли право собственности на него после вызова функции,
  • , когда IsUnitVector возвращает True, затем назначение MagnitudeFactor будет пропущено, и это будет случайное значение (в настоящее время присутствует в адресе этой памяти), что объясняет, почему вы получаете rubish. Компилятор также предупреждает вас об этом:переменная MagnitudeFactor возможно, не были инициализированы.

вместо этого я бы переписал процедуру следующим образом:

function TVector3D.Normalise: TVector3D;
begin
  if not IsUnitVector then
  begin
    x := x / GetMagnitude;
    y := y / GetMagnitude;
    z := z / GetMagnitude;
  end;
  Result := Self;
end;

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

в вашем коде, даже когда вы исправляете проблему, идентифицированную @NGLN, вы все равно уничтожили все экземпляры своего класса к моменту начала вызова WriteLn.

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

вот что вам нужно для начала:

type
  TVector3 = record
  public
    class operator Negative(const V: TVector3): TVector3;
    class operator Equal(const V1, V2: TVector3): Boolean;
    class operator NotEqual(const V1, V2: TVector3): Boolean;
    class operator Add(const V1, V2: TVector3): TVector3;
    class operator Subtract(const V1, V2: TVector3): TVector3;
    class operator Multiply(const V: TVector3; const D: Double): TVector3;
    class operator Multiply(const D: Double; const V: TVector3): TVector3;
    class operator Divide(const V: TVector3; const D: Double): TVector3;
    class function New(const X, Y, Z: Double): TVector3; static;
    function IsZero: Boolean;
    function IsNonZero: Boolean;
    function IsUnit: Boolean;
    function Mag: Double;
    function SqrMag: Double;
    function Normalised: TVector3;
    function ToString: string;
  public
    X, Y, Z: Double;
  end;

const
  ZeroVector3: TVector3=();

class operator TVector3.Negative(const V: TVector3): TVector3;
begin
  Result.X := -V.X;
  Result.Y := -V.Y;
  Result.Z := -V.Z;
end;

class operator TVector3.Equal(const V1, V2: TVector3): Boolean;
begin
  Result := (V1.X=V2.X) and (V1.Y=V2.Y) and (V1.Z=V2.Z);
end;

class operator TVector3.NotEqual(const V1, V2: TVector3): Boolean;
begin
  Result := not (V1=V2);
end;

class operator TVector3.Add(const V1, V2: TVector3): TVector3;
begin
  Result.X := V1.X + V2.X;
  Result.Y := V1.Y + V2.Y;
  Result.Z := V1.Z + V2.Z;
end;

class operator TVector3.Subtract(const V1, V2: TVector3): TVector3;
begin
  Result.X := V1.X - V2.X;
  Result.Y := V1.Y - V2.Y;
  Result.Z := V1.Z - V2.Z;
end;

class operator TVector3.Multiply(const V: TVector3; const D: Double): TVector3;
begin
  Result.X := D*V.X;
  Result.Y := D*V.Y;
  Result.Z := D*V.Z;
end;

class operator TVector3.Multiply(const D: Double; const V: TVector3): TVector3;
begin
  Result.X := D*V.X;
  Result.Y := D*V.Y;
  Result.Z := D*V.Z;
end;

class operator TVector3.Divide(const V: TVector3; const D: Double): TVector3;
begin
  Result := (1.0/D)*V;
end;

class function TVector3.New(const X, Y, Z: Double): TVector3;
begin
  Result.X := X;
  Result.Y := Y;
  Result.Z := Z;
end;

function TVector3.IsZero: Boolean;
begin
  Result := Self=ZeroVector3;
end;

function TVector3.IsNonZero: Boolean;
begin
  Result := Self<>ZeroVector3;
end;

function TVector3.IsUnit: Boolean;
begin
  Result := abs(1.0-Mag)<1.0e-5;
end;

function TVector3.Mag: Double;
begin
  Result := Sqrt(X*X + Y*Y + Z*Z);
end;

function TVector3.SqrMag: Double;
begin
  Result := X*X + Y*Y + Z*Z;
end;

function TVector3.Normalised;
begin
  Result := Self/Mag;
end;

function TVector3.ToString: string;
begin
  Result := Format('(%g, %g, %g)', [X, Y, Z]);
end;

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

использование перегрузки операторов делает код гораздо более читаемым. Теперь вы можете написать V3 := V1 + V2 и так далее.

вот как выглядит ваш тестовый код с этим запись:

var
  nonUnitVector: TVector3;
  unitVector: TVector3;
  nUVNormed: TVector3;
  uVNormed: TVector3;

begin
  //Setup Vectors for Test
  nonUnitVector := TVector3.New(1, 1, 1);
  unitVector := TVector3.New(1, 0, 0);

  //Normalise Vectors
  nUVNormed := nonUnitVector.Normalised;
  uVNormed := unitVector.Normalised;

  //Print Output
  WriteLn('nUVNormed = ' + nUVNormed.ToString);
  WriteLn('uVNormed = ' + uVNormed.ToString);
  Readln;
end.

или если вы хотите сжать несколько:

WriteLn('nUVNormed = ' + TVector3.New(1, 1, 1).Normalised.ToString);
WriteLn('uVNormed = ' + TVector3.New(1, 0, 0).Normalised.ToString);

несколько советов:

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

function TVector3D.IsUnitVector;
begin
  if self.GetMagnitude = 1 then
    result := True
  else
    result := False;
end;

обычно пишется, синтаксически и точно эквивалентно,

function TVector3D.IsUnitVector;
begin
  result := GetMagnitude = 1
end;

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

function TVector3D.IsUnitVector;
begin
  result := IsZero(GetMagnitude - 1)
end;

в-третьих, ваш всегда создайте новый векторный экземпляр! Тем не менее, моя точка зрения очень важна: ваша исходная функция "намеревается" (если вы просто добавите begin...end block) Для возврата self или создать новый экземпляр в зависимости от условия, что довольно страшно, так как вы не знаете, сколько экземпляров у вас есть! (И поэтому вы, вероятно, начнете пропускать векторы...)