ข้อผิดพลาด:“ ไม่สามารถแก้ไขค่าที่ส่งคืนได้” c #


155

ฉันใช้คุณสมบัติที่ใช้งานอัตโนมัติ ฉันเดาวิธีที่เร็วที่สุดในการแก้ไขปัญหาต่อไปนี้คือการประกาศตัวแปรสำรองของฉันเอง

public Point Origin { get; set; }

Origin.X = 10; // fails with CS1612

ข้อความแสดงข้อผิดพลาด: ไม่สามารถแก้ไขค่าที่ส่งคืนของ 'expression' ได้เนื่องจากไม่ใช่ตัวแปร

มีความพยายามในการแก้ไขชนิดของค่าที่เป็นผลลัพธ์ของนิพจน์กลาง เนื่องจากค่าไม่คงอยู่ค่าจะไม่เปลี่ยนแปลง

เมื่อต้องการแก้ไขข้อผิดพลาดนี้ให้เก็บผลลัพธ์ของนิพจน์ในค่าระดับกลางหรือใช้ประเภทการอ้างอิงสำหรับนิพจน์กลาง


13
นี่เป็นอีกตัวอย่างหนึ่งที่อธิบายว่าทำไมประเภทค่าที่ไม่แน่นอนคือความคิดที่ไม่ดี หากคุณสามารถหลีกเลี่ยงการกลายพันธุ์ชนิดค่าให้ทำ
Eric Lippert

ใช้รหัสต่อไปนี้ (จากความพยายามของฉันในการติดตั้ง AStar ที่ถูกบล็อกโดย EL :-) ที่แน่นอนซึ่งไม่สามารถหลีกเลี่ยงการเปลี่ยนประเภทของค่า: class Path <T>: IEnumerable <T> โดยที่ T: INode, new () { .. } HexNode สาธารณะ (int x, int y): นี่ (จุดใหม่ (x, y)) {} เส้นทาง <T> เส้นทาง = เส้นทางใหม่ <T> (ใหม่ T (x, y)); // ข้อผิดพลาด // น่าเกลียดแก้ไขเส้นทาง <T> path = new Path <T> (new T ()); path.LastStep.Centre = จุดใหม่ (x, y);
Tom Wilson

คำตอบ:


198

นี่เป็นเพราะPointเป็นประเภทค่า ( struct)

ด้วยเหตุนี้เมื่อคุณเข้าถึงOriginคุณสมบัติที่คุณกำลังเข้าถึงสำเนาของค่าที่เก็บไว้ในชั้นเรียนไม่ได้ค่าตัวเองเช่นเดียวกับที่คุณมีประเภทอ้างอิง ( class) ดังนั้นหากคุณตั้งค่าXคุณสมบัติในนั้นคุณจะตั้งค่า คุณสมบัติบนสำเนาแล้วละทิ้งทิ้งค่าเดิมไม่เปลี่ยนแปลง นี่อาจไม่ใช่สิ่งที่คุณตั้งใจซึ่งเป็นสาเหตุที่คอมไพเลอร์เตือนคุณเกี่ยวกับเรื่องนี้

หากคุณต้องการเปลี่ยนเพียงXค่าคุณต้องทำสิ่งนี้:

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

2
@ พอล: คุณมีความสามารถในการเปลี่ยน struct เป็นคลาสหรือไม่?
Doug

1
นี่เป็นคนเกียจคร้านเพราะคุณสมบัติ setter im assign มีผลข้างเคียง (struct ทำหน้าที่เป็นมุมมองในประเภทการอ้างอิงสำรอง)
Alexander - Reinstate Monica

อีกวิธีหนึ่งคือการทำให้โครงสร้างของคุณเป็นคลาส แตกต่างจาก C ++ ที่คลาสและโครงสร้างแตกต่างกันเฉพาะการเข้าถึงสมาชิกเริ่มต้น (ส่วนตัวและสาธารณะตามลำดับ), structs และคลาสใน C # มีความแตกต่างเล็กน้อย นี่คือข้อมูลเพิ่มเติม: docs.microsoft.com/en-us/dotnet/csharp/programming-guide/…
Artorias2718

9

การใช้ตัวแปรสำรองจะไม่ช่วย Pointประเภทเป็นชนิดราคา

คุณต้องกำหนดค่าคะแนนทั้งหมดให้กับคุณสมบัติ Origin: -

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

ปัญหาคือเมื่อคุณเข้าถึงคุณสมบัติ Origin สิ่งที่ส่งคืนโดยgetคือสำเนาของโครงสร้างจุดในฟิลด์คุณสมบัติที่สร้างขึ้นโดยอัตโนมัติ ดังนั้นการแก้ไขฟิลด์ X สำเนาของคุณนี้จะไม่ส่งผลกระทบกับฟิลด์ต้นแบบ คอมไพเลอร์ตรวจจับสิ่งนี้และแจ้งข้อผิดพลาดให้คุณเนื่องจากการดำเนินการนี้ไม่มีประโยชน์อย่างสิ้นเชิง

แม้ว่าคุณจะใช้ตัวแปรสำรองของคุณเองคุณgetก็จะมีลักษณะดังนี้: -

get { return myOrigin; }

คุณยังคงส่งคืนสำเนาของโครงสร้างจุดและคุณจะได้รับข้อผิดพลาดเดียวกัน

อืม ... เมื่อได้อ่านคำถามของคุณอย่างระมัดระวังบางทีคุณอาจต้องการปรับเปลี่ยนตัวแปรสำรองโดยตรงจากในชั้นเรียนของคุณ: -

myOrigin.X = 10;

ใช่ว่าจะเป็นสิ่งที่คุณต้องการ


6

ถึงตอนนี้คุณก็รู้แล้วว่าแหล่งที่มาของข้อผิดพลาดคืออะไร ในกรณีที่คอนสตรัคเตอร์ไม่ได้มีการโอเวอร์โหลดเพื่อใช้คุณสมบัติของคุณ (ในกรณีนี้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;

ดูเหมือนว่าจะเป็นสิ่งที่แปลกมากที่จะไม่แนะนำเลย เพียงแค่แสดงรายการวิธีอื่น วิธีที่ดีกว่าที่จะทำคือทำให้ struct ไม่เปลี่ยนแปลงและจัดให้มี Constructor ที่เหมาะสม


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 # ไม่ได้ให้สร้างเริ่มต้นสำหรับ struct โดยค่าเริ่มต้น (ในกรณีที่ผมอย่างเคร่งครัดต้องผ่านทั้งสองXและYการคอนสตรัคที่เฉพาะเจาะจง) Point p = new Point()ตอนนี้ก็สูญเสียจุดเมื่อหนึ่งสามารถทำ ฉันรู้ว่าทำไมมันถึงต้องการโครงสร้างจริงๆดังนั้นจึงไม่มีประเด็นที่จะคิดเกี่ยวกับมัน แต่คุณจะมีความคิดที่เย็นเพื่ออัปเดตเพียงหนึ่งในสถานที่ให้บริการเช่นX?
nawfal

สำหรับ structs ที่ encapsualate ชุดของตัวแปรอิสระ แต่ที่เกี่ยวข้อง (เช่นพิกัดของจุด), การตั้งค่าของฉันคือการมีโครงสร้างเพียงเปิดเผยสมาชิกทั้งหมดเป็นเขตข้อมูลสาธารณะ เพื่อแก้ไขสมาชิกหนึ่งคนของคุณสมบัติ struct เพียงอ่านออกแก้ไขสมาชิกและเขียนกลับ มันคงจะดีถ้า C # ได้จัดทำประกาศ "ธรรมดาธรรมดา - ข้อมูล - โครงสร้าง" ซึ่งจะกำหนดคอนสตรัคเตอร์ซึ่งรายการพารามิเตอร์ตรงกับรายการฟิลด์โดยอัตโนมัติ
supercat

@ supercat ฉันเข้าใจแล้ว พฤติกรรมที่ไม่สอดคล้องกันของ structs และคลาสมีความสับสน
nawfal

ความสับสนเป็นผลมาจากความเชื่อที่ไม่มีประโยชน์ของ IMHO ว่าทุกสิ่งควรทำตัวเหมือนคลาสวัตถุ Objectในขณะที่มันมีประโยชน์ที่จะมีวิธีในการส่งผ่านค่ามูลค่าชนิดสิ่งที่คาดหวังอ้างอิงวัตถุกองก็ไม่ได้มีประโยชน์ในการหลอกตัวแปรค่าชนิดถือสิ่งที่เป็นผลมาจาก พวกเขาทำไม่ได้ คำจำกัดความประเภทค่าทั้งหมดจะกำหนดสิ่งสองชนิด: ประเภทตำแหน่งที่เก็บข้อมูล (ใช้สำหรับตัวแปร, สล็อตอาร์เรย์ ฯลฯ ) และประเภทวัตถุฮีปบางครั้งเรียกว่าเป็น "ชนิดบรรจุกล่อง" (ใช้เมื่อค่าประเภทค่า ถูกเก็บไว้ในตำแหน่งประเภทการอ้างอิง)
supercat

2

นอกเหนือจากการอภิปรายข้อดีข้อเสียของ structs กับคลาสฉันมักจะมองไปที่เป้าหมายและเข้าถึงปัญหาจากมุมมองนั้น

ที่ถูกกล่าวว่าถ้าคุณไม่จำเป็นต้องเขียนโค้ดด้านหลังคุณสมบัติรับและกำหนดวิธีการ (เช่นในตัวอย่างของคุณ) แล้วมันจะไม่ง่ายกว่าที่จะประกาศ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

0

ปัญหาคือคุณชี้ไปที่ค่าที่ตั้งอยู่บนสแต็กและค่าจะไม่ถูก relfected กลับไปที่คุณสมบัติ orignal ดังนั้น C # ไม่อนุญาตให้คุณส่งคืนการอ้างอิงถึงชนิดของค่า ฉันคิดว่าคุณสามารถแก้ปัญหานี้ได้โดยการลบคุณสมบัติ Origin และใช้ไฟล์สาธารณะแทนใช่ฉันรู้ว่าไม่ใช่วิธีแก้ปัญหาที่ดี โซลูชันอื่นคือไม่ใช้ Point และสร้างประเภท Point ของคุณเองเป็นวัตถุแทน


ถ้าPointเป็นสมาชิกของประเภทการอ้างอิงแล้วมันจะไม่อยู่ในสแต็คมันจะอยู่ในกองในหน่วยความจำของวัตถุที่มี
เกร็กบีช

0

ฉันเดาว่าที่นี่คือคุณกำลังพยายามกำหนดค่าย่อยของวัตถุในคำสั่งแทนที่จะกำหนดวัตถุเอง คุณจำเป็นต้องกำหนดออบเจ็กต์ Point ทั้งหมดในกรณีนี้เนื่องจากประเภทคุณสมบัติคือ Point

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

หวังว่าฉันจะทำให้รู้สึกมี


2
จุดสำคัญคือจุดนั้นเป็นโครงสร้าง (ชนิดของค่า) ถ้าเป็น class (object) รหัสต้นฉบับก็น่าจะใช้ได้
Hans Ke ในวันที่

@HansKesting: ถ้าPointถูกประเภทชั้นไม่แน่นอนรหัสเดิมจะมีข้อมูลชุดหรือคุณสมบัติในวัตถุที่ส่งกลับโดยคุณสมบัติX Originฉันไม่เห็นเหตุผลที่จะเชื่อว่าจะมีผลตามที่ต้องการกับวัตถุที่มีOriginคุณสมบัติ บางคลาส Framework มีคุณสมบัติที่คัดลอกสถานะไปยังอินสแตนซ์คลาสที่ไม่แน่นอนใหม่และส่งคืนคลาสเหล่านั้น การออกแบบดังกล่าวมีความได้เปรียบในการอนุญาตให้รหัสเช่นthing1.Origin = thing2.Origin;การตั้งค่าสถานะของการกำเนิดของวัตถุเพื่อให้ตรงกับที่อื่น thing1.Origin.X += 4;แต่ก็ไม่สามารถเตือนเกี่ยวกับรหัสเช่น
supercat

0

เพียงลบคุณสมบัติ "รับการตั้งค่า" ดังต่อไปนี้จากนั้นทุกอย่างจะทำงานเหมือนเดิม

ในกรณีที่ชนิดดั้งเดิมนั้นใช้ 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

ผมคิดว่าคนจำนวนมากจะได้รับการสับสนที่นี่ปัญหานี้โดยเฉพาะอย่างยิ่งที่เกี่ยวข้องกับการทำความเข้าใจว่าประเภทค่าคุณสมบัติกลับสำเนาของประเภทค่าที่ (เช่นเดียวกับวิธีการและ indexers) และประเภทค่าเขตข้อมูลที่มีการเข้าถึงโดยตรง รหัสต่อไปนี้ทำในสิ่งที่คุณพยายามจะทำโดยการเข้าถึงเขตข้อมูลสำรองของทรัพย์สินโดยตรง (หมายเหตุ: การแสดงคุณสมบัติในรูปแบบ verbose ด้วยเขตข้อมูลสำรองนั้นเทียบเท่ากับคุณสมบัติอัตโนมัติ แต่มีข้อได้เปรียบในรหัสของเราที่เราสามารถทำได้ เข้าถึงข้อมูลสำรองโดยตรง):

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
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.