【问题标题】:Error: "Cannot modify the return value" c#错误:“无法修改返回值”c#
【发布时间】:2010-12-17 09:17:23
【问题描述】:

我正在使用自动实现的属性。 我想解决以下问题的最快方法是声明我自己的支持变量?

public Point Origin { get; set; }

Origin.X = 10; // fails with CS1612

错误消息:无法修改“表达式”的返回值,因为 它不是变量

试图修改一个值类型,该值类型是 中间表达。因为值没有持久化,所以值 将保持不变。

要解决此错误,请将表达式的结果存储在 中间值,或使用中间值的引用类型 表达。

【问题讨论】:

  • 这再次说明了为什么可变值类型是一个坏主意。如果您可以避免改变值类型,请这样做。
  • 获取以下代码(来自我在某个 EL 博客上的 AStar 实现的努力:-),这无法避免更改值类型: class Path : IEnumerable where T : INode, new() {...} public HexNode(int x, int y) : this(new Point(x, y)) {} Path path = new Path(new T(x, y)); // 错误 // 丑陋的修复 Path path = new Path(new T()); path.LastStep.Centre = new Point(x, y);

标签: c# variables struct immutability


【解决方案1】:

这是因为Point 是一个值类型(struct)。

因此,当您访问 Origin 属性时,您访问的是类所持有值的副本,而不是引用类型 (@987654325) 中的值本身@),因此如果您在其上设置了 X 属性,那么您将在副本上设置该属性,然后将其丢弃,保持原始值不变。这可能不是您想要的,这就是编译器警告您的原因。

如果您只想更改 X 值,则需要执行以下操作:

Origin = new Point(10, Origin.Y);

【讨论】:

  • @Paul:你有能力把结构改成类吗?
  • 这有点令人失望,因为我分配的属性设置器有副作用(结构充当支持引用类型的视图)
  • 另一个解决方案是简单地将你的结构变成一个类。与 C++ 中的类和结构仅通过默认成员访问(分别为私有和公共)不同,C# 中的结构和类有更多不同之处。这里有更多信息:docs.microsoft.com/en-us/dotnet/csharp/programming-guide/…
【解决方案2】:

使用支持变量没有帮助。Point 类型是值类型。

您需要将整个 Point 值分配给 Origin 属性:-

Origin = new Point(10, Origin.Y);

问题是当您访问 Origin 属性时,get 返回的是 Origin 属性自动创建字段中 Point 结构的副本。因此,您对该副本的 X 字段的修改不会影响基础字段。编译器检测到这一点并给你一个错误,因为这个操作完全没用。

即使您使用自己的支持变量,您的 get 也会如下所示:-

get { return myOrigin; }

您仍然会返回 Point 结构的副本,并且会收到相同的错误。

嗯...更仔细地阅读了您的问题,也许您实际上的意思是直接从您的类中修改支持变量:-

myOrigin.X = 10;

是的,这正是您所需要的。

【讨论】:

    【解决方案3】:

    现在您已经知道错误的根源是什么。如果构造函数不存在并带有重载来获取您的属性(在本例中为X),您可以使用对象初始化器(它将在幕后完成所有魔术)。 并不是说你不需要让你的结构不可变,而只是提供额外的信息:

    struct Point
    {
        public int X { get; set; }
        public int Y { get; set; }
    }
    
    class MyClass
    {
        public Point Origin { get; set; }
    }
    
    MyClass c = new MyClass();
    c.Origin.X = 23; //fails.
    
    //but you could do:
    c.Origin = new Point { X = 23, Y = c.Origin.Y }; //though you are invoking default constructor
    
    //instead of
    c.Origin = new Point(23, c.Origin.Y); //in case there is no constructor like this.
    

    这是可能的,因为这是在幕后发生的:

    Point tmp = new Point();
    tmp.X = 23;
    tmp.Y = Origin.Y;
    c.Origin = tmp;
    

    这看起来很奇怪,完全不推荐。只是列出另一种方式。更好的方法是使结构不可变并提供适当的构造函数。

    【讨论】:

    • 这不会破坏Origin.Y 的价值吗?给定Point 类型的属性,我认为更改X 的惯用方式是var temp=thing.Origin; temp.X = 23; thing.Origin = temp;。惯用方法的优点是它不必提及它不想修改的成员,这个特性只有在Point 是可变的时候才有可能。我对这样的哲学感到困惑,因为编译器不能允许Origin.X = 23;,所以应该设计一个结构来要求像Origin.X = new Point(23, Origin.Y); 这样的代码。后者对我来说似乎真的很恶心。
    • @supercat 这是我第一次想到你的观点,很有道理!你有替代的模式/设计理念来解决这个问题吗?如果 C# 默认不为结构提供默认构造函数,那会更容易(在这种情况下,我必须将 XY 都传递给特定的构造函数)。现在它失去了可以做Point p = new Point()的意义。我知道为什么它真的需要一个结构,所以没有必要考虑它。但是你有一个很酷的想法来更新一个像X这样的属性吗?
    • 对于封装了一组独立但相关的变量(例如点的坐标)的结构,我的偏好是简单地让结构将其所有成员公开为公共字段;要修改结构属性的一个成员,只需将其读出,修改该成员,然后将其写回。如果 C# 提供了一个“简单的 Plain-Old-Data-Struct”声明,该声明会自动定义一个参数列表与字段列表匹配的构造函数,那就太好了,但是负责 C# 的人鄙视可变结构。
    • @supercat 我明白了。结构和类的不一致行为令人困惑。
    • 这种混乱源于恕我直言无益的信念,即一切都应该表现得像一个类对象。虽然将值类型值传递给期望堆对象引用的事物很有用,但假装值类型变量包含从Object 派生的事物是没有用的。他们没有。每个值类型定义实际上定义了两种东西:存储位置类型(用于变量、数组槽等)和堆对象类型,有时称为“装箱”类型(用于值类型值存储到引用类型的位置)。
    【解决方案4】:

    除了辩论结构与类的优缺点之外,我倾向于从这个角度看待目标并解决问题。

    话虽如此,如果您不需要在属性 get 和 set 方法后面编写代码(如您的示例中所示),那么简单地将 Origin 声明为类的字段不是更容易吗?比财产?我应该认为这会让你实现你的目标。

    struct Point
    {
        public int X { get; set; }
        public int Y { get; set; }
    }
    
    class MyClass
    {
        public Point Origin;
    }
    
    MyClass c = new MyClass();
    c.Origin.X = 23;   // No error.  Sets X just fine
    

    【讨论】:

    • 不,仅供参考。这也行不通
    • 你错了。我在上面发布的示例在我在此处发布的 dotnetfiddle.net/ajdvII 演示的 .Net Fiddle 示例中运行良好。
    【解决方案5】:

    我认为很多人在这里感到困惑,这个特殊问题与理解值类型属性返回值类型的副本(与方法和索引器一样)有关,和值类型字段直接访问。以下代码通过直接访问属性的支持字段来完成您尝试实现的目标(注意:使用支持字段以详细形式表示属性相当于自动属性,但具有在我们的代码中我们可以直接访问支持字段):

    class Program
    {
        static void Main(string[] args)
        {
            var myClass = new MyClass();
            myClass.SetOrigin();
            Debug.Assert(myClass.Origin.X == 10); //succeeds
        }
    }
    
    class MyClass
    {
        private Point _origin;
        public Point Origin
        { 
            get => _origin; 
            set => _origin = value; 
        }
    
        public void SetOrigin()
        {
            _origin.X = 10; //this works
            //Origin.X = 10; // fails with CS1612;
        }
    }
    

    您遇到的错误是不了解属性返回值类型副本的间接后果。如果您返回一个值类型的副本并且您没有将其分配给局部变量,那么您对该副本所做的任何更改都无法读取,因此编译器将此作为错误提出,因为这不是故意的。如果我们将副本分配给局部变量,那么我们可以更改 X 的值,但它只会在本地副本上更改,这修复了编译时错误,但不会达到修改 Origin 属性的预期效果。下面的代码说明了这一点,因为编译错误消失了,但调试断言会失败:

    class Program
    {
        static void Main(string[] args)
        {
            var myClass = new MyClass();
            myClass.SetOrigin();
            Debug.Assert(myClass.Origin.X == 10); //throws error
        }
    }
    
    class MyClass
    {
        private Point _origin;
        public Point Origin
        { 
            get => _origin; 
            set => _origin = value; 
        }
    
        public void SetOrigin()
        {
            var origin = Origin;
            origin.X = 10; //this is only changing the value of the local copy
        }
    }
    

    【讨论】:

      【解决方案6】:

      问题是您指向位于堆栈上的值,并且该值不会被重新反射回原始属性,因此 C# 不允许您返回对值类型的引用。我认为您可以通过删除 Origin 属性来解决这个问题,而是使用公共文件,是的,我知道这不是一个好的解决方案。另一种解决方案是不使用 Point,而是创建自己的 Point 类型作为对象。

      【讨论】:

      • 如果Point 是引用类型的成员,那么它将不在堆栈上,而是在包含对象内存的堆上。
      【解决方案7】:

      我想这里的问题是您试图在语句中分配对象的子值,而不是分配对象本身。在这种情况下,您需要分配整个 Point 对象,因为属性类型是 Point。

      Point newOrigin = new Point(10, 10);
      Origin = newOrigin;
      

      希望我说得有道理

      【讨论】:

      • 重点是Point是一个struct(valuetype)。如果它是一个类(对象),那么原始代码就可以工作。
      • @HansKesting:如果Point 是可变类类型,则原始代码将在属性Origin 返回的对象中设置字段或属性X。我认为没有理由相信这会对包含 Origin 属性的对象产生预期的效果。一些框架类具有将其状态复制到新的可变类实例并返回它们的属性。这样的设计的优点是允许像thing1.Origin = thing2.Origin;这样的代码设置对象的原点状态以匹配另一个的状态,但它不能警告像thing1.Origin.X += 4;这样的代码。
      【解决方案8】:

      只需按如下方式删除“get set”属性,然后一切正常。

      如果是原始类型,请使用 get;set;...

      using Microsoft.Xna.Framework;
      using System;
      
      namespace DL
      {
          [Serializable()]
          public class CameraProperty
          {
              #region [READONLY PROPERTIES]
              public static readonly string CameraPropertyVersion = "v1.00";
              #endregion [READONLY PROPERTIES]
      
      
              /// <summary>
              /// CONSTRUCTOR
              /// </summary>
              public CameraProperty() {
                  // INIT
                  Scrolling               = 0f;
                  CameraPos               = new Vector2(0f, 0f);
              }
              #region [PROPERTIES]   
      
              /// <summary>
              /// Scrolling
              /// </summary>
              public float Scrolling { get; set; }
      
              /// <summary>
              /// Position of the camera
              /// </summary>
              public Vector2 CameraPos;
              // instead of: public Vector2 CameraPos { get; set; }
      
              #endregion [PROPERTIES]
      
          }
      }      
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2023-04-02
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-03-01
        • 2012-01-20
        • 1970-01-01
        • 2015-12-28
        相关资源
        最近更新 更多