Błąd: „Nie można zmodyfikować wartości zwracanej” C #

155

Używam właściwości zaimplementowanych automatycznie. Wydaje mi się, że najszybszym sposobem rozwiązania tego problemu jest zadeklarowanie własnej zmiennej bazowej?

public Point Origin { get; set; }

Origin.X = 10; // fails with CS1612

Komunikat o błędzie: Nie można zmodyfikować wartości zwracanej „wyrażenie”, ponieważ nie jest to zmienna

Podjęto próbę zmodyfikowania typu wartości, który był wynikiem wyrażenia pośredniego. Ponieważ wartość nie jest utrwalana, pozostanie niezmieniona.

Aby rozwiązać ten błąd, zapisz wynik wyrażenia w wartości pośredniej lub użyj typu odwołania dla wyrażenia pośredniego.

Paweł
źródło
13
To kolejna ilustracja tego, dlaczego zmienne typy wartości są złym pomysłem. Jeśli możesz uniknąć zmiany typu wartości, zrób to.
Eric Lippert
Weź następujący kod (z moich wysiłków przy implementacji AStar na blogu przez pewien EL :-), który nie mógł uniknąć zmiany typu wartości: class Path <T>: IEnumerable <T> gdzie T: INode, new () {. ..} public HexNode (int x, int y): this (new Point (x, y)) {} Path <T> path = new Path <T> (new T (x, y)); // Błąd // Ugly fix Path <T> path = new Path <T> (new T ()); path.LastStep.Centre = new Point (x, y);
Tom Wilson,

Odpowiedzi:

198

Dzieje się tak, ponieważ Pointjest to typ wartości ( struct).

Z tego powodu, gdy uzyskujesz dostęp do Originwłaściwości, uzyskujesz dostęp do kopii wartości przechowywanej przez klasę, a nie do samej wartości, jak w przypadku typu referencyjnego ( class), więc jeśli ustawisz na niej Xwłaściwość, to ustawiasz właściwość na kopii, a następnie odrzucenie jej, pozostawiając pierwotną wartość niezmienioną. Prawdopodobnie nie jest to zamierzone przez Ciebie rozwiązanie, dlatego kompilator ostrzega Cię o tym.

Jeśli chcesz zmienić tylko Xwartość, musisz zrobić coś takiego:

Origin = new Point(10, Origin.Y);
Greg Beech
źródło
2
@Paul: Czy masz możliwość zmiany struktury na klasę?
Doug
1
To trochę kłopotliwe, ponieważ przypisywanie im właściwości ustawiających ma efekt uboczny (struktura działa jako wgląd w typ odniesienia kopii zapasowej)
Alexander - Przywróć Monikę
Innym rozwiązaniem jest po prostu przekształcenie struktury w klasę. W przeciwieństwie do C ++, gdzie klasa i struktura różnią się tylko domyślnym dostępem do elementu członkowskiego (odpowiednio prywatnym i publicznym), struktury i klasy w języku C # mają kilka dodatkowych różnic. Oto więcej informacji: docs.microsoft.com/en-us/dotnet/csharp/programming-guide/…
Artorias2718
9

Używanie zmiennej bazowej nie pomoże. PointTyp to typ wartości.

Musisz przypisać całą wartość Point do właściwości Origin: -

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

Problem polega na tym, że kiedy uzyskujesz dostęp do właściwości Origin, to, co jest zwracane przez, getjest kopią struktury Point w automatycznie utworzonym polu Właściwości Origin. W związku z tym modyfikacja pola X ta kopia nie wpłynie na pole bazowe. Kompilator wykrywa to i wyświetla błąd, ponieważ ta operacja jest całkowicie bezużyteczna.

Nawet jeśli użyłeś własnej zmiennej getbazowej, wyglądałoby to tak: -

get { return myOrigin; }

Nadal zwracałbyś kopię struktury Point i otrzymywałbyś ten sam błąd.

Hmm ... po dokładniejszym przeczytaniu pytania, być może faktycznie masz na myśli zmodyfikowanie zmiennej bazowej bezpośrednio z poziomu swojej klasy: -

myOrigin.X = 10;

Tak, to byłoby to, czego potrzebujesz.

AnthonyWJones
źródło
6

Teraz już wiesz, jakie jest źródło błędu. W przypadku, gdy nie istnieje konstruktor z przeciążeniem, aby przejąć twoją właściwość (w tym przypadku X), możesz użyć inicjatora obiektu (który wykona całą magię za kulisami). Nie chodzi o to, że nie musisz uczynić swoich struktur niezmiennymi , ale po prostu podać dodatkowe informacje:

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.

Jest to możliwe, ponieważ dzieje się to za kulisami:

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

Wygląda to na bardzo dziwną czynność, w ogóle niezalecaną. Podaj tylko inny sposób. Lepszym sposobem jest uczynienie struktury niezmienną i zapewnienie odpowiedniego konstruktora.

nawfal
źródło
2
Czy to nie zniweczy wartości Origin.Y? Biorąc pod uwagę właściwość typu Point, pomyślałbym, że idiomatyczny sposób zmiany Xbyłby po prostu taki var temp=thing.Origin; temp.X = 23; thing.Origin = temp;. Podejście idiomatyczne ma tę zaletę, że nie musi wspominać o członkach, których nie chce modyfikować, co jest możliwe tylko dlatego, że Pointjest zmienne. Jestem zdziwiony filozofią, która mówi, że ponieważ kompilator nie może pozwolić, Origin.X = 23;aby projekt struktury wymagał takiego kodu Origin.X = new Point(23, Origin.Y);. To ostatnie wydaje mi się naprawdę obrzydliwe.
supercat
@supercat. Po raz pierwszy myślę o Twoim punkcie, ma to sens! Czy masz inny pomysł na wzór / projekt, aby rozwiązać ten problem? Byłoby łatwiej, gdyby C # domyślnie nie dostarczał domyślnego konstruktora dla struktury (w takim przypadku muszę ściśle przekazać oba Xi Ydo określonego konstruktora). Teraz traci sens, kiedy można to zrobić Point p = new Point(). Wiem, dlaczego jest to naprawdę wymagane dla struktury, więc nie ma sensu o tym myśleć. Ale czy masz fajny pomysł na aktualizację tylko jednej właściwości X?
nawfal
W przypadku struktur, które zawierają zbiór niezależnych, ale powiązanych ze sobą zmiennych (takich jak współrzędne punktu), wolę po prostu, aby struktura ujawniała wszystkie swoje elementy członkowskie jako pola publiczne; aby zmodyfikować jeden element członkowski właściwości struct, po prostu przeczytaj go, zmodyfikuj element i zapisz go z powrotem. Byłoby miło, gdyby C # dostarczył deklarację „simple-Old-Data-Struct”, która automatycznie definiowałaby konstruktora, którego lista parametrów jest zgodna z listą pól, ale osoby odpowiedzialne za C # gardzą strukturami zmiennymi.
supercat
@supercat, rozumiem. Niespójne zachowanie struktur i klas jest mylące.
nawfal
Zamieszanie wynika z niepomocnego przekonania IMHO, że wszystko powinno zachowywać się jak obiekt klasy. Chociaż przydatne jest posiadanie środków do przekazywania wartości typu wartości do rzeczy, które oczekują odwołań do obiektów sterty, nie jest przydatne udawać, że zmienne typu wartości przechowują rzeczy, z których pochodzą Object. Oni tego nie robią. Każda definicja typu wartości w rzeczywistości definiuje dwa rodzaje rzeczy: typ miejsca przechowywania (używany dla zmiennych, pól tablicowych itp.) Oraz typ obiektu sterty, czasami nazywany typem „pudełkowym” (używany, gdy wartość typu wartości jest przechowywany w lokalizacji typu referencyjnego).
supercat
2

Oprócz omawiania zalet i wad struktur kontra klas, staram się patrzeć na cel i podchodzić do problemu z tej perspektywy.

Biorąc to pod uwagę, jeśli nie musisz pisać kodu za właściwością metody get i set (jak w twoim przykładzie), to czy nie byłoby łatwiej po prostu zadeklarować the Originjako pole klasy, a nie właściwość? Myślę, że to pozwoliłoby ci osiągnąć twój cel.

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
Mitselplik
źródło
0

Problem polega na tym, że wskazujesz wartość znajdującą się na stosie, a wartość nie zostanie ponownie przeniesiona z powrotem do właściwości oryginalnej, więc C # nie zezwala na zwrócenie odwołania do typu wartości. Myślę, że możesz rozwiązać ten problem, usuwając właściwość Origin i zamiast tego użyj publicznego pola, tak, wiem, że to nie jest miłe rozwiązanie. Innym rozwiązaniem jest nie używanie Point, a zamiast tego utworzenie własnego typu Point jako obiektu.

Fredrik Normén
źródło
Jeśli Pointjest członkiem typu referencyjnego, to nie będzie na stosie, będzie na stercie w pamięci obiektu zawierającego.
Greg Beech
0

Wydaje mi się, że haczyk polega na tym, że próbujesz przypisać wartości podrzędne obiektu w instrukcji, zamiast przypisywać sam obiekt. W tym przypadku musisz przypisać cały obiekt Point, ponieważ typ właściwości to Point.

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

Mam nadzieję, że mam tam sens

MSIL
źródło
2
Ważne jest to, że Point jest strukturą (typem wartości). Gdyby to była klasa (obiekt), oryginalny kod działałby.
Hans Ke ing
@HansKesting: Gdyby Pointbył zmiennym typem klasy, oryginalny kod ustawiłby pole lub właściwość Xw obiekcie zwróconym przez właściwość Origin. Nie widzę powodu, by sądzić, że miałoby to pożądany wpływ na przedmiot zawierający tę Originwłaściwość. Niektóre klasy Framework mają właściwości, które kopiują swój stan do nowych mutowalnych wystąpień klas i zwracają je. Taki projekt ma tę zaletę, że pozwala kodowi thing1.Origin = thing2.Origin;na ustawienie stanu pochodzenia obiektu tak, aby pasował do innego, ale nie może ostrzegać o kodzie takim jak thing1.Origin.X += 4;.
supercat
0

Po prostu usuń właściwość „get set” w następujący sposób, a wszystko będzie działać jak zawsze.

W przypadku typów pierwotnych instread użyj polecenia 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]

    }
}      
Roberto Mutti
źródło
0

Myślę, że wiele osób jest tu zdezorientowanych, ten konkretny problem jest związany ze zrozumieniem, że właściwości typu wartości zwracają kopię typu wartości (tak jak w przypadku metod i indeksatorów), a pola typu wartości są dostępne bezpośrednio . Poniższy kod robi dokładnie to, co próbujesz osiągnąć, uzyskując bezpośredni dostęp do pola zapasowego właściwości (uwaga: wyrażenie właściwości w postaci pełnej za pomocą pola zapasowego jest odpowiednikiem właściwości automatycznej, ale ma tę zaletę, że w naszym kodzie możemy uzyskać bezpośredni dostęp do pola zapasowego):

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

Otrzymany błąd jest pośrednią konsekwencją niezrozumienia, że ​​właściwość zwraca kopię typu wartości. Jeśli otrzymasz kopię typu wartości i nie przypiszesz jej do zmiennej lokalnej, wszelkie zmiany, które wprowadzisz w tej kopii, nigdy nie mogą zostać odczytane, a zatem kompilator zgłasza to jako błąd, ponieważ nie może to być zamierzone. Jeśli przypiszemy kopię do zmiennej lokalnej, możemy zmienić wartość X, ale zostanie ona zmieniona tylko na kopii lokalnej, co naprawia błąd czasu kompilacji, ale nie będzie miało pożądanego efektu modyfikacji właściwości Origin. Poniższy kod ilustruje to, ponieważ błąd kompilacji zniknął, ale asercja debugowania zakończy się niepowodzeniem:

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
    }
}
Matt
źródło