【问题标题】:"Left side cannot be assigned to" for record type properties in DelphiDelphi中记录类型属性的“左侧不能分配给”
【发布时间】:2010-10-11 20:29:48
【问题描述】:

我很想知道为什么 Delphi 将记录类型属性视为只读:

  TRec = record
    A : integer;
    B : string;
  end;

  TForm1 = class(TForm)
  private
    FRec : TRec;
  public
    procedure DoSomething(ARec: TRec);
    property Rec : TRec read FRec write FRec;
  end;

如果我尝试为 Rec 属性的任何成员赋值,我将收到“无法将左侧赋值给”错误:

procedure TForm1.DoSomething(ARec: TRec);
begin
  Rec.A := ARec.A;
end;

允许对基础字段执行相同操作:

procedure TForm1.DoSomething(ARec: TRec);
begin
  FRec.A := ARec.A;
end;

对这种行为有什么解释吗?

【问题讨论】:

    标签: delphi record


    【解决方案1】:

    由于“Rec”是一个属性,编译器对它的处理略有不同,因为它必须首先评估属性 decl 的“读取”。考虑一下,这在语义上等同于您的示例:

    ...
    property Rec: TRec read GetRec write FRec;
    ...
    

    如果你这样看,你可以看到对“Rec”的第一个引用(在点'.'之前)必须调用 GetRec,这将创建 Rec 的临时本地副本。这些临时对象在设计上是“只读的”。这就是你遇到的问题。

    您可以在这里做的另一件事是将记录的各个字段分解为包含类的属性:

    ...
    property RecField: Integer read FRec.A write FRec.A;
    ...
    

    这将允许您通过属性直接分配给类实例中嵌入记录的字段。

    【讨论】:

    • 那么,你的意思是“property Rec : TRec read FRec write FRec”有一个“秘密”的getter和setter?
    【解决方案2】:

    因为你有隐式的 getter 和 setter 函数,你不能修改函数的 Result,因为它是一个 const 参数。

    (注意:如果你转换对象中的记录,结果实际上是一个指针,因此相当于一个 var 参数。

    如果要保留记录,则必须使用中间变量(或字段变量)或使用 WITH 语句。

    使用显式的 getter 和 setter 函数查看以下代码中的不同行为:

    type
      TRec = record
        A: Integer;
        B: string;
      end;
    
      TForm2 = class(TForm)
      private
        FRec : TRec;
        FRec2: TRec;
        procedure SetRec2(const Value: TRec);
        function GetRec2: TRec;
      public
        procedure DoSomething(ARec: TRec);
        property Rec: TRec read FRec write FRec;
        property Rec2: TRec  read GetRec2 write SetRec2;
      end;
    
    var
      Form2: TForm2;
    
    implementation
    
    {$R *.dfm}
    
    { TForm2 }
    
    procedure TForm2.DoSomething(ARec: TRec);
    var
      LocalRec: TRec;
    begin
      // copy in a local variable
      LocalRec := Rec2;
      LocalRec.A := Arec.A; // works
    
      // try to modify the Result of a function (a const) => NOT ALLOWED
      Rec2.A := Arec.A; // compiler refused!
    
      with Rec do
        A := ARec.A; // works with original property and with!
    end;
    
    function TForm2.GetRec2: TRec;
    begin
      Result:=FRec2;
    end;
    
    procedure TForm2.SetRec2(const Value: TRec);
    begin
      FRec2 := Value;
    end;
    

    【讨论】:

      【解决方案3】:

      是的,这是个问题。但是这个问题可以使用记录属性来解决:

      type
        TRec = record
        private
          FA : integer;
          FB : string;
          procedure SetA(const Value: Integer);
          procedure SetB(const Value: string);
        public
          property A: Integer read FA write SetA;
          property B: string read FB write SetB;
        end;
      
      procedure TRec.SetA(const Value: Integer);
      begin
        FA := Value;
      end;
      
      procedure TRec.SetB(const Value: string);
      begin
        FB := Value;
      end;
      
      TForm1 = class(TForm)
        Button1: TButton;
        procedure Button1Click(Sender: TObject);
      private
        FRec : TRec;
      public
        property Rec : TRec read FRec write FRec;
      end;
      
      procedure TForm1.Button1Click(Sender: TObject);
      begin
        Rec.A := 21;
        Rec.B := 'Hi';
      end;
      

      这编译和工作没有问题。

      【讨论】:

      • +1 请注意,您的解决方案还不错,但它的用户需要记住,如果他们将属性更改为“property Rec : TRec read GetRec write FRec;”,赋值技巧将惨遭失败(因为 GetRec 将返回 copy,因为记录是 值类型)。
      • TForm1 中的 Rec 属性只有在只需要对记录的属性进行读/写访问时才能读取。此解决方案的关键部分是记录属性中的设置方法。
      【解决方案4】:

      编译器正在阻止您分配给临时文件。 C# 中的等价物是允许的,但没有效果; Rec 属性的返回值是底层字段的副本,分配给副本上的字段是 nop。

      【讨论】:

        【解决方案5】:

        正如其他人所说 - 读取属性将返回记录的副本,因此字段的分配不会作用于 TForm1 拥有的副本。

        另一个选项是这样的:

          TRec = record
            A : integer;
            B : string;
          end;
          PRec = ^TRec;
        
          TForm1 = class(TForm)
          private
            FRec : PRec;
          public
            constructor Create;
            destructor Destroy; override;
        
            procedure DoSomething(ARec: TRec);
            property Rec : PRec read FRec; 
          end;
        
        constructor TForm1.Create;
        begin
          inherited;
          FRec := AllocMem(sizeof(TRec));
        end;
        
        destructor TForm1.Destroy;
        begin
          FreeMem(FRec);
        
          inherited;
        end;
        

        Delphi 将为您取消引用 PRec 指针,所以这样的事情仍然有效:

        Form1.Rec.A := 1234; 
        

        不需要属性的写入部分,除非您想交换 FRec 指向的 PRec 缓冲区。我真的不建议通过属性进行这种交换。

        【讨论】:

          【解决方案6】:

          最简单的方法是:

          procedure TForm1.DoSomething(ARec: TRec);
          begin
            with Rec do
              A := ARec.A;
          end;
          

          【讨论】:

          • 我认为你是对的 - 使用记录的属性没有意义,这似乎需要做很多工作......只需有一个对记录执行某些操作的过程:SetSomething(var ARec: TRec )
          【解决方案7】:

          这是因为属性实际上是作为函数编译的。属性只返回或设置一个值。它不是对记录的引用或指针

          所以:

          Testing.TestRecord.I := 10;  // error
          

          和调用这样的函数是一样的:

          Testing.getTestRecord().I := 10;   //error (i think)
          

          你可以做的是:

          r := Testing.TestRecord;    // read
          r.I := 10;
          Testing.TestRecord := r;    //write
          

          它有点凌乱,但在这种类型的架构中是固有的。

          【讨论】:

            【解决方案8】:

            我经常使用的一个解决方案是将属性声明为指向记录的指针。

            type
              PRec = ^TRec;
              TRec = record
                A : integer;
                B : string;
              end;
            
              TForm1 = class(TForm)
              private
                FRec : TRec;
            
                function GetRec: PRec;
                procedure SetRec(Value: PRec);
              public
                property Rec : PRec read GetRec write SetRec; 
              end;
            
            implementation
            
            function TForm1.GetRec: PRec;
            begin
              Result := @FRec;
            end;  
            
            procedure TForm1.SetRec(Value: PRec);
            begin
              FRec := Value^;
            end;
            

            这样,直接分配Form1.Rec.A := MyInteger 就可以了,而且Form1.Rec := MyRec 也可以通过将MyRec 中的所有值复制到FRec 字段来正常工作。

            这里唯一的陷阱是,当您希望实际检索记录的副本以使用时,您将不得不使用类似MyRec := Form1.Rec^

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-05-08
              • 1970-01-01
              • 2021-06-22
              • 1970-01-01
              • 1970-01-01
              • 2019-09-09
              相关资源
              最近更新 更多