Lỗi: Có thể sửa đổi giá trị trả về


155

Tôi đang sử dụng các thuộc tính tự động triển khai. Tôi đoán cách nhanh nhất để khắc phục sau đây là khai báo biến sao lưu của riêng tôi?

public Point Origin { get; set; }

Origin.X = 10; // fails with CS1612

Thông báo lỗi: Không thể sửa đổi giá trị trả về của 'biểu thức' vì đó không phải là biến

Một nỗ lực đã được thực hiện để sửa đổi một loại giá trị là kết quả của một biểu thức trung gian. Bởi vì giá trị không được duy trì, giá trị sẽ không thay đổi.

Để khắc phục lỗi này, lưu trữ kết quả của biểu thức trong một giá trị trung gian hoặc sử dụng loại tham chiếu cho biểu thức trung gian.


13
Đây là một minh họa khác về lý do tại sao các loại giá trị có thể thay đổi là một ý tưởng tồi. Nếu bạn có thể tránh làm biến đổi một loại giá trị, hãy làm như vậy.
Eric Lippert

Lấy mã sau đây (từ những nỗ lực của tôi khi triển khai AStar được viết bởi một EL nhất định :-), không thể tránh việc thay đổi loại giá trị: Đường dẫn lớp <T>: IEnumerable <T> trong đó T: INode, new () {. ..} công khai HexNode (int x, int y): this (new Point (x, y)) {} Đường dẫn <T> path = new Path <T> (new T (x, y)); // Lỗi // Đường dẫn sửa lỗi xấu <T> path = new Path <T> (new T ()); path.LastStep.Centre = new Point (x, y);
Tom Wilson

Câu trả lời:


197

Điều này là do Pointmột loại giá trị ( struct).

Bởi vì điều này, khi bạn truy cập vào thuộc Origintính bạn đang truy cập một bản sao của giá trị được giữ bởi lớp, chứ không phải giá trị như với loại tham chiếu ( class), vì vậy nếu bạn đặt thuộc Xtính trên đó thì bạn sẽ đặt tài sản trên bản sao và sau đó loại bỏ nó, giữ nguyên giá trị ban đầu. Đây có lẽ không phải là những gì bạn dự định, đó là lý do tại sao trình biên dịch cảnh báo bạn về nó.

Nếu bạn muốn thay đổi chỉ Xgiá trị, bạn cần làm một cái gì đó như thế này:

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

2
@Paul: Bạn có khả năng thay đổi cấu trúc thành một lớp không?
Doug

1
Đây là một kiểu người lập dị, bởi vì trình thiết lập thuộc tính mà tôi gán có tác dụng phụ (cấu trúc đóng vai trò là kiểu xem tham chiếu ủng hộ)
Alexander - Tái lập Monica

Một giải pháp khác là chỉ cần làm cho cấu trúc của bạn thành một lớp. Không giống như trong C ++, trong đó một lớp và một cấu trúc chỉ khác nhau bởi quyền truy cập thành viên mặc định (tương ứng là riêng tư và công khai), các cấu trúc và các lớp trong C # có một vài khác biệt nữa. Dưới đây là một số thông tin khác: docs.microsoft.com/en-us/dotnet/csharp/programming-guide/ chủ
Artorias2718

9

Sử dụng một biến sao lưu sẽ không giúp đỡ. Các Pointtype là một kiểu giá trị gia tăng.

Bạn cần gán toàn bộ giá trị Điểm cho thuộc tính Xuất xứ: -

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

Vấn đề là khi bạn truy cập vào thuộc tính Origin, cái được trả về getlà một bản sao của cấu trúc Điểm trong trường tự động tạo thuộc tính Origin. Do đó, việc sửa đổi trường X của bạn bản sao này sẽ không ảnh hưởng đến trường bên dưới. Trình biên dịch phát hiện ra điều này và cung cấp cho bạn một lỗi vì thao tác này hoàn toàn vô dụng.

Ngay cả khi bạn đã sử dụng biến sao lưu của riêng mình, bạn getsẽ trông như sau: -

get { return myOrigin; }

Bạn vẫn sẽ trả lại một bản sao của cấu trúc Điểm và bạn sẽ gặp lỗi tương tự.

Hmm ... đã đọc câu hỏi của bạn cẩn thận hơn có lẽ bạn thực sự muốn sửa đổi biến sao lưu trực tiếp từ trong lớp của bạn: -

myOrigin.X = 10;

Vâng, đó sẽ là những gì bạn cần.


6

Đến bây giờ bạn đã biết nguồn gốc của lỗi là gì. Trong trường hợp một nhà xây dựng không tồn tại quá tải để lấy tài sản của bạn (trong trường hợp này X), bạn có thể sử dụng trình khởi tạo đối tượng (sẽ thực hiện tất cả các phép thuật đằng sau hậu trường). Không phải là bạn không cần phải biến cấu trúc của mình thành bất biến , mà chỉ cần cung cấp thêm thông tin:

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.

Điều này là có thể bởi vì đằng sau hậu trường điều này xảy ra:

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

Điều này có vẻ như là một điều rất kỳ lạ để làm, không được khuyến khích. Chỉ cần liệt kê một cách thay thế. Cách tốt hơn để làm là làm cho cấu trúc bất biến và cung cấp một hàm tạo thích hợp.


2
Sẽ không rác ra giá trị của Origin.Y? Với một thuộc tính loại Point, tôi sẽ nghĩ rằng cách thành ngữ để thay đổi Xsẽ là var temp=thing.Origin; temp.X = 23; thing.Origin = temp;. Cách tiếp cận thành ngữ có ưu điểm là không cần phải đề cập đến các thành viên mà nó không muốn sửa đổi, một tính năng chỉ có thể có vì Pointcó thể thay đổi. Tôi bối rối với triết lý nói rằng vì trình biên dịch không thể cho phép Origin.X = 23;người ta thiết kế một cấu trúc để yêu cầu mã như thế nào Origin.X = new Point(23, Origin.Y);. Cái sau có vẻ thực sự khó chịu với tôi.
supercat

@supercat đây là lần đầu tiên tôi nghĩ về quan điểm của bạn, rất có ý nghĩa! Bạn có một ý tưởng mẫu / thiết kế thay thế để giải quyết điều này? Sẽ dễ dàng hơn nếu C # không cung cấp hàm tạo mặc định cho cấu trúc theo mặc định (trong trường hợp đó tôi hoàn toàn phải chuyển cả XYcho hàm tạo cụ thể). Bây giờ nó mất điểm khi người ta có thể làm Point p = new Point(). Tôi biết tại sao nó thực sự cần thiết cho một cấu trúc, vì vậy không có điểm nào trong suy nghĩ abt nó. Nhưng bạn có một ý tưởng tuyệt vời để cập nhật chỉ một tài sản như thế Xnào?
nawfal

Đối với các cấu trúc đóng gói một tập hợp các biến độc lập nhưng có liên quan (chẳng hạn như tọa độ của một điểm), sở thích của tôi chỉ đơn giản là cấu trúc hiển thị tất cả các thành viên của nó dưới dạng các trường công khai; để sửa đổi một thành viên của thuộc tính struct, chỉ cần đọc nó ra, sửa đổi thành viên đó và viết lại. Sẽ thật tuyệt nếu C # đã cung cấp một khai báo "Cấu trúc dữ liệu đơn giản cũ đơn giản" sẽ tự động xác định một hàm tạo có danh sách tham số khớp với danh sách trường, nhưng những người chịu trách nhiệm về các cấu trúc có thể thay đổi của C #.
supercat

@supercat Tôi hiểu rồi. Các hành vi không nhất quán của các cấu trúc và các lớp là khó hiểu.
nawfal

Sự nhầm lẫn xuất phát từ niềm tin vô ích của IMHO rằng mọi thứ nên hành xử như một đối tượng của lớp. Mặc dù rất hữu ích khi có một phương tiện truyền các giá trị loại giá trị cho những thứ mong đợi các tham chiếu đối tượng heap, nhưng sẽ không hữu ích khi giả vờ các biến loại giá trị giữ những thứ xuất phát từ đó Object. Họ không. Mỗi định nghĩa loại giá trị thực sự xác định hai loại: loại vị trí lưu trữ (được sử dụng cho biến, vị trí mảng, v.v.) và loại đối tượng heap, đôi khi được gọi là loại "đóng hộp" (được sử dụng khi giá trị loại giá trị được lưu trữ đến một vị trí kiểu tham chiếu).
supercat

2

Ngoài việc tranh luận về ưu và nhược điểm của các cấu trúc so với các lớp, tôi có xu hướng nhìn vào mục tiêu và tiếp cận vấn đề từ quan điểm đó.

Điều đó đang được nói, nếu bạn không cần phải viết mã phía sau thuộc tính get và set phương thức (như trong ví dụ của bạn), thì việc khai báo Originnhư là một trường của lớp chứ không phải là một thuộc tính? Tôi nên nghĩ rằng điều này sẽ cho phép bạn hoàn thành mục tiêu của mình.

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

0

Vấn đề là bạn trỏ đến một giá trị nằm trên ngăn xếp và giá trị sẽ không được chuyển trở lại thuộc tính gốc để C # không cho phép bạn trả về tham chiếu cho loại giá trị. Tôi nghĩ bạn có thể giải quyết vấn đề này bằng cách xóa thuộc tính Origin và thay vào đó sử dụng tệp công khai, vâng tôi biết đó không phải là một giải pháp tốt. Giải pháp khác là không sử dụng Điểm và thay vào đó hãy tạo loại Điểm của riêng bạn làm đối tượng.


Nếu Pointlà thành viên của kiểu tham chiếu thì nó sẽ không nằm trong ngăn xếp, nó sẽ nằm trong heap trong bộ nhớ của đối tượng chứa.
Greg Beech

0

Tôi đoán điều hấp dẫn ở đây là bạn đang cố gán các giá trị phụ của đối tượng trong câu lệnh thay vì chỉ định chính đối tượng đó. Bạn cần gán toàn bộ đối tượng Point trong trường hợp này vì loại thuộc tính là Point.

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

Hy vọng tôi có ý nghĩa ở đó


2
Điểm quan trọng là Điểm là một cấu trúc (valuetype). Nếu đó là một lớp (đối tượng) thì mã ban đầu sẽ hoạt động.
Hans Ke

@HansKesting: Nếu Pointlà một loại lớp có thể thay đổi, mã ban đầu sẽ đặt trường hoặc Xthuộc tính trong đối tượng được trả về bởi thuộc tính Origin. Tôi thấy không có lý do để tin rằng sẽ có tác dụng mong muốn đối với Origintài sản có chứa tài sản. Một số lớp Framework có các thuộc tính sao chép trạng thái của chúng sang các thể hiện của lớp có thể thay đổi mới và trả về chúng. Thiết kế như vậy có lợi thế là cho phép mã muốn thing1.Origin = thing2.Origin;đặt trạng thái nguồn gốc của đối tượng khớp với trạng thái khác, nhưng nó không thể cảnh báo về mã như thế nào thing1.Origin.X += 4;.
supercat

0

Chỉ cần xóa thuộc tính "get set" như sau, và sau đó mọi thứ hoạt động như mọi khi.

Trong trường hợp các kiểu nguyên thủy, hãy sử dụng 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]

    }
}      

0

Tôi nghĩ rằng rất nhiều người đang bị nhầm lẫn ở đây, vấn đề cụ thể này có liên quan đến việc hiểu rằng các thuộc tính loại giá trị trả về một bản sao của loại giá trị (như với các phương thức và bộ chỉ mục) và các trường loại giá trị được truy cập trực tiếp . Đoạn mã sau thực hiện chính xác những gì bạn đang cố gắng đạt được bằng cách truy cập trực tiếp vào trường sao lưu của thuộc tính (lưu ý: thể hiện một thuộc tính ở dạng dài dòng của nó với trường sao lưu là tương đương với thuộc tính tự động, nhưng có lợi thế là trong mã của chúng ta có thể truy cập trực tiếp vào trường sao lưu):

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

Lỗi bạn gặp phải là hậu quả gián tiếp của việc không hiểu rằng một thuộc tính trả về một bản sao của loại giá trị. Nếu bạn được trả về một bản sao của một loại giá trị và bạn không gán nó cho một biến cục bộ thì mọi thay đổi bạn thực hiện đối với bản sao đó không bao giờ có thể được đọc và do đó trình biên dịch đưa ra đây là lỗi do điều này không thể cố ý. Nếu chúng ta gán bản sao cho một biến cục bộ thì chúng ta có thể thay đổi giá trị của X, nhưng nó sẽ chỉ được thay đổi trên bản sao cục bộ, giúp sửa lỗi thời gian biên dịch, nhưng sẽ không có tác dụng sửa đổi thuộc tính Origin. Đoạn mã sau minh họa điều này, vì lỗi biên dịch không còn nữa, nhưng xác nhận gỡ lỗi sẽ thất bại:

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
    }
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.