Kesalahan: “Tidak dapat mengubah nilai kembali” c #


155

Saya menggunakan properti yang diimplementasikan secara otomatis. Saya kira cara tercepat untuk memperbaiki yang berikut adalah dengan mendeklarasikan variabel dukungan saya sendiri?

public Point Origin { get; set; }

Origin.X = 10; // fails with CS1612

Pesan Galat: Tidak dapat mengubah nilai balik 'ekspresi' karena ini bukan variabel

Upaya dilakukan untuk mengubah tipe nilai yang merupakan hasil dari ekspresi perantara. Karena nilainya tidak bertahan, nilainya tidak akan berubah.

Untuk mengatasi kesalahan ini, simpan hasil ekspresi dalam nilai perantara, atau gunakan tipe referensi untuk ekspresi perantara.


13
Ini adalah ilustrasi lain mengapa tipe nilai yang bisa berubah adalah ide yang buruk. Jika Anda dapat menghindari mutasi tipe nilai, lakukanlah.
Eric Lippert

Ambil kode berikut (dari upaya saya di implementasi AStar yang di-blog oleh EL tertentu :-), yang tidak dapat menghindari mengubah tipe nilai: kelas Path <T>: IEnumerable <T> di mana T: INode, new () {. ..} HexNode publik (int x, int y): this (Point baru (x, y)) {} Jalur <T> jalur = Jalur baru <T> (T baru (x, y)); // Kesalahan // Jalur Memperbaiki Jelek <T> jalur = Jalur baru <T> (T baru ()); path.LastStep.Centre = Titik baru (x, y);
Tom Wilson

Jawaban:


198

Ini karena Pointmerupakan tipe nilai ( struct).

Karena itu, ketika Anda mengakses Originproperti Anda mengakses salinan nilai yang dipegang oleh kelas, bukan nilai itu sendiri seperti yang Anda lakukan dengan tipe referensi ( class), jadi jika Anda menyetel Xproperti di atasnya maka Anda menetapkan properti pada salinan dan kemudian membuangnya, meninggalkan nilai asli tidak berubah. Ini mungkin bukan yang Anda maksudkan, itulah sebabnya kompiler memperingatkan Anda tentang hal itu.

Jika Anda ingin mengubah Xnilainya saja, Anda perlu melakukan sesuatu seperti ini:

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

2
@ Paul: Apakah Anda memiliki kemampuan untuk mengubah struct ke kelas?
Doug

1
Ini agak mengecewakan, karena penetapan properti yang saya tetapkan memiliki efek samping (struct bertindak sebagai tampilan ke dalam jenis referensi dukungan)
Alexander - Reinstate Monica

Solusi lain adalah dengan membuat struct Anda menjadi sebuah kelas. Tidak seperti di C ++, di mana kelas dan struct hanya berbeda dengan akses anggota default (pribadi dan publik, masing-masing), struct dan kelas di C # memiliki beberapa perbedaan lagi. Inilah beberapa info lebih lanjut: docs.microsoft.com/en-us/dotnet/csharp/programming-guide/…
Artorias2718

9

Menggunakan variabel pendukung tidak akan membantu. The Pointtipe adalah tipe Nilai.

Anda perlu menetapkan seluruh nilai Poin ke properti Origin: -

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

Masalahnya adalah bahwa ketika Anda mengakses properti Origin apa yang dikembalikan oleh getadalah salinan struktur Point di bidang properti Origin yang dibuat secara otomatis. Oleh karena itu modifikasi bidang X Anda salinan ini tidak akan mempengaruhi bidang yang mendasarinya. Compiler mendeteksi ini dan memberi Anda kesalahan karena operasi ini sama sekali tidak berguna.

Bahkan jika Anda menggunakan variabel dukungan Anda sendiri Anda getakan terlihat seperti: -

get { return myOrigin; }

Anda masih akan mengembalikan salinan struktur Point dan Anda akan mendapatkan kesalahan yang sama.

Hmm ... setelah membaca pertanyaan Anda dengan lebih hati-hati, mungkin Anda sebenarnya bermaksud memodifikasi variabel dukungan langsung dari dalam kelas Anda: -

myOrigin.X = 10;

Ya itu yang akan Anda butuhkan.


6

Sekarang Anda sudah tahu apa sumber kesalahan itu. Jika konstruktor tidak ada dengan kelebihan untuk mengambil properti Anda (dalam hal ini X), Anda dapat menggunakan penginisialisasi objek (yang akan melakukan semua keajaiban di belakang layar). Bukan berarti Anda tidak perlu membuat struct Anda abadi , tetapi hanya memberikan info tambahan:

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.

Ini dimungkinkan karena di balik layar ini terjadi:

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

Ini sepertinya hal yang sangat aneh untuk dilakukan, sama sekali tidak direkomendasikan. Hanya mendaftar cara alternatif. Cara yang lebih baik untuk dilakukan adalah membuat struct tidak berubah dan menyediakan konstruktor yang tepat.


2
Bukankah itu membuang nilainya Origin.Y? Diberikan properti tipe Point, saya akan berpikir cara idiomatis untuk berubah Xakan var temp=thing.Origin; temp.X = 23; thing.Origin = temp;. Pendekatan idiomatik memiliki keuntungan yang tidak harus menyebutkan anggota yang tidak ingin dimodifikasi, fitur yang hanya mungkin karena Pointdapat diubah. Saya bingung dengan filosofi yang mengatakan bahwa karena kompiler tidak dapat mengizinkan Origin.X = 23;orang harus merancang struct untuk memerlukan kode seperti Origin.X = new Point(23, Origin.Y);. Yang terakhir sepertinya sangat menjengkelkan bagi saya.
supercat

@ supercat ini adalah pertama kalinya saya memikirkan maksud Anda, sangat masuk akal! Apakah Anda memiliki pola / ide desain alternatif untuk mengatasi ini? Akan lebih mudah jika C # tidak memberikan konstruktor default untuk struct secara default (dalam hal ini saya benar-benar harus melewati keduanya Xdan Yuntuk konstruktor tertentu). Sekarang kehilangan titik ketika seseorang bisa melakukannya Point p = new Point(). Saya tahu mengapa itu benar-benar diperlukan untuk struct, jadi tidak ada gunanya berpikir tentang itu. Tetapi apakah Anda memiliki ide keren untuk memperbarui satu properti saja X?
nawfal

Untuk struct yang merangkum kumpulan variabel independen tetapi terkait (seperti koordinat suatu titik), preferensi saya adalah hanya memiliki struct mengekspos semua anggotanya sebagai bidang publik; untuk memodifikasi satu anggota properti struct, cukup bacakan, modifikasi anggota, dan tulis kembali. Akan lebih baik jika C # telah memberikan deklarasi "Simple-Old-Data-Struct" sederhana yang akan secara otomatis mendefinisikan konstruktor yang daftar parameternya cocok dengan daftar bidang, tetapi orang-orang yang bertanggung jawab untuk C # memandang rendah struct yang bisa berubah.
supercat

@ supercat saya mengerti. Perilaku yang tidak konsisten dari struct dan kelas membingungkan.
nawfal

Hasil kebingungan dari kepercayaan IMHO tidak membantu bahwa semuanya harus berperilaku seperti objek kelas. Sementara itu berguna untuk memiliki sarana meneruskan nilai-tipe nilai ke hal-hal yang mengharapkan tumpukan objek referensi, itu tidak berguna untuk berpura-pura variabel tipe-nilai menyimpan hal-hal yang berasal dari Object. Mereka tidak melakukannya. Setiap definisi tipe nilai sebenarnya mendefinisikan dua jenis hal: tipe lokasi penyimpanan (digunakan untuk variabel, slot array, dll.) Dan tipe objek tumpukan, kadang-kadang disebut sebagai tipe "kotak" (digunakan ketika nilai tipe nilai disimpan ke lokasi tipe referensi).
supercat

2

Selain memperdebatkan pro dan kontra dari struct versus kelas, saya cenderung melihat tujuan dan mendekati masalah dari perspektif itu.

Yang sedang berkata, jika Anda tidak perlu menulis kode di belakang properti dapatkan dan mengatur metode (seperti dalam contoh Anda), maka tidak akan lebih mudah untuk hanya mendeklarasikan Originsebagai bidang kelas daripada properti? Saya harus berpikir ini akan memungkinkan Anda untuk mencapai tujuan Anda.

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

Masalahnya adalah Anda menunjuk ke nilai yang terletak di tumpukan dan nilainya tidak akan dikembalikan ke properti orignal sehingga C # tidak memungkinkan Anda mengembalikan referensi ke tipe nilai. Saya pikir Anda dapat menyelesaikan ini dengan menghapus properti Origin dan bukannya menggunakan arsip publik, ya saya tahu itu bukan solusi yang bagus. Solusi lain adalah dengan tidak menggunakan Point, dan sebaliknya membuat tipe Point Anda sendiri sebagai objek.


Jika itu Pointadalah anggota dari tipe referensi maka itu tidak akan berada di tumpukan, itu akan berada di tumpukan di memori objek yang mengandung.
Greg Beech

0

Saya kira tangkapan di sini adalah bahwa Anda mencoba untuk menetapkan sub-nilai objek dalam pernyataan daripada menetapkan objek itu sendiri. Anda perlu menetapkan seluruh objek Point dalam kasus ini karena tipe properti adalah Point.

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

Semoga saya masuk akal di sana


2
Poin penting adalah bahwa Point adalah struct (valuetype). Jika itu adalah kelas (objek) maka kode asli akan berfungsi.
Hans Ke st ing

@HansKesting: Jika Pointadalah tipe kelas yang bisa diubah, kode asli akan menetapkan bidang atau properti Xdalam objek yang dikembalikan oleh properti Origin. Saya tidak melihat alasan untuk percaya bahwa akan memiliki efek yang diinginkan pada objek yang mengandung Originproperti. Beberapa kelas Framework memiliki properti yang menyalin statusnya ke instance kelas yang dapat diubah dan mengembalikannya. Desain seperti ini memiliki keunggulan memungkinkan kode suka thing1.Origin = thing2.Origin;mengatur keadaan asal objek agar sesuai dengan yang lain, tetapi tidak bisa memperingatkan tentang kode seperti thing1.Origin.X += 4;.
supercat

0

Hapus saja properti "siapkan" sebagai diikuti, dan kemudian semuanya berfungsi seperti biasa.

Dalam kasus jenis primitif instread gunakan 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

Saya pikir banyak orang menjadi bingung di sini, masalah khusus ini berkaitan dengan pemahaman bahwa properti tipe nilai mengembalikan salinan tipe nilai (seperti dengan metode dan pengindeks), dan bidang tipe nilai diakses secara langsung . Kode berikut melakukan persis apa yang Anda coba capai dengan mengakses bidang dukungan properti secara langsung (catatan: mengekspresikan properti dalam bentuk verbose dengan bidang dukungan adalah setara dengan properti otomatis, tetapi memiliki keuntungan bahwa dalam kode kami, kami dapat mengakses bidang dukungan secara langsung):

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

Kesalahan yang Anda dapatkan adalah konsekuensi tidak langsung dari tidak memahami bahwa properti mengembalikan salinan tipe nilai. Jika Anda mengembalikan salinan tipe nilai dan Anda tidak menetapkannya ke variabel lokal maka setiap perubahan yang Anda buat pada salinan itu tidak akan pernah bisa dibaca dan oleh karena itu kompiler meningkatkan ini sebagai kesalahan karena ini tidak bisa disengaja. Jika kita menetapkan salinan ke variabel lokal maka kita dapat mengubah nilai X, tetapi itu hanya akan diubah pada salinan lokal, yang memperbaiki kesalahan waktu kompilasi, tetapi tidak akan memiliki efek yang diinginkan dari memodifikasi properti Origin. Kode berikut menggambarkan ini, karena kesalahan kompilasi hilang, tetapi pernyataan debug akan gagal:

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
    }
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.