【问题标题】:Exception.RaiseOuterException vs. W1035 Return value of function '%s' might be undefinedException.RaiseOuterException 与 W1035 函数“%s”的返回值可能未定义
【发布时间】:2020-11-14 14:18:23
【问题描述】:

这已经被报告为RSP-25603: "Exception.RaiseOuterException can cause wrong W1035 warning"


鉴于以下(演示)函数F,我已将异常引发语句更改为现在链接异常:

--- before
+++ after
@@ -1,11 +1,11 @@
 function F(X: NativeInt): NativeInt;
 begin
   try
     Result := 1 div X;
   except
     on EDivByZero do
-      {ECustom}Exception.Create('...');
+      Exception.RaiseOuterException({ECustom}Exception.Create('...'));
     else
       raise;
   end;
 end;

现在,Ctrl-F9 发出警告 W1035

[dcc32 警告]: W1035 函数“F”的返回值可能未定义

但是,所有情况都会处理。编译器无法将 Exception.RaiseOuterException 识别为 raise 操作。

不幸的是,FAcquireInnerException: BooleanException 类的私有,甚至不能在派生的自定义类中设置为 True,我可以继续直接提升 (raise ECustomException.Create)。

有没有办法让编译器理解,同时保持异常链接?否则我可以想到{$Warn No_RetVal Off}。我还能如何解决此警告?

【问题讨论】:

  • 我经常有一个像 procedure Inv; begin raise EFrogProperties.Create('Invalid frog properties.'); end; 这样的本地子过程,并且还喜欢一些 noreturn 关键字,这样编译器就知道它永远不会返回给调用者。
  • 我曾经很喜欢RaiseOuterException()的设计。为什么他们不简单地引入一个新的构造函数来捕获现有的Exception 或获取当前的Exception,或者至少是一个CreateOuterException() 方法,该方法返回一个新的Exception,可以单独为raised,是在我身后。
  • @RemyLebeau:只是好奇:您对noreturn 关键字或属性有何看法? (也许不是为了解决这个特定问题,但一般来说。)
  • 我创建了一个 EChainedException 类,它在引发时总是捕获内部异常(如 Exception.RaiseOuterExcepotion)。由于您可以直接使用 raise,警告也会消失,但仍然会链接 exceotions
  • @H.Hasenack 查看实现,private FAcquireInnerException: Boolean = False 使Exception 中的整个机制无法访问;例如,protected procedure SetInnerException 成为派生类的无操作。你重新实现机制吗?为了与Exception 保持一致,这是相当多的事情要跟踪。还是突破FAcquireInnerException的知名度?你能分享你的EChainedException的代码吗?

标签: delphi


【解决方案1】:

我能想到的一种避免警告但不禁用警告的方法是:

function F(X: NativeInt): NativeInt;
begin
  try
    Result := 1 div X;
  except
    on E: Exception do
    begin
      if E is EDivByZero then
        Exception.RaiseOuterException({ECustom}Exception.Create('...'));
      raise;
    end;
  end;
end;

更新:另一种方法,如评论中所述,将简单地定义一个在运行时实际未达到的返回值,例如:

function F(X: NativeInt): NativeInt;
begin
  try
    Result := 1 div X;
  except
    on E: EDivByZero do
    begin
      Exception.RaiseOuterException({ECustom}Exception.Create('...'));
      Result := 0; // <-- just to keep the compiler happy
    end;
  end;
end;

【讨论】:

  • 当然有很多变种。例如,您还可以通过将某些内容分配给Result 来消除警告。您可以在 (!) RaiseOuterException 之后执行此操作。在OP的原始代码中:将Exception.RaiseOuterException({ECustom}Exception.Create('...'));替换为begin Exception.RaiseOuterException({ECustom}Exception.Create('...')); Exit(0) end;
【解决方案2】:

EChainedException 解决方案

(按照 Max 的要求)

更新 我已经在 Embarcadero 推出了一个 FR。如果您喜欢这个提议的解决方案,请投票。 RSP-31679

通过使用这个类,内部异常总是被记录为“好像”你调用了Exception.RaiseOuterException。这允许您使用简单的raise 语句,这样可以避免编译器发出警告消息。

用途

只需从EChainedException 而不是Exception 派生您的自定义异常,并使用raise 而不是Exception.RaiseOuterException

源代码

相关代码如下。我完整的EChainedException 比这更复杂一点,用于支持检测致命异常和堆栈跟踪等。如果它不能编译,请告诉我缺少什么,我会添加缺少的部分。

unit uChainedException;

interface

uses Sysutils;

{$M+} // ensures RTTI info is present for EChainedException

type
    EChainedException = class(Exception)
    protected
      procedure RaisingException(P: system.sysutils.PExceptionRecord); override;
    end;

implementation

uses rtti;

var // rtti pointers for handling the inner exception
  vInnerExceptionOffset: NativeInt = -1;
  vAcquireInnerExceptionOffset: NativeInt = -1;
  vRunningInIDEInitialized: Boolean;
  vRunningInIDE: Boolean;

function RunningInIDE:boolean;
begin
  if not vRunningInIDEInitialized then
  begin
    vRunningInIDE:=AnsiSameText(ExtractFileName(ParamStr(0)),'BDS.EXE');
    vRunningInIDEInitialized:=True;
  end;
  Result:=vRunningInIDE;
end;


procedure EChainedException.RaisingException(P: System.sysutils.PExceptionRecord);
var
  PBoolean: ^Boolean;
  PObject : ^TObject;
begin
  if (ExceptObject<>self) and (vAcquireInnerExceptionOffset >=0)  then
  begin
    PBoolean := Pointer(NativeInt(Self)+vAcquireInnerExceptionOffset);
    PBoolean^ := PBoolean^ or not RunningInIDE;
  end;
  inherited;
  // in some rare cases (like reraise exception from another thread)
  // it may happen that the innerexception points to self
  // this is corrected here.
  if InnerException=self then
  begin
    PObject := Pointer(NativeInt(Self)+vInnerExceptionOffset);
    PObject^ := nil;
  end;
end;

procedure UnprepAutoInnerException;
begin
  vInnerExceptionOffset:=-1;
  vAcquireInnerExceptionOffset:=-1;
end;

procedure PrepAutoInnerException;
var
  lRTTIContext: TRttiContext;
  lInnerException:TRttiField;
  lAcquireInnerException:TRttiField;
  lClass: TRttiInstanceType;
begin
  try
    lRTTIContext.Create;     //Notice vRTTIContext is a record, .Create initializes properties
    try
      lClass:=lRTTIContext.GetType(Exception) as TRttiInstanceType;
      lInnerException:=lClass.GetField('FInnerException');
      vInnerExceptionOffset := lInnerException.Offset;
      lAcquireInnerException:=lClass.GetField('FAcquireInnerException');
      vAcquireInnerExceptionOffset := lAcquireInnerException.Offset;
    except
      UnprepAutoInnerException;
      raise;
    end;
  finally
    lRTTIContext.Free;
  end;
end;

initialization
  PrepAutoInnerException;
finalization
  UnprepAutoInnerException;
end.

查看这段代码,我发现它可以使用一些现代化,例如使用类变量而不是全局变量,以及使用内联语言环境变量。 整个单元从 Delphi 回来 6 天,包含许多 $ifdef,因为它会超过答案而被忽略。

我仍然想知道为什么异常链接在 delphi/rad studio 中不像在其他语言中那样默认。很可能是因为它会以某种方式破坏现有代码。

【讨论】:

  • 非常感谢您的代码! +1 找出缺失的部分并不难,所以我自己将它们添加到你的答案中,这样它就可以编译了。您的回答使我转向另一种解决方案。见my answer。我希望里面也有适合你的东西!!
【解决方案3】:

我(也)回答我自己的问题,因为我将采取另一种方法。它规定了以下要求:

  • 我喜欢保留 raise 语句,就像它们最初的样子,
    • 所以这里不会有任何必要的代码更改,并且
    • 这也意味着不会有新引入的警告,如W1035W1036
  • 不过,我不想重建内部 RTL 机制
  • 我想尽可能少地干扰 RTL 机制。
  • 我希望灵活地控制链接异常
    • 有时强制或默认情况下,在异常实现方面,以及
    • 有时通过参数,在异常使用方面,以扩展功能。

在我的解决方案中:

  • 我接受突破 Exception 字段的可见性,特别是 FAcquireInnerException
  • 我依靠 RTTI 来验证字段的对齐方式(在 ExceptionFields 中,根据 Exception)。

这里我提供了一个简洁的复制粘贴实现:

EException的构造函数展示了ExceptionFields的使用:

ExceptionFields(Self).FAcquireInnerException := True;

-- 用于任何Exception 派生的异常,它会在引发异常时触发 RTL 机制来设置InnerException。此外,如果需要,EException 可以作为自定义异常类的公共根。重新引入了一些构造函数以使用 const AcquireInnerException: Boolean = True 进行扩展,以便将控制权移交给调用者,同时为所需的链接提供默认值。

运行ExceptionFields.VerifyFieldAlignments,如果你想验证对齐方式

  • ExceptionFields 中声明的(“re-”)外部可访问字段和
  • Exception 中的(私人)同行。

如果它无法验证这一点,它将引发异常。它在EException 的类构造函数中运行。如果您不使用EException,但想保留验证,请酌情移动它。

(精简)实现:

unit Exceptions;

interface

uses
  System.SysUtils;

type
  EException = class (Exception)
  public
    class constructor Create;
    constructor Create(const Msg: String; const AcquireInnerException: Boolean = True);
    constructor CreateFmt(const Msg: String; const Args: array of const; const AcquireInnerException: Boolean = True); overload;
    constructor CreateRes(const Msg: PResStringRec; const AcquireInnerException: Boolean = True);
    constructor CreateResFmt(const Msg: PResStringRec; const Args: array of const; const AcquireInnerException: Boolean = True); overload;
  end;

type
  ExceptionFields = class (TObject)
  {$Hints Off} // H2219
  strict private
    FMessage: String;
    FHelpContext: Integer;
    FInnerException: Exception;
    FStackInfo: Pointer;
  {$Hints On}

  public
    FAcquireInnerException: Boolean;

  private
    class procedure VerifyFieldAlignments;
  end;

implementation

uses
  System.Generics.Collections,
  System.RTTI,
  System.TypInfo;

{ ExceptionFields }

class procedure ExceptionFields.VerifyFieldAlignments;

  procedure RaiseTypeNotFound(const ClassName: String);
  begin
    raise Exception.CreateFmt(
      'Typ nicht gefunden: %s',
      [ClassName]
    );
  end;

  procedure RaiseFieldNotFound(const ClassName, FieldName: String);
  begin
    raise Exception.CreateFmt(
      'Feld nicht gefunden: %s.%s',
      [ClassName, FieldName]
    );
  end;

  procedure RaiseFieldNotAligned(const LeftClassName: String; const LeftField: TPair<String, Integer>; const RightClassName: String; const RightField: TRTTIField);
  begin
    raise Exception.CreateFmt(
      'Feld nicht ausgerichtet: %s.%s+%d (tatsächlich) vs. %s.%s+%d (erwartet)',
      [
        LeftClassName,
        LeftField.Key,
        LeftField.Value,
        RightClassName,
        RightField.Name,
        RightField.Offset
      ]
    );
  end;

  type
    TMemberVisibilities = set of TMemberVisibility;

  function GetDeclaredFields(const RTTIContext: TRTTIContext; const &Class: TClass; const IncludedVisibilities: TMemberVisibilities = [mvPublic, mvPublished]): TArray<TPair<String, Integer>>;
  var
    RTTIType: TRTTIType;
    RTTIFields: TArray<TRTTIField>;
    Index: NativeInt;
    RTTIField: TRTTIField;
  begin
    RTTIType := RTTIContext.GetType(&Class);
    if not Assigned(RTTIType) then
      RaiseTypeNotFound(&Class.ClassName);
    RTTIFields := RTTIType.GetDeclaredFields;
    SetLength(Result, Length(RTTIFields));
    Index := 0;
    for RTTIField in RTTIFields do
      if RTTIField.Visibility in IncludedVisibilities then
      begin
        Result[Index] := TPair<String, Integer>.Create(
          RTTIField.Name,
          RTTIField.Offset
        );
        Inc(Index);
      end;
    SetLength(Result, Index);
  end;

const
  Left: TClass = ExceptionFields;
  Right: TClass = Exception;
var
  RTTIContext: TRTTIContext;
  DeclaredFields: TArray<TPair<String, Integer>>;
  RTTIType: TRTTIType;
  DeclaredField: TPair<String, Integer>;
  RTTIField: TRTTIField;
begin
  RTTIContext := TRTTIContext.Create;
  try
    DeclaredFields := GetDeclaredFields(RTTIContext, Left);
    RTTIType := RTTIContext.GetType(Right);
    if not Assigned(RTTIType) then
      RaiseTypeNotFound(Right.ClassName);
    for DeclaredField in DeclaredFields do
    begin
      RTTIField := RTTIType.GetField(DeclaredField.Key);
      if not Assigned(RTTIField) then
        RaiseFieldNotFound(Right.ClassName, DeclaredField.Key);
      if DeclaredField.Value <> RTTIField.Offset then
        RaiseFieldNotAligned(
          Left.ClassName, DeclaredField,
          RTTIType.Name, RTTIField
        );
    end;
  finally
    RTTIContext.Free;
  end;
end;

{ EException }

class constructor EException.Create;
begin
  inherited;
  ExceptionFields.VerifyFieldAlignments;
end;

constructor EException.Create(const Msg: String;
  const AcquireInnerException: Boolean);
begin
  inherited Create(Msg);
  ExceptionFields(Self).FAcquireInnerException := AcquireInnerException;
end;

constructor EException.CreateFmt(const Msg: String;
  const Args: array of const;
  const AcquireInnerException: Boolean);
begin
  inherited CreateFmt(Msg, Args);
  ExceptionFields(Self).FAcquireInnerException := AcquireInnerException;
end;

constructor EException.CreateRes(const Msg: PResStringRec;
  const AcquireInnerException: Boolean);
begin
  inherited CreateRes(Msg);
  ExceptionFields(Self).FAcquireInnerException := AcquireInnerException;
end;

constructor EException.CreateResFmt(const Msg: PResStringRec;
  const Args: array of const;
  const AcquireInnerException: Boolean);
begin
  inherited CreateResFmt(Msg, Args);
  ExceptionFields(Self).FAcquireInnerException := AcquireInnerException;
end;

end.

还有一个演示:

program ExceptionsDemo;

{$AppType Console}

{$R *.res}

uses
  System.SysUtils,
  Exceptions in 'Exceptions.pas';

type
  EDemoException = class (EException)
  end;

begin
  try
    try
      try
        raise EZeroDivide.Create('Level 3');
      except
        raise EException.Create('Level 2', False);
      end;
    except
      raise EDemoException.Create('Level 1');
    end;
  except
    on E: Exception do
    begin
      WriteLn(E.ClassName, ': ', E.Message);
      while Assigned(E.InnerException) do
      begin
        E := E.InnerException;
        WriteLn(E.ClassName, ': ', E.Message);
      end;
    end;
  end;
  ReadLn;
end.

输出 -- 最后一行只存在于raise EException.Create('Level 2', True):

EDemoException: Level 1
EException: Level 2
EZeroDivide: Level 3

感谢所有回复者!

【讨论】:

  • 这是绕过 RTTI 解决方案的有趣解决方案。由于性能,我主要使用指针(而不是 TrttiField.SetValue)进行轻量级设计。另外,像您一样,我会立即知道 Emb 是否决定 change.rename FInnerException 因为 RTTI 将无法找到它们。想一想:您是否考虑过使用辅助类来做这些骇人听闻的事情?
  • 无论如何,请务必查看quality.embarcadero.com/browse/RSP-31679
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-05-03
相关资源
最近更新 更多