11

当我调用函数来获取值时,我通常会初始化变量,以防函数失败或不返回任何内容,并且我想避免处理未初始化的变量。我对字符串、整数或任何其他类型做同样的事情。

整数变量的示例:

vPropValue := 0;
vPropValue := GetPropValue(vObject,'Height');

IF vPropValue > 0 Then
...

这是我最常见的使用方式。

我知道我可以使用:

If GetPropValue(vObject,'Height') > 0 Then
...

但是对于第一个示例,如果稍后在代码中再次需要结果,我会避免多次调用函数。

字符串相同(即使我知道本地字符串被初始化为空字符串,而整数不是可以保存任何值)

vName := '';
vName := GetObjectName(vObject,'ObjectName');

IF Trim(vPropStrValue) <> '' Then
...

我知道我可以采取措施避免重复的赋值,比如确保 Function 在一切都失败时返回 0。但是我有 100 个函数,我不能依赖我从来没有犯过函数如何处理所有事情的错误,我敢肯定,如果一切都失败了,有些函数不会返回 0。

我试图理解为什么这不是可取的做法以及如何最好地避免它。

编辑

这是函数没有返回正确值或 0 的示例:

function GetValue(vType:integer):integer;
begin
   if vType=1 then
    Result:=100
   else if (vType>2) and (vType<=9) then
     Result:=200;

end;

procedure TForm1.Button1Click(Sender: TObject);
var vValue:integer;
begin

  vValue:=GetValue(11);
  Button1.Caption:=IntToStr(vValue);

end;

在这种情况下,函数返回的值是某个随机数。

在这种情况下,初始化似乎是有效的方法。或不?

编辑2:

正如大卫在他的回答中指出的那样,正确,有一个警告

[dcc32 Warning] Unit1.pas(33): W1035 Return value of function 'GetValue' might be undefined

但是,我无缘无故地忽略了它,只是没有看那里。因为它让我编译它,我认为它没问题。因此,我确实寻找了警告并“修复”了很多具有类似问题的函数,因为所有 IF 结果可能尚未定义。

编辑 3 和结论:

我希望它增加了问题和解释的范围:

也许我在我的大多数函数中使用的另一个扭曲的例子也可以解释为什么我认为我的变量初始化是必要的,我不确定我的函数是否会一直正确运行,尤其是在嵌套函数的情况下. 他们中的大多数仍然是这样设置的:

function GetProperty(vType:integer):integer;
begin
  Try
    if vType = 99 then
      Result:=GetDifferentProperty(vType)// <-- Call to another Function, that could return whatever...
    else
    begin
       if vType=1 then
          Result:=100
       else if (vType>2) and (vType<=9) then
         Result:=200;
    end;
  except
  end;
end;

现在我正在解决这些问题Try Except End;,但有些功能已经有 10 年的历史了,根据我当时的经验,期望它们能够 100% 工作,并不是可以依赖的。

作为该项目的唯一开发人员,我认为我应该信任我的函数(以及我的其余代码),但我无法想象在多个开发人员环境中所有函数都已正确设置。

所以我的结论是:由于我没有处理基础知识——正确设计的函数,我需要进行所有这些检查(变量初始化、Try Except行..)以及可能一些其他不必要的东西。

4

4 回答 4

15

假设这vPropValue是一个局部变量,那么这段代码

vPropValue := 0;
vPropValue := GetPropValue(vObject,'Height');

无法区分

vPropValue := GetPropValue(vObject,'Height');

一个更简单的例子可能是这样的:

i := 0;
i := 1;

分配0i然后立即分配1给有什么意义i?所以,你肯定永远不会写那个。你会写:

i := 1;

在您的代码中,在此答案的顶部,您将两次分配给同一个变量。在第一个赋值中分配的值立即被第二个赋值中分配的值替换。因此,第一个分配是没有意义的,应该被删除。

第二个例子稍微复杂一些。假设您的函数编写正确,并且始终分配给它们的返回值,并且这vName是一个局部变量,那么

vName := '';
vName := GetObjectName(vObject,'ObjectName');

无法区分

vName := GetObjectName(vObject,'ObjectName');

我添加额外条件的原因与函数返回值实现的一个怪癖有关,下面将讨论。本例与上述情况的区别在于返回值类型。这里是托管类型,string而在第一个示例中,类型是简单的Integer

同样,考虑到函数总是分配给返回值的附带条件,第一次分配是没有意义的,因为该值会立即被替换。删除第一个作业。


关于编辑中的函数,如果您启用提示和警告,编译器将警告您其错误实现。编译器会告诉您并非所有代码路径都返回值。

function GetValue(vType:integer):integer;
begin
  if vType=1 then
    Result:=100
  else if (vType>2) and (vType<=9) then
    Result:=200;
end;

如果两个条件都不满足,则不会为结果变量赋值。这个函数应该是:

function GetValue(vType:integer):integer;
begin
  if vType=1 then
    Result:=100
  else if (vType>2) and (vType<=9) then
    Result:=200
  else
    Result:=0;
end;

我无法强调始终从函数返回值的重要性。事实上,Delphi 甚至允许编译你的函数是一个可怕的弱点。


您的双重赋值有时对您有用的原因是由于 Delphi 中函数返回值的实现的一个怪癖。与几乎所有其他语言不同,某些更复杂类型的 Delphi 函数返回值实际上是一个var参数。所以这个函数

function foo: string;

实际上,在语义上,与此相同:

procedure foo(var result: string);

这是德尔福设计师做出的一个非常奇怪的决定。在大多数其他语言中,如 C、C++、C#、Java 等,函数返回值就像从被调用者传递给调用者的按值参数。

这意味着,如果您想反常,可以通过函数的返回值将值传递给函数。例如,考虑以下代码:

// Note: this code is an example of very bad practice, do not write code like this

function foo: string;
begin
  Writeln(Result);
end;

procedure main;
var
  s: string;
begin
  s := 'bar';
  s := foo;
end;

当你调用时main,它会输出bar。这是一个相当奇怪的实现细节。你不应该依赖它。让我重复一遍。你不应该依赖它。不要养成在调用站点初始化返回值的习惯。这会导致无法维护的代码。

而是遵循确保函数返回值始终由函数分配的简单规则,并且在分配之前从不读取。


文档提供了有关函数返回值实现的更多详细信息,我强调:

以下约定用于返回函数结果值。

  • 如果可能,在 CPU 寄存器中返回有序结果。在 AL 中返回字节,在 AX 中返回字,在 EAX 中返回双字。
  • 实际结果在浮点协处理器的栈顶寄存器 (ST(0)) 中返回。对于 Currency 类型的函数结果,ST(0) 中的值按 10000 缩放。例如,货币值 1.234 在 ST(0) 中返回为 12340。
  • 对于字符串、动态数组、方法指针或变体结果,效果与将函数结果声明为声明参数之后的 附加 var 参数一样。换句话说,调用者传递了一个额外的 32 位指针,该指针指向一个返回函数结果的变量。
  • Int64 在 EDX:EAX 中返回。
  • 指针、类、类引用和过程指针结果在 EAX 中返回。
  • 对于静态数组、记录和设置结果,如果该值占用一个字节,则在 AL 中返回;如果该值占用两个字节,则在 AX 中返回;如果该值占用四个字节,则在 EAX 中返回。否则,结果将在附加的 var 参数 中返回,该参数在声明的参数之后传递给函数。
于 2015-11-25T23:09:27.177 回答
4

以下代码(一)

vPropValue := 0;
vPropValue := GetPropValue(vObject,'Height');

与 (B) 无法区分

vPropValue := GetPropValue(vObject,'Height');

是否GetPropValue“写得正确”的问题完全无关紧要

让我们考虑一下即使你GetPropValue写错了会发生什么。

function GetPropValue(AObject: TObject; AStr: String): Integer;
begin
  if AStr = 'Hello' then Result := 5;
end;

如您所知,当输入AStr不是“Hello”时,函数的结果几乎是随机的。(为了讨论,假设它会返回-42。)

代码块 (A) 将执行以下操作:

  • 设置vPropValue为 0
  • 然后设置vPropValue为 - 42

代码块 (B) 将vPropValue立即设置为 - 42。

提示:仅仅因为您担心自己在调用的函数中可能犯了错误而编写浪费的代码行是没有意义的。
首先,正如 David 指出的那样,您只需注意编译器提示和警告即可避免许多错误。其次,这种“偏执”的编码只会导致更多的代码浪费,因为现在您必须开始考虑将无效值作为可能的结果。
当有一天您的“安全值”实际上是有效值时,情况会变得更糟。例如,您如何区分“默认 0”和“正确返回 0”?

不要通过不必要的冗余使代码臃肿而人为地使编程变得困难。


边注

在一些特殊情况下,代码的行为可能会有所不同。但是无论如何,您都应该避免导致这些情况的设计,因为它们使维护代码变得更加困难。

我提到它们纯粹是为了完整起见,上面的建议仍然有效。

1) 如果vPropValue作为属性实现,则设置器可能具有导致不同行为的副作用。虽然属性没有任何问题,但当它们做出意想不到的事情时,你就会遇到严重的问题。

2) 如果vPropValue是类上的一个字段(或者更糟糕的是一个全局变量),那么 (A) 和 (B)的行为可能会有所不同,但 GetPropValue前提是引发异常。这是因为异常会阻止分配结果。请注意,除非在特殊情况下,否则这是要避免的,因为它确实使您更难以推理您的代码在做什么。

事实上,这使得避免冗余初始化变得更加重要。您希望您的特殊情况代码看起来与众不同。

于 2015-11-26T08:37:00.803 回答
2

如果 Pastebin 失败,请从顶级评论中收集我的建议

function GetValueUpdate3(vType:integer):integer;
begin
// with SOME types like Integer you can use Pascal case-block instead of error-prone many-ifs-ladder

  case vType of 

    99:   Result:=GetDifferentProperty(vType);// <-- Call to another Function, that could return whatever...
    1:    Result:=100;
    3..9: Result:=200;

    else Result := 12345; // initialization with safe default value for illegal input like vType=2  

  end; // case-block
end;

function GetValueUpdate3e(vType:integer):integer;
begin
  case vType of

    99:   Result:=GetDifferentProperty(vType);// <-- Call to another Function, that could return whatever...
    1:    Result:=100;
    3..9: Result:=200;

    else raise EInvalidArgument.Create('prohibited value - GetValue( ' + IntToStr(vType) +' )' );
      // runtime eror when vType = 2 or any other illegal input
  end;
end;


function GetValueUpdate1(vType:integer):integer;
begin
  Result := 12345; // initialization with safe default value;

  if vType=1 then Exit(100); // special value for special case #1

  if (vType>2) and (vType<=9) then Exit(200); // special value for special case #2

  // exit with default value
end;

procedure TForm1.Button1Click(Sender: TObject);
var vValue:integer;
begin

  vValue:=GetValue(11);
  Button1.Caption:=IntToStr(vValue);

end;

// http://stackoverflow.com/questions/33927750
于 2015-11-26T21:34:49.123 回答
2

您可以使用断言来验证您的输入是否正确。断言有助于发现代码中的逻辑错误。

使用断言迫使您考虑有效和无效的输入数据,并有助于编写更好的代码。在编译时启用和禁用断言是通过 \$C 或 \$ASSERTIONS 编译器(全局开关)完成的

在您的示例中,函数 GetValue 可以使用如下断言:

function GetValue(vType:integer):integer;
begin
   Assert((vType >= 1) and (vType <=9), 'Invalid input value in function GetValue');

   if vType=1 then
    Result:=100
   else if (vType>2) and (vType<=9) then
     Result:=200
   else Result := 0; // always catch the last else!
end;

此外,每个 if 语句都应该捕获最后的 else!(在我看来总是!)

于 2015-11-27T09:24:34.513 回答