【问题标题】:Problems returning self as a function result返回 self 作为函数结果的问题
【发布时间】:2013-01-27 13:38:57
【问题描述】:

我有一个非常简单的 3D 矢量类定义 TVector3D,以及一些用于实现 TVector3D.Normalise 函数的方法。如果我向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 适用于非单位向量,即IsUnitVector 返回 false。但是对于单位向量,例如(1,0,0),我没有返回自身,而是在之前存在非零的地方得到一个非常低的非零数字的结果,例如(8.47122...E-38,0,0)

如果我通过调试器运行此程序,并在 Result := Self 行设置一个断点来评估 Self,Self 是 (1,0,0) 但结果变为 (Very Low Number,0,0)Very Low Number 每次运行程序时都会发生变化,但似乎总是在 E-38/E-39 附近。

我不明白为什么会这样。为什么会发生这种情况以及如何最好地更改我的 Normalise 函数以避免它。

【问题讨论】:

  • 我会让向量成为记录而不是类,但是 YMMV。
  • 您对TVector3D.Normalise 实现的想法要求TVector3D 类型是引用计数接口而不是类。
  • 我之前已经告诉过你,至少一次,要让这成为记录。我不明白你为什么不接受这个建议。
  • @DavidHeffernan 我认识到您使用记录进行编码的建议是合理的,我将继续这样做。但是,使用记录重新编码并不能帮助我理解为什么上面的代码给了我它的结果。如果您阅读了这个问题,这就是我在这里提出的问题。现在我知道了答案,我将用记录重新实现它。
  • 很公平。但我看起来比这个问题更广泛。例如,我可以看到您创建了两个对象,但释放了四个。因为根据您的设计,nUVNormednonUnitVector 实际上指的是同一个对象。另一对也是如此。所以你可以在免费后访问,并且双倍免费。而这些问题会吞噬你。因此,我敦促您尽快开始使用记录,从而避免这些陷阱。我已经为你提供了一些代码来开始。这是该程序的代码:orcina.com/SoftwareProducts/OrcaFlex

标签: delphi delphi-xe3


【解决方案1】:

您当前的TVector3D.Normalise 实现存在一些问题:

  • 最后 4 行总是执行,因为在 else 之后没有使用 begin-end 块,
  • 因此例程永远不会返回Self,而是始终返回一个新实例,
  • 返回的实例的内存可能会泄漏,因为您在函数调用后失去了对它的所有权,
  • IsUnitVector 返回True 时,MagnitudeFactor 的赋值将被跳过,它将是一个随机值(当前存在于该内存的地址),这就解释了为什么你会得到垃圾。编译器还会警告您:变量MagnitudeFactor 可能尚未初始化。

相反,我会改写如下例程:

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

【讨论】:

  • +1,我错过了这一点 (begin...end)。我专注于更“重要”的问题(=不能归类为“错字”):您永远不知道函数返回时是否有一个或两个实例。
  • 很好的答案,谢谢!所以我的头痛是由于排除了 begin..end 块!我是否认为这可以发出是在 else 之后只有一行,但如果有多行则不是?
  • @Trojanian:是的,else 对应下一行代码。如果该行是begin,则包括整个begin...end 块(可能这里有一些标准术语,我不知道......)。
  • 这个答案只是表面上的问题。这里的整个设计是一场灾难。就地操作的函数应该是一个不返回任何内容的过程。没有提到问题中的代码在释放内存后访问内存!整个事情已经彻底崩溃了。
  • @NGLN - 还有一点 - 除法,应该有乘法,并三次调用重型函数而不是缓存结果。请参阅stackoverflow.com/questions/14461199pastebin.ca/2306480 BTW,大卫通过引入Vector/Scalar 操作优雅地解决了这个问题:-)
【解决方案2】:

所有问题的根源在于您使用了一个类,它是一个引用类型。相反,您需要使您的向量成为值类型。这意味着使用record

在您的代码中,即使您修复了@NGLN 确定的问题,当您开始调用WriteLn 时,您仍然销毁了类的所有实例。

除非你很快掌握这个问题,否则我担心你会继续遇到问题。与您当前的方法相比,切换到使用值类型将使您的编码变得非常简单。

以下是帮助您入门的内容:

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);

【讨论】:

  • +1 出色的工作! (给读者的一些练习:可以使用 * 运算符计算两个向量之间的内积;创建 normalize procedure;创建 AreOrthogonal 类函数。)
  • @AndreasRejbrand 向量上的乘积算子太多了。所以我个人不使用 op 重载来实现任何东西。我最初将Normalise 写为一个过程,但使用函数来组合功能更容易。因此Normalised。但是,实际上有很多方法可以以有用的方式扩展它。首先,您需要TMatrix3,然后是对矩阵、矩阵/向量等进行运算的乘积运算符。
  • 确实,尤其是向量之间的向量(交叉)积会竞争(可能也是张量积)。不过,矩阵时间向量似乎很明显。无论如何,设计这些简单但有用的小记录还是很有趣的,尤其是在引入高级记录之后(带有运算符重载等)。
  • @AndreasRejbrand 很有趣,而且真的非常有用和强大。我公司的全部生计都是靠这一张小唱片所完成的工作!
  • @DavidHeffernan 非常感谢你 - 它为我节省了很多自己编写代码的工作。我会很开心扩展这个。 :-)
【解决方案3】:

一些提示:

首先,如果我是你,我实际上会将向量设为记录而不是类,但 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;

第三,您的Normalize 函数如果需要规范化,则返回一个new 向量对象,否则返回same 对象。这很令人困惑。你永远不知道你有多少实例!相反,把它变成一个程序:

procedure TVector3D.Normalize;
var
  norm: single;
begin
  norm := GetMagnitude;
  x := x / norm;
  y := y / norm;
  z := z / norm;
end;

第四,为什么使用single而不是doublereal

第五,正如 NGLN 指出的那样(请支持他的回答!),您忘记了 else 部分中的 begin...endNormalize 函数,所以最后四行 总是 执行!因此,您总是创建一个新的向量实例!不过,我的观点非常重要:您的原始函数“打算”(如果您只是添加 begin...end 块)返回 self 或根据条件创建一个新实例,这是相当可怕的,从那时起您不知道你有多少实例! (因此,您可能会开始泄漏向量...)

【讨论】:

  • 谢谢你,@Andreas!尽管在不必要时运行 Normalise 不会对单位向量造成轻微的性能损失吗?为什么要规范化已经规范化的东西。请注意,我想在实践中它会加快速度,因为当向量尚未标准化时,检查是不必要的。
  • @Trojanian:这取决于情况,正如您所说:向量的哪一部分将被归一化?尽管如此,这种“微优化”很少值得担心。 [事实上,从长远来看,我会更关心精度的差异。]
  • 我使用的是 single,因为在实践中我使用的向量太多,我真的需要内存并且愿意牺牲准确性。
  • 那你真的应该使用记录!它节省了四分之一。
  • @Trojanian 我还是觉得你最好给你的数据类型一个特殊的名字。因此,您可以通过更改单行将其更改为 real 或 double 甚至其他类型。就像在stackoverflow.com/questions/14461199
猜你喜欢
  • 2011-11-18
  • 2021-07-22
  • 1970-01-01
  • 1970-01-01
  • 2019-12-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-11-23
相关资源
最近更新 更多