Erreur: «Impossible de modifier la valeur de retour» c #

155

J'utilise des propriétés implémentées automatiquement. Je suppose que le moyen le plus rapide de résoudre le problème suivant est de déclarer ma propre variable de sauvegarde?

public Point Origin { get; set; }

Origin.X = 10; // fails with CS1612

Message d'erreur: impossible de modifier la valeur de retour de 'expression' car ce n'est pas une variable

Tentative de modification d'un type de valeur résultant d'une expression intermédiaire. Étant donné que la valeur n'est pas persistante, la valeur restera inchangée.

Pour résoudre cette erreur, stockez le résultat de l'expression dans une valeur intermédiaire ou utilisez un type de référence pour l'expression intermédiaire.

Paul
la source
13
Ceci est encore une autre illustration de la raison pour laquelle les types de valeur mutables sont une mauvaise idée. Si vous pouvez éviter de muter un type valeur, faites-le.
Eric Lippert
Prenez le code suivant (issu de mes efforts sur une implémentation AStar bloggée par un certain EL :-), qui n'a pas pu éviter de changer un type de valeur: class Path <T>: IEnumerable <T> où T: INode, new () {. ..} public HexNode (int x, int y): this (new Point (x, y)) {} Path <T> path = new Path <T> (new T (x, y)); // Erreur // Correctif laid Chemin <T> chemin = nouveau chemin <T> (nouveau T ()); path.LastStep.Centre = nouveau point (x, y);
Tom Wilson

Réponses:

198

C'est parce que Pointc'est un type valeur ( struct).

Pour cette raison, lorsque vous accédez à la Originpropriété, vous accédez à une copie de la valeur détenue par la classe, pas à la valeur elle-même comme vous le feriez avec un type de référence ( class), donc si vous définissez la Xpropriété dessus, vous définissez la propriété sur la copie, puis en la supprimant, en laissant la valeur d'origine inchangée. Ce n'est probablement pas ce que vous vouliez, c'est pourquoi le compilateur vous en avertit.

Si vous souhaitez modifier uniquement la Xvaleur, vous devez faire quelque chose comme ceci:

Origin = new Point(10, Origin.Y);
Greg Beech
la source
2
@Paul: Avez-vous la possibilité de changer la structure en classe?
Doug le
1
C'est un peu décevant, car le setter de propriété que j'attribue a un effet secondaire (la structure agit comme une vue dans un type de référence de support)
Alexander - Réintégrer Monica
Une autre solution consiste simplement à transformer votre structure en classe. Contrairement à C ++, où une classe et une structure ne diffèrent que par l'accès aux membres par défaut (privé et public, respectivement), les structures et les classes en C # présentent quelques différences supplémentaires. Voici quelques informations supplémentaires: docs.microsoft.com/en-us/dotnet/csharp/programming-guide/…
Artorias2718
9

L'utilisation d'une variable de sauvegarde n'aidera pas. Le Pointtype est un type Value.

Vous devez attribuer toute la valeur Point à la propriété Origin: -

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

Le problème est que lorsque vous accédez à la propriété Origin, ce qui est renvoyé par le getest une copie de la structure Point dans le champ créé automatiquement par les propriétés Origin. Par conséquent, votre modification du champ X cette copie n'affecterait pas le champ sous-jacent. Le compilateur détecte cela et vous donne une erreur car cette opération est totalement inutile.

Même si vous utilisiez votre propre variable de sauvegarde, getcela ressemblerait à: -

get { return myOrigin; }

Vous retourneriez toujours une copie de la structure Point et vous obtiendriez la même erreur.

Hmm ... après avoir lu votre question plus attentivement, peut-être voulez-vous en fait modifier la variable de sauvegarde directement depuis votre classe: -

myOrigin.X = 10;

Oui, ce serait ce dont vous auriez besoin.

AnthonyWJones
la source
6

À présent, vous savez déjà quelle est la source de l'erreur. Dans le cas où un constructeur n'existe pas avec une surcharge pour prendre votre propriété (dans ce cas X), vous pouvez utiliser l'initialiseur d'objet (qui fera toute la magie dans les coulisses). Non pas que vous n'ayez pas besoin de rendre vos structures immuables , mais simplement de donner des informations supplémentaires:

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.

Ceci est possible car dans les coulisses cela se produit:

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

Cela semble être une chose très étrange à faire, pas du tout recommandée. Juste énumérer une autre manière. La meilleure façon de faire est de rendre struct immuable et de fournir un constructeur approprié.

nawfal
la source
2
Cela ne ferait-il pas perdre la valeur de Origin.Y? Étant donné une propriété de type Point, je pense que la manière idiomatique de changer Xserait simplement var temp=thing.Origin; temp.X = 23; thing.Origin = temp;. L'approche idiomatique a l'avantage de ne pas avoir à mentionner les membres qu'elle ne veut pas modifier, une fonctionnalité qui n'est possible que parce qu'elle Pointest mutable. Je suis perplexe devant la philosophie qui dit cela parce que le compilateur ne peut pas permettre que l' Origin.X = 23;on doive concevoir une structure pour exiger du code comme Origin.X = new Point(23, Origin.Y);. Ce dernier me semble vraiment dégueulasse.
supercat
@supercat c'est la première fois que je pense à votre point, cela a beaucoup de sens! Avez-vous une autre idée de modèle / conception pour résoudre ce problème? Cela aurait été plus facile si C # n'avait pas fourni le constructeur par défaut pour une structure par défaut (dans ce cas, je dois strictement passer à la fois Xet Yà un constructeur spécifique). Maintenant, il perd le point quand on peut le faire Point p = new Point(). Je sais pourquoi c'est vraiment nécessaire pour une structure, donc inutile d'y penser. Mais avez-vous une bonne idée de mettre à jour une seule propriété comme X?
nawfal
Pour les structures qui encapsulent une collection de variables indépendantes mais liées (telles que les coordonnées d'un point), ma préférence est simplement que la structure expose tous ses membres en tant que champs publics; pour modifier un membre d'une propriété struct, il suffit de le lire, de modifier le membre et de le réécrire. Cela aurait été bien si C # avait fourni une déclaration "simple Plain-Old-Data-Struct" qui définirait automatiquement un constructeur dont la liste de paramètres correspondait à la liste des champs, mais les personnes responsables de C # méprisent les structures mutables.
supercat
@supercat je comprends. Le comportement incohérent des structures et des classes est déroutant.
nawfal
La confusion résulte de la croyance inutile à mon humble avis que tout devrait se comporter comme un objet de classe. Bien qu'il soit utile d'avoir un moyen de passer des valeurs de type valeur à des choses qui attendent des références d'objet de tas, il n'est pas utile de prétendre que les variables de type valeur contiennent des éléments qui dérivent Object. Ils ne le font pas. Chaque définition de type valeur définit en fait deux types de choses: un type d'emplacement de stockage (utilisé pour les variables, les emplacements de tableau, etc.) et un type d'objet de tas, parfois appelé type «en boîte» (utilisé lorsqu'une valeur de type valeur est stocké dans un emplacement de type référence).
supercat
2

En plus de débattre des avantages et des inconvénients des structures par rapport aux classes, j'ai tendance à regarder l'objectif et à aborder le problème de cette perspective.

Cela étant dit, si vous n'avez pas besoin d'écrire du code derrière les méthodes get et set de la propriété (comme dans votre exemple), ne serait-il pas plus facile de simplement déclarer le Origincomme un champ de la classe plutôt qu'une propriété? Je devrais penser que cela vous permettrait d'atteindre votre objectif.

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
la source
0

Le problème est que vous pointez sur une valeur située sur la pile et que la valeur ne sera pas relfectée vers la propriété orignal, donc C # ne vous permet pas de renvoyer une référence à un type valeur. Je pense que vous pouvez résoudre ce problème en supprimant la propriété Origin et en utilisant à la place un fichier public, oui je sais que ce n'est pas une bonne solution. L'autre solution consiste à ne pas utiliser le point, mais à créer votre propre type de point en tant qu'objet.

Fredrik Normén
la source
Si le Pointest un membre d'un type de référence, il ne sera pas sur la pile, il sera sur le tas dans la mémoire de l'objet conteneur.
Greg Beech
0

Je suppose que le hic ici est que vous essayez d'attribuer des sous-valeurs d'objet dans l'instruction plutôt que d'attribuer l'objet lui-même. Dans ce cas, vous devez affecter l'objet Point entier car le type de propriété est Point.

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

J'espère que j'ai eu du sens là-bas

MSIL
la source
2
Le point important est que Point est une structure (valeur type). S'il s'agissait d'une classe (objet), le code d'origine aurait fonctionné.
Hans Ke st ing
@HansKesting: S'il Points'agissait d'un type de classe mutable, le code d'origine aurait défini un champ ou une propriété Xdans l'objet retourné par propriété Origin. Je ne vois aucune raison de croire que cela aurait l'effet désiré sur l'objet contenant la Originpropriété. Certaines classes Framework ont ​​des propriétés qui copient leur état dans de nouvelles instances de classe mutable et les renvoient. Une telle conception a l'avantage de permettre au code comme thing1.Origin = thing2.Origin;de définir l'état d'origine de l'objet pour qu'il corresponde à celui d'un autre, mais il ne peut pas avertir du code comme thing1.Origin.X += 4;.
supercat
0

Supprimez simplement la propriété "get set" comme suit, puis tout fonctionne comme toujours.

Dans le cas des types primitifs, utilisez la commande 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
la source
0

Je pense que beaucoup de gens sont confus ici, ce problème particulier est lié à la compréhension que les propriétés de type valeur renvoient une copie du type valeur (comme avec les méthodes et les indexeurs) et que les champs de type valeur sont accessibles directement . Le code suivant fait exactement ce que vous essayez d'obtenir en accédant directement au champ de sauvegarde de la propriété (note: exprimer une propriété sous sa forme verbeuse avec un champ de sauvegarde est l'équivalent d'une propriété automatique, mais présente l'avantage que dans notre code, nous pouvons accéder directement au champ de support):

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'erreur que vous obtenez est une conséquence indirecte de ne pas comprendre qu'une propriété renvoie une copie d'un type valeur. Si vous recevez une copie d'un type valeur et que vous ne l'assignez pas à une variable locale, toutes les modifications que vous apportez à cette copie ne peuvent jamais être lues et par conséquent, le compilateur génère cela comme une erreur car cela ne peut pas être intentionnel. Si nous affectons la copie à une variable locale, nous pouvons modifier la valeur de X, mais elle ne sera modifiée que sur la copie locale, ce qui corrige l'erreur de compilation, mais n'aura pas l'effet souhaité de modifier la propriété Origin. Le code suivant illustre cela, car l'erreur de compilation a disparu, mais l'assertion de débogage échouera:

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
    }
}
Mat
la source