Pourquoi est-il impossible de remplacer une propriété en lecture seule et d'ajouter un séparateur? [fermé]

StackOverflow https://stackoverflow.com/questions/82437

Question

Pourquoi le code C # suivant n'est-il pas autorisé:

public abstract class BaseClass
{
    public abstract int Bar { get;}
}

public class ConcreteClass : BaseClass
{
    public override int Bar
    {
        get { return 0; }
        set {}
    }
}
  

CS0546 'ConcreteClass.Bar.set': impossible de remplacer car 'BaseClass.Bar' ne dispose pas d'un accesseur défini pouvant être remplacé

Était-ce utile?

La solution

Parce que l'auteur de Baseclass a explicitement déclaré que Bar doit être une propriété en lecture seule. Cela n'a pas de sens que les dérivations rompent ce contrat et le rendent en lecture-écriture.

Je suis avec Microsoft à ce sujet.
Disons que je suis un nouveau programmeur à qui il a été demandé de coder contre la dérivation Baseclass. J'écris quelque chose qui suppose que Bar ne peut pas être écrit (puisque la classe de base indique explicitement qu'il s'agit d'une propriété à obtenir uniquement). Maintenant, avec votre dérivation, mon code peut casser. par exemple

public class BarProvider
{ BaseClass _source;
  Bar _currentBar;

  public void setSource(BaseClass b)
  {
    _source = b;
    _currentBar = b.Bar;
  }

  public Bar getBar()
  { return _currentBar;  }
}

Etant donné que Bar ne peut pas être défini conformément à l'interface BaseClass, BarProvider suppose que la mise en cache est une opération sûre, car Bar ne peut pas être modifié. Mais si set était possible dans une dérivation, cette classe pourrait servir des valeurs périmées si quelqu'un modifiait la propriété Bar de l'objet _source en externe. " Soyez ouvert, évitez de faire des choses sournoises et de surprendre les gens "

Mise à jour : Ilya Ryzhenkov demande "Pourquoi les interfaces ne fonctionnent-elles pas selon les mêmes règles alors?" Hmm .. ça devient plus boueux quand j'y pense.
Une interface est un contrat qui dit "attendez d'une implémentation une propriété de lecture nommée Bar". Personnellement , je suis beaucoup moins susceptible de faire cette hypothèse de lecture seule si je voyais une interface. Lorsque je vois une propriété get-only sur une interface, je la lis comme suit: "Toute implémentation exposerait cet attribut Bar" ... sur une classe de base, il clique sur le bouton "Bar est une propriété en lecture seule". Bien sûr, techniquement, vous ne rompez pas le contrat… vous en faites plus. En un sens, vous avez donc raison. Je terminerais en disant: "évitez autant que possible les malentendus".

Autres conseils

Je pense que la raison principale est simplement que la syntaxe est trop explicite pour que cela fonctionne autrement. Ce code:

public override int MyProperty { get { ... } set { ... } }

est assez explicite sur le fait que le get et le set sont des remplacements. Il n'y a pas de set dans la classe de base, le compilateur se plaint donc. Tout comme vous ne pouvez pas remplacer une méthode qui n'est pas définie dans la classe de base, vous ne pouvez pas non plus remplacer un setter.

Vous pourriez dire que le compilateur devrait deviner votre intention et appliquer uniquement le remplacement à la méthode qui peut être remplacée (c'est-à-dire le getter dans ce cas), mais cela va à l'encontre de l'un des principes de conception C #, à savoir que le compilateur ne doit pas devinez vos intentions, car il se peut que vous vous trompiez sans le savoir

Je pense que la syntaxe suivante pourrait bien fonctionner, mais comme le répète Eric Lippert, implémenter même une fonctionnalité mineure comme celle-ci demande toujours beaucoup d'efforts ...

public int MyProperty
{
    override get { ... }
    set { ... }
}

ou, pour les propriétés auto-implémentées,

public int MyProperty { override get; set; }

Je suis tombé sur le même problème aujourd'hui et je pense avoir une raison très valable de vouloir cela.

Tout d'abord, j'aimerais dire que le fait d'avoir une propriété get-only ne se traduit pas nécessairement en lecture seule. Je l’interprète comme "de cette interface / abstraction, vous pouvez obtenir cette valeur", cela ne signifie pas qu’une implémentation de cette classe / interface abstraite n’aura pas besoin de l’utilisateur / du programme pour définir explicitement cette valeur. Les classes abstraites servent à implémenter une partie de la fonctionnalité requise. Je ne vois absolument aucune raison pour laquelle une classe héritée ne pourrait pas ajouter un séparateur sans violer aucun contrat.

Ce qui suit est un exemple simplifié de ce dont j'avais besoin aujourd'hui. J'ai fini par avoir à ajouter un setter dans mon interface pour contourner le problème. La raison pour laquelle ajouter le setter et ne pas ajouter, par exemple, une méthode SetProp est qu'une implémentation particulière de l'interface utilisait DataContract / DataMember pour la sérialisation de Prop, ce qui aurait été compliqué inutilement si je devais ajouter une autre propriété uniquement dans le but de la sérialisation.

interface ITest
{
    // Other stuff
    string Prop { get; }
}

// Implements other stuff
abstract class ATest : ITest
{
    abstract public string Prop { get; }
}

// This implementation of ITest needs the user to set the value of Prop
class BTest : ATest
{
    string foo = "BTest";
    public override string Prop
    {
        get { return foo; }
        set { foo = value; } // Not allowed. 'BTest.Prop.set': cannot override because 'ATest.Prop' does not have an overridable set accessor
    }
}

// This implementation of ITest generates the value for Prop itself
class CTest : ATest
{
    string foo = "CTest";
    public override string Prop
    {
        get { return foo; }
        // set; // Not needed
    }
}

Je sais que c’est juste un "mes 2 cents" poster, mais j’ai le sentiment avec l’affiche originale et essayer de rationaliser le fait que c’est une bonne chose me semble étrange, surtout si l’on considère que les mêmes limitations ne s’appliquent pas lorsqu’on hérite directement d’une interface.

De plus, la mention sur l'utilisation de new au lieu de override ne s'applique pas ici, cela ne fonctionne tout simplement pas et même si cela ne fonctionnait pas, le résultat souhaité ne serait pas obtenu, à savoir un getter virtuel tel que décrit par l'interface.

C'est possible

tl; dr - Si vous le souhaitez, vous pouvez remplacer une méthode get-only par un paramètre set. C'est fondamentalement juste:

  1. Créez une propriété new comportant à la fois un get et un set utilisant le même nom.

  2. Si vous ne faites rien d'autre, l'ancienne méthode get sera toujours appelée lorsque la classe dérivée sera appelée via son type de base. Pour résoudre ce problème, ajoutez une couche intermédiaire abstract qui utilise override sur l'ancienne méthode get pour le forcer à renvoyer le nouveau get résultat de la méthode.

Cela nous permet de remplacer les propriétés avec get / set même s'il en manquait un dans leur définition de base.

En prime, vous pouvez également modifier le type de retour si vous le souhaitez.

  • Si la définition de base était get -only, vous pouvez utiliser un type de retour plus dérivé.

  • Si la définition de base était set -only, vous pouvez nous utiliser un type de retour moins dérivé.

  • Si la définition de base était déjà get / set , alors:

    • vous pouvez utiliser un type de retour plus dérivé si vous le définissez set -only;

    • vous pouvez utiliser un type de retour moins dérivé si vous le faites obtenir -only.

Dans tous les cas, vous pouvez conserver le même type de retour si vous le souhaitez. Les exemples ci-dessous utilisent le même type de retour pour plus de simplicité.

Situation: propriété préexistante -only

Vous avez une structure de classe que vous ne pouvez pas modifier. Peut-être que c'est juste une classe, ou c'est un arbre d'héritage préexistant. Quoi qu’il en soit, vous voulez ajouter une méthode set à une propriété, mais vous ne pouvez pas.

public abstract class A                     // Pre-existing class; can't modify
{
    public abstract int X { get; }          // You want a setter, but can't add it.
}
public class B : A                          // Pre-existing class; can't modify
{
    public override int X { get { return 0; } }
}

Problème: ne peut pas remplacer le obtenir -only avec obtenir / définir

Vous voulez remplacer par une propriété obtenir / set , mais cela ne compilera pas.

public class C : B
{
    private int _x;
    public override int X
    {
        get { return _x; }
        set { _x = value; }   //  Won't compile
    }
}

Solution: utiliser une couche intermédiaire abstract

.

Bien que vous ne puissiez pas directement remplacer avec une propriété obtenir / set , vous pouvez :

  1. Créez une nouvelle obtenez une / ensemble de même nom.

  2. substitue l'ancienne méthode get avec un accesseur à la nouvelle méthode get pour assurer la cohérence.

Donc, vous écrivez d’abord la couche intermédiaire abstract :

public abstract class C : B
{
    //  Seal off the old getter.  From now on, its only job
    //  is to alias the new getter in the base classes.
    public sealed override int X { get { return this.XGetter; }  }
    protected abstract int XGetter { get; }
}

Ensuite, vous écrivez la classe qui ne compilerait pas plus tôt. Il sera compilé cette fois car vous n'êtes pas réellement écrasé en utilisant la propriété get -only; au lieu de cela, vous le remplacez à l'aide du mot clé new .

public class D : C
{
    private int _x;
    public new virtual int X { get { return this._x; } set { this._x = value; } }

    //  Ensure base classes (A,B,C) use the new get method.
    protected sealed override int XGetter { get { return this.X; } }
}

Résultat: tout fonctionne!

Évidemment, cela fonctionne comme prévu pour D .

var test = new D();
Print(test.X);      // Prints "0", the default value of an int.

test.X = 7;
Print(test.X);      // Prints "7", as intended.

Tout fonctionne toujours comme prévu lorsque vous affichez D comme l'une de ses classes de base, par exemple. A ou B . Mais la raison pour laquelle cela fonctionne peut être un peu moins évident.

var test = new D() as B;
//test.X = 7;       // This won't compile, because test looks like a B,
                    // and B still doesn't provide a visible setter.

Cependant, la définition de la classe de base de get est toujours remplacée par la définition de la classe dérivée de get , elle est donc toujours parfaitement cohérente.

var test = new D();
Print(test.X);      // Prints "0", the default value of an int.

var baseTest = test as A;
Print(test.X);      // Prints "7", as intended.

Discussion

Cette méthode vous permet d'ajouter des méthodes set à des propriétés get -only. Vous pouvez également l'utiliser pour faire des choses comme:

  1. Modifiez n'importe quelle propriété en get -only, set -only ou get -et- set , indépendamment de ce qu’il était dans une classe de base.

  2. Changez le type de retour d'une méthode dans les classes dérivées.

Les principaux inconvénients sont qu’il ya plus de codage à faire et une classe abstraite supplémentaire dans l’arbre de l’héritage. Cela peut être un peu gênant pour les constructeurs qui prennent des paramètres car ceux-ci doivent être copiés / collés dans la couche intermédiaire.

Je conviens que le fait de ne pas pouvoir ignorer un getter dans un type dérivé est un anti-motif. Lecture seule spécifie l'absence d'implémentation, pas un contrat purement fonctionnel (sous-entendu par la réponse du vote en haut).

Je soupçonne que Microsoft avait cette limitation soit parce que la même idée fausse avait été promue, soit peut-être à cause de la simplification de la grammaire; Cependant, maintenant que cette portée peut être appliquée pour obtenir ou définir individuellement, nous pouvons peut-être espérer que la substitution le sera aussi.

L'idée fausse indiquée par la réponse du vote en haut, selon laquelle une propriété en lecture seule devrait en quelque sorte être plus "pure" qu'une propriété en lecture / écriture est ridicule. Il suffit de regarder de nombreuses propriétés en lecture seule courantes dans la structure; la valeur n'est pas une constante / purement fonctionnelle; Par exemple, DateTime.Now est en lecture seule, mais n’a rien d’une valeur fonctionnelle pure. Vouloir "mettre en cache" la valeur d’une propriété en lecture seule en supposant que la même valeur sera renvoyée la prochaine fois est risqué.

Dans tous les cas, j’ai utilisé l’une des stratégies suivantes pour surmonter cette limitation; les deux sont moins parfaits, mais vous permettront de boiter au-delà de cette déficience linguistique:

   class BaseType
   {
      public virtual T LastRequest { get {...} }
   }

   class DerivedTypeStrategy1
   {
      /// get or set the value returned by the LastRequest property.
      public bool T LastRequestValue { get; set; }

      public override T LastRequest { get { return LastRequestValue; } }
   }

   class DerivedTypeStrategy2
   {
      /// set the value returned by the LastRequest property.
      public bool SetLastRequest( T value ) { this._x = value; }

      public override T LastRequest { get { return _x; } }

      private bool _x;
   }

Vous pourriez peut-être contourner le problème en créant une nouvelle propriété:

public new int Bar 
{            
    get { return 0; }
    set {}        
}

int IBase.Bar { 
  get { return Bar; }
}

Je peux comprendre tous vos arguments, mais effectivement, les propriétés automatiques de C # 3.0 deviennent inutiles dans ce cas.

Vous ne pouvez rien faire de pareil:

public class ConcreteClass : BaseClass
{
    public override int Bar
    {
        get;
        private set;
    }
}

IMO, C # ne devrait pas restreindre de tels scénarios. Il incombe au développeur de l'utiliser en conséquence.

Le problème est que, quelle que soit la raison, Microsoft a décidé de créer trois types de propriétés distincts: lecture seule, écriture seule et lecture-écriture, un seul d'entre eux pouvant exister avec une signature donnée dans un contexte donné; les propriétés ne peuvent être remplacées que par des propriétés déclarées de manière identique. Pour faire ce que vous voulez, il serait nécessaire de créer deux propriétés avec le même nom et la même signature, l’une en lecture seule et l’autre en lecture-écriture.

Personnellement, je souhaite que tout le concept de "propriétés" pourrait être aboli, sauf que la syntaxe property-ish pourrait être utilisée comme sucre syntaxique pour appeler "get". et " set " méthodes. Cela faciliterait non seulement l'option 'add set', mais permettrait également à 'get' de retourner un type différent de 'set'. Bien qu'une telle capacité ne soit pas très souvent utilisée, il peut parfois être utile de renvoyer un objet wrapper par une méthode 'get', tandis que le 'set' peut accepter un wrapper ou des données réelles.

Voici un moyen de contourner ce problème en utilisant Reflection:

var UpdatedGiftItem = // object value to update;

foreach (var proInfo in UpdatedGiftItem.GetType().GetProperties())
{
    var updatedValue = proInfo.GetValue(UpdatedGiftItem, null);
    var targetpropInfo = this.GiftItem.GetType().GetProperty(proInfo.Name);
    targetpropInfo.SetValue(this.GiftItem, updatedValue,null);
}

De cette façon, nous pouvons définir la valeur de l'objet sur une propriété en lecture seule. Pourrait ne pas fonctionner dans tous les scénarios cependant!

Vous devez modifier le titre de votre question pour indiquer soit que votre question concerne uniquement la substitution d'une propriété abstraite, soit que votre question concerne la substitution générale de la propriété get-only d'une classe.

Si l'ancien (remplacement d'une propriété abstraite)

Ce code est inutile. Une classe de base ne devrait pas à elle seule vous dire que vous êtes obligé de remplacer une propriété Get-Only (peut-être une interface). Une classe de base fournit des fonctionnalités communes pouvant nécessiter une entrée spécifique d'une classe d'implémentation. Par conséquent, la fonctionnalité commune peut appeler des propriétés ou des méthodes abstraites. Dans le cas donné, les méthodes de fonctionnalité communes devraient vous demander de remplacer une méthode abstraite telle que:

public int GetBar(){}

Mais si vous n'avez aucun contrôle sur cela et que les fonctionnalités de la classe de base sont lues à partir de sa propre propriété publique (bizarre), procédez comme suit:

public abstract class BaseClass
{
    public abstract int Bar { get; }
}

public class ConcreteClass : BaseClass
{
    private int _bar;
    public override int Bar
    {
        get { return _bar; }
    }
    public void SetBar(int value)
    {
        _bar = value;
    }
}

Je tiens à souligner le commentaire (étrange): je dirais qu'une bonne pratique consiste pour une classe à ne pas utiliser ses propres propriétés publiques, mais à utiliser ses champs privés / protégés lorsqu'ils existent. Donc, ceci est un meilleur modèle:

public abstract class BaseClass {
    protected int _bar;
    public int Bar { get { return _bar; } }
    protected void DoBaseStuff()
    {
        SetBar();
        //Do something with _bar;
    }
    protected abstract void SetBar();
}

public class ConcreteClass : BaseClass {
    protected override void SetBar() { _bar = 5; }
}

Si ce dernier (remplacement de la propriété get-only d'une classe)

Chaque propriété non abstraite a un séparateur. Sinon, c'est inutile et vous ne devriez pas vous soucier de l'utiliser. Microsoft n'a pas à vous permettre de faire ce que vous voulez. Raison d'être: le passeur existe sous une forme ou une autre et vous pouvez accomplir ce que vous voulez Veerryy facilement.

La classe de base, ou toute classe dans laquelle vous pouvez lire une propriété avec {get;} , a CERTAINES sortes de sélecteurs exposés pour cette propriété. Les métadonnées ressembleront à ceci:

public abstract class BaseClass
{
    public int Bar { get; }
}

Mais la mise en œuvre aura deux extrémités du spectre de complexité:

moins complexe:

public abstract class BaseClass
{
    private int _bar;
    public int Bar { 
        get{
            return _bar;
        }}
    public void SetBar(int value) { _bar = value; }
}

Plus complexe:

public abstract class BaseClass
{
    private int _foo;
    private int _baz;
    private int _wtf;
    private int _kthx;
    private int _lawl;

    public int Bar
    {
        get { return _foo * _baz + _kthx; }
    }
    public bool TryDoSomethingBaz(MyEnum whatever, int input)
    {
        switch (whatever)
        {
            case MyEnum.lol:
                _baz = _lawl + input;
                return true;
            case MyEnum.wtf:
                _baz = _wtf * input;
                break;
        }
        return false;
    }
    public void TryBlowThingsUp(DateTime when)
    {
        //Some Crazy Madeup Code
        _kthx = DaysSinceEaster(when);
    }
    public int DaysSinceEaster(DateTime when)
    {
        return 2; //<-- calculations
    }
}
public enum MyEnum
{
    lol,
    wtf,
}

Mon point de vue étant, de toute façon, vous avez le passeur exposé. Dans votre cas, vous voudrez peut-être remplacer int Bar car vous ne voulez pas que la classe de base le gère, vous n'avez pas le droit de voir comment elle est traitée, ou vous êtes chargé de manipuler du code. vrai quick'n'dirty contre votre volonté.

À la fois tard et ancien (Conclusion)

Long-Story Short: Microsoft n'a pas à changer quoi que ce soit. Vous pouvez choisir le mode de configuration de votre classe d'implémentation et, sans le constructeur, utiliser tout ou rien de la classe de base.

Solution pour seulement un petit sous-ensemble de cas d'utilisation, mais néanmoins: en C # 6.0 "readonly" setter est automatiquement ajouté pour les propriétés getter-only remplacées.

public abstract class BaseClass
{
    public abstract int Bar { get; }
}

public class ConcreteClass : BaseClass
{
    public override int Bar { get; }

    public ConcreteClass(int bar)
    {
        Bar = bar;
    }
}

Parce qu'au niveau IL, une propriété en lecture / écriture se traduit par deux méthodes (getter et setter).

Lors du remplacement, vous devez continuer à prendre en charge l'interface sous-jacente. Si vous pouviez ajouter un setter, vous ajouteriez effectivement une nouvelle méthode, qui resterait invisible pour le monde extérieur, en ce qui concerne l'interface de vos classes.

Certes, l’ajout d’une nouvelle méthode ne constituerait pas une rupture de la compatibilité en soi, mais comme elle resterait cachée, il est parfaitement logique d’annuler cette interdiction.

Parce que cela briserait le concept d’encapsulation et de dissimulation de mise en œuvre. Considérez le cas lorsque vous créez une classe, envoyez-la, puis le consommateur de votre classe se permet de définir une propriété pour laquelle vous ne fournissez à l'origine qu'un getter. Cela perturberait efficacement les invariants de votre classe sur lesquels vous pouvez compter dans votre implémentation.

Parce qu'une classe qui a une propriété en lecture seule (pas de séparateur) a probablement une bonne raison de le faire. Par exemple, il pourrait ne pas y avoir de magasin de données sous-jacent. Vous permettre de créer un passeur rompt le contrat défini par la classe. C'est juste une mauvaise OOP.

Une propriété en lecture seule dans la classe de base indique que cette propriété représente une valeur qui peut toujours être déterminée à partir de la classe (par exemple, une valeur enum correspondant au contexte (db-) d'un objet). La responsabilité de déterminer la valeur reste donc dans la classe.

Ajouter un passeur causerait un problème délicat ici: Une erreur de validation doit survenir si vous définissez la valeur sur une valeur autre que la seule valeur possible déjà définie.

Les règles comportent souvent des exceptions. Il est très possible que, par exemple, dans une classe dérivée, le contexte réduise les valeurs d’énumération possibles à 3 sur 10, mais l’utilisateur de cet objet doit encore choisir celle qui est correcte. La classe dérivée doit déléguer la responsabilité de déterminer la valeur à l'utilisateur de cet objet. Il est important de comprendre que l'utilisateur de cet objet doit être parfaitement conscient de cette exception et assumer la responsabilité de définir la valeur correcte.

Dans ce type de situation, ma solution serait de laisser la propriété en lecture seule et d'ajouter une nouvelle propriété en lecture-écriture à la classe dérivée pour prendre en charge l'exception. Le remplacement de la propriété d'origine retournera simplement la valeur de la nouvelle propriété. La nouvelle propriété peut avoir un nom propre indiquant correctement le contexte de cette exception.

Cela confirme également la remarque valide: "évitez les malentendus et les incohérences possibles", par Gishu.

Ce n’est pas impossible. Vous devez simplement utiliser le " nouveau " mot-clé dans votre propriété. Par exemple,

namespace {
    public class Base {
        private int _baseProperty = 0;

        public virtual int BaseProperty {
            get {
                return _baseProperty;
            }
        }

    }

    public class Test : Base {
        private int _testBaseProperty = 5;

        public new int BaseProperty {
            get {
                return _testBaseProperty;
            }
            set {
                _testBaseProperty = value;
            }
        }
    }
}

Il semble que cette approche satisfasse les deux côtés de la discussion. Utilisation de " new " rompt le contrat entre l'implémentation de classe de base et l'implémentation de sous-classe. Cela est nécessaire lorsqu'une classe peut avoir plusieurs contrats (via une interface ou une classe de base).

J'espère que cela vous aidera

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top