错误:"无法修改返回值" c#

189

我正在使用自动实现属性。 我猜修复以下代码最快的方法是声明自己的支持变量?

public Point Origin { get; set; }

Origin.X = 10; // fails with CS1612

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

试图修改中间表达式的值类型。由于该值没有被持久化,所以该值将不会改变。

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


14
这再次证明了可变值类型为什么是一个不好的想法。如果你可以避免改变一个值类型,就避免这么做吧。 - Eric Lippert
请看以下代码(来自我在某个 EL 的 AStar 实现博客中的努力),它无法避免更改值类型: class Path<T> : IEnumerable<T> where T : INode, new() {...} public HexNode(int x, int y) : this(new Point(x, y)) {} Path<T> path = new Path<T>(new T(x, y)); // 错误 // 丑陋的修复 Path<T> path = new Path<T>(new T()); path.LastStep.Centre = new Point(x, y); - Tom Wilson
8个回答

236

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

由于这一点,当访问Origin属性时,您访问的是类所持有的值的副本,而不是像引用类型(class)那样直接访问该值本身。因此,如果您在副本上设置X属性,那么您就是在副本上设置该属性,然后丢弃它,从而保留原始值不变。这可能不是您想要的,这也是编译器警告您的原因。

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

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

2
@Paul:你有把结构体改为类的能力吗? - Doug
2
这有点令人沮丧,因为我正在分配的属性设置器具有副作用(该结构体充当对后备引用类型的视图)。 - Alexander
另一个解决方案是将您的结构体简单地转换为类。与C++不同,其中类和结构体仅通过默认成员访问(分别为private和public)进行区分,C#中的结构体和类有更多的差异。这里有更多信息:https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/ - Artorias2718

11

使用支撑变量是无用的。 Point类型是一个值类型。

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

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

问题在于当你访问Origin属性时,get返回的是Origin属性自动创建字段中Point结构的副本。因此,对这个副本的X字段进行修改不会影响底层字段。编译器检测到这一点并给出错误提示,因为这个操作完全没有意义。

即使你使用了自己的后备变量,你的get看起来也像:

get { return myOrigin; }

你仍然会返回 Point 结构的副本,而且会得到相同的错误。

嗯... 经过仔细阅读您的问题,也许您实际上是想直接从类内部修改后备变量:

myOrigin.X = 10;

是的,那就是你需要的。


7
到目前为止,您已经知道错误的来源。如果构造函数不存在重载以接受您的属性(在本例中为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;

这看起来是一件非常奇怪的事情,并不推荐这样做。只是列出了一种替代方式。更好的做法是使结构成为不可变的,并提供一个适当的构造函数。

2
这难道不会破坏 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
@supercat 这是我第一次考虑到你的观点,非常有道理! 你有没有其他的模式/设计想法来解决这个问题?如果 C# 没有默认提供结构体的默认构造函数,那么这将更容易(在这种情况下,我必须严格传递 XY 给特定的构造函数)。现在当一个人可以使用 Point p = new Point() 时,它就失去了意义。我知道为什么结构体真正需要它,所以不用考虑这个问题。但是你有没有一个很酷的想法来更新只有一个属性,比如 X - nawfal
@supercat 我明白了,结构体和类的不一致行为很令人困惑。 - nawfal
如果有人说 object o = 5;,那么 o 将持有一个指向堆对象类型 System.Int32 的实例的引用,该类型派生自 System.Object。然而,如果有人改为说 int i = 5;,那么 i 将持有位模式 0000...00000101。如果将值类型视为存在于与堆对象类型不同的自己的“宇宙”中,则它们的行为就会有意义。但是,如果假装它们是 System.Object 的派生类,那么就不会有意义。我建议采用与实际行为相匹配的世界观。 - supercat
@supercat 我理解两者的概念差异,但再次感谢您的解释。我最初的评论只是一个思考而不是质疑。 - nawfal
显示剩余2条评论

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
    }
}

2
除了辩论结构体和类的优缺点之外,我倾向于从目标出发并从那个角度解决问题。
话虽如此,如果你不需要在属性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

不,这只是提供信息。这并不起作用。 - rimes
你的想法是错误的。我在这里发布了一个示例,它在我发布的 .Net Fiddle 示例中运行良好,链接为 https://dotnetfiddle.net/ajdvII。 - Mitselplik

0

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

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

希望我表达得清楚


2
重要的一点是,Point 是一个结构体(值类型)。如果它是一个类(对象),那么原始代码就可以工作。 - Hans Kesting
@HansKesting: 如果Point是一个可变的类类型,原始代码将会设置由属性Origin返回的对象中的字段或属性X。我看不出有什么理由相信这会对包含Origin属性的对象产生期望的影响。一些框架类具有属性,它们将它们的状态复制到新的可变类实例并返回。这样的设计有一个优点,就是允许像thing1.Origin = thing2.Origin;这样的代码设置对象原点的状态以匹配另一个对象的状态,但它不能警告像thing1.Origin.X += 4;这样的代码。 - supercat

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

1
如果 Point 是引用类型的成员,那么它将不会在堆栈上,而是在包含对象内存的堆上。 - Greg Beech

-1

只需按照以下方式删除“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]

    }
}      

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接