Почему невозможно переопределить свойство только для получения и добавить установщик?[закрыто]

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

Вопрос

Почему следующий код на C # не разрешен:

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

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

CS0546 "Бетонный класс.Bar.set":не удается переопределить, поскольку 'BaseClass.Bar' не имеет переопределяемого средства доступа set

Это было полезно?

Решение

Потому что автор Baseclass явно объявил, что Bar должен быть свойством, доступным только для чтения.Для дериваций не имеет смысла разрывать этот контракт и переводить его на чтение-запись.

В этом вопросе я согласен с Microsoft.
Допустим, я начинающий программист, которому сказали кодировать на основе деривации базового класса.я пишу что-то, что предполагает, что Bar не может быть записан в (поскольку в Baseclass явно указано, что это свойство только для получения).Теперь, с вашим выводом, мой код может сломаться.например ,

public class BarProvider
{ BaseClass _source;
  Bar _currentBar;

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

  public Bar getBar()
  { return _currentBar;  }
}

Поскольку Bar не может быть установлен в соответствии с интерфейсом BaseClass, BarProvider предполагает, что кэширование является безопасным, поскольку Bar нельзя изменить.Но если бы set был возможен при деривации, этот класс мог бы обслуживать устаревшие значения, если бы кто-то изменил свойство Bar объекта _source извне.Дело в том, что 'Будьте открыты, избегайте совершать подлые поступки и удивлять людей'

Обновить: Илья Рыженков спрашивает: "Тогда почему интерфейсы не играют по тем же правилам?" Хм..по мере того, как я думаю об этом, ситуация становится все более запутанной.
Интерфейс - это контракт, в котором говорится: "ожидайте, что реализация будет иметь свойство чтения с именем Bar". Лично У меня гораздо меньше шансов сделать такое предположение о доступности только для чтения, если я увижу Интерфейс.Когда я вижу свойство только для получения в интерфейсе, я читаю его как "Любая реализация предоставила бы эту панель атрибутов"...в базовом классе он щелкает как "Bar - это свойство, доступное только для чтения".Конечно, технически вы не нарушаете контракт..ты делаешь больше.Так что в каком-то смысле вы правы..В заключение я бы сказал: "сделайте так, чтобы возникло как можно больше недоразумений".

Другие советы

Я думаю, основная причина просто в том, что синтаксис слишком явный, чтобы это работало каким-либо другим способом.Этот код:

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

совершенно очевидно, что оба get и тот set являются переопределениями.Там нет никакого set в базовом классе, поэтому компилятор жалуется.Точно так же, как вы не можете переопределить метод, который не определен в базовом классе, вы также не можете переопределить установщик.

Вы могли бы сказать, что компилятор должен угадать ваше намерение и применить переопределение только к методу, который может быть переопределен (т. Е.получатель в данном случае), но это противоречит одному из принципов проектирования C # - компилятор не должен угадывать ваши намерения, потому что он может ошибиться без вашего ведома.

Я думаю, что следующий синтаксис мог бы подойти, но, как продолжает говорить Эрик Липперт, реализация даже такой незначительной функции, как эта, по-прежнему требует значительных усилий...

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

или для автоматически реализуемых свойств,

public int MyProperty { override get; set; }

Сегодня я наткнулся на ту же проблему, и я думаю, что у меня есть очень веская причина хотеть этого.

Сначала я хотел бы возразить, что наличие свойства "только для получения" не обязательно переводится в "только для чтения".Я интерпретирую это как "Из этого интерфейса / abtract вы можете получить это значение", это не означает, что какой-либо реализации этого интерфейса / абстрактного класса не потребуется, чтобы пользователь / программа явно устанавливали это значение.Абстрактные классы служат для реализации части необходимой функциональности.Я не вижу абсолютно никаких причин, по которым унаследованный класс не мог бы добавить установщик без нарушения каких-либо контрактов.

Ниже приведен упрощенный пример того, что мне было нужно сегодня.В итоге мне пришлось добавить сеттер в свой интерфейс, просто чтобы обойти это.Причина добавления установщика и не добавления, скажем, метода SetProp заключается в том, что одна конкретная реализация интерфейса использовала DataContract / DataMember для сериализации Prop, что было бы излишне усложнено, если бы мне пришлось добавлять другое свойство только с целью сериализации.

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

Я знаю, что это всего лишь сообщение "мои 2 цента", но я чувствую, что оригинальный постер и попытка обосновать, что это хорошая вещь, кажутся мне странными, особенно учитывая, что те же ограничения не применяются при наследовании непосредственно из интерфейса.

Также упоминание об использовании new вместо override здесь неприменимо, это просто не работает, и даже если бы это сработало, это не дало бы вам желаемого результата, а именно виртуального геттера, как описано в интерфейсе.

Это возможно

tl;dr- Вы можете переопределить метод только для получения с помощью установщика, если хотите.По сути, это просто:

  1. Создать new свойство, которое обладает как get и еще set используя то же имя.

  2. Если вы больше ничего не будете делать, то старый get метод по-прежнему будет вызываться, когда производный класс вызывается через его базовый тип.Чтобы исправить это, добавьте abstract промежуточный слой, использующий override на старом get метод, позволяющий заставить его вернуть новый get результат метода.

Это позволяет нам переопределять свойства с помощью get/set даже если им не хватало такового в их базовом определении.

В качестве бонуса вы также можете изменить тип возвращаемого значения, если хотите.

  • Если базовое определение было get-только тогда вы можете использовать более производный возвращаемый тип.

  • Если базовое определение было set-только тогда вы можете использовать менее производный возвращаемый тип.

  • Если базовое определение уже было get/set, тогда:

    • вы можете использовать более производный возвращаемый тип если ты делаешь это set-только;

    • вы можете использовать менее производный возвращаемый тип если ты делаешь это get-только.

Во всех случаях вы можете сохранить один и тот же возвращаемый тип, если хотите.В приведенных ниже примерах для простоты используется тот же возвращаемый тип.

Ситуация:Ранее существовавший get-только собственность

У вас есть некоторая структура класса, которую вы не можете изменить.Возможно, это всего лишь один класс, или это уже существующее дерево наследования.В любом случае, вы хотите добавить set метод для свойства, но не может.

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

Проблема:Не могу override тот самый get-только с get/set

Ты хочешь override с помощью get/set свойство, но оно не будет компилироваться.

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

Решение:Используйте abstract промежуточный слой

В то время как вы не можете напрямую override с помощью get/set собственность, вы может:

  1. Создать new get/set свойство с тем же именем.

  2. override старый get метод с средством доступа к новому get метод обеспечения согласованности.

Итак, сначала вы пишете 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; }
}

Затем вы пишете класс, который не компилировался ранее.На этот раз он будет скомпилирован, потому что на самом деле вы не override'ввод в действие get-только собственность;вместо этого вы заменяете его с помощью 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; } }
}

Результат:Все работает!

Очевидно, что это работает по назначению для 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.

При просмотре все по-прежнему работает так, как задумано D как один из его базовых классов, например A или B.Но причина, по которой это работает, может быть немного менее очевидной.

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.

Однако определение базового класса get по-прежнему в конечном счете переопределяется определением производного класса get, так что это по-прежнему полностью непротиворечиво.

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.

Обсуждение

Этот метод позволяет вам добавить set методы для get-только недвижимость.Вы также можете использовать его для выполнения таких вещей, как:

  1. Измените любое свойство на get-только, set-только, или get-и-set свойство, независимо от того, каким оно было в базовом классе.

  2. Измените возвращаемый тип метода в производных классах.

Основные недостатки заключаются в том, что требуется больше кодирования и дополнительная abstract class в дереве наследования.Это может немного раздражать конструкторы, которые принимают параметры, потому что они должны быть скопированы / вставлены в промежуточный слой.

Я согласен, что невозможность переопределить геттер в производном типе является антишаблоном.Только для чтения указывает на отсутствие реализации, а не на контракт с чистым функционалом (подразумевается ответом с наибольшим количеством голосов).

Я подозреваю, что у Microsoft было это ограничение либо потому, что пропагандировалось то же самое заблуждение, либо, возможно, из-за упрощения грамматики;хотя, теперь, когда область видимости может быть применена для получения или установки по отдельности, возможно, мы можем надеяться, что переопределение тоже возможно.

Неправильное представление, указанное ответом с наибольшим количеством голосов, о том, что свойство, доступное только для чтения, должно каким-то образом быть более "чистым", чем свойство чтения / записи, нелепо.Просто посмотрите на многие распространенные свойства фреймворка, доступные только для чтения;значение не является константой / чисто функциональным;например, DateTime.Now доступно только для чтения, но не является чисто функциональным значением.Попытка "кэшировать" значение свойства, доступного только для чтения, предполагая, что в следующий раз оно вернет то же значение, рискованна.

В любом случае, я использовал одну из следующих стратегий, чтобы преодолеть это ограничение;оба варианта далеки от совершенства, но позволят вам преодолеть этот языковой дефицит:

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

Возможно, вы могли бы обойти проблему, создав новое свойство:

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

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

Я могу понять все ваши замечания, но фактически автоматические свойства C # 3.0 в этом случае становятся бесполезными.

Ты не можешь сделать ничего подобного:

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

ИМО, C # не должен ограничивать такие сценарии.Ответственность за его соответствующее использование лежит на разработчике.

Проблема в том, что по какой-то причине Microsoft решила, что должно быть три различных типа свойств:доступны только для чтения, записи и read-write, только один из которых может существовать с заданной сигнатурой в данном контексте;свойства могут быть переопределены только идентично объявленными свойствами.Чтобы сделать то, что вы хотите, было бы необходимо создать два свойства с одинаковым именем и подписью, одно из которых было доступно только для чтения, а другое - для чтения-записи.

Лично я хотел бы, чтобы вся концепция "свойств" могла быть отменена, за исключением того, что синтаксис свойств можно было бы использовать в качестве синтаксического сахара для вызова методов "get" и "set".Это не только упростило бы использование опции "добавить набор", но также позволило бы "get" возвращать тип, отличный от "set".Хотя такая возможность использовалась бы не очень часто, иногда было бы полезно, чтобы метод 'get' возвращал объект-оболочку, в то время как 'set' мог принимать либо оболочку, либо фактические данные.

Вот обходной путь для достижения этой цели с помощью отражения:

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

Таким образом, мы можем установить значение объекта для свойства, доступного только для чтения.Однако это может сработать не во всех сценариях!

Вам следует изменить заголовок вашего вопроса, либо указав, что ваш вопрос касается исключительно переопределения абстрактного свойства, либо что ваш вопрос касается общего переопределения свойства класса, доступного только для получения.


Если первое (переопределение абстрактного свойства)

Этот код бесполезен.Сам по себе базовый класс не должен сообщать вам, что вы вынуждены переопределять свойство, доступное только для получения (возможно, Интерфейс).Базовый класс предоставляет общую функциональность, которая может потребовать определенных входных данных от реализующего класса.Следовательно, общая функциональность может вызывать абстрактные свойства или методы.В данном случае общие функциональные методы должны запрашивать у вас переопределение абстрактного метода, такого как:

public int GetBar(){}

Но если у вас нет контроля над этим, и функциональность базового класса считывается из его собственного общедоступного свойства (странно), тогда просто сделайте это:

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

Я хочу обратить внимание на (странный) комментарий:Я бы сказал, что наилучшей практикой для класса является не использовать свои собственные общедоступные свойства, а использовать свои частные / защищенные поля, когда они существуют.Так что это лучший шаблон:

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

Если последнее (переопределение свойства класса, доступного только для получения)

Каждое неабстрактное свойство имеет установщик.В противном случае он бесполезен, и вам не стоит им пользоваться.Microsoft не обязана разрешать вам делать то, что вы хотите.Причина в том, что:установщик существует в той или иной форме, и вы можете выполнить то, что хотите Вееррийи запросто.

Базовый класс или любой класс, в котором вы можете прочитать свойство с помощью {get;}, имеет НЕКОТОРЫЕ своего рода открытый установщик для этого свойства.Метаданные будут выглядеть следующим образом:

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

Но реализация будет иметь два конца спектра сложности:

Наименее Сложный:

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

Самый Сложный:

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

Я хочу сказать, что в любом случае у вас есть разоблаченный сеттер.В вашем случае вы можете переопределить int Bar потому что вы не хотите, чтобы базовый класс обрабатывал это, у вас нет доступа к просмотру того, как он это обрабатывает, или вам было поручено быстро и грязно взломать какой-то код против вашей воли.

Как во Втором, так и в Первом случае (Заключение)

Длинная-Короткая история:Microsoft не обязательно что-либо менять.Вы можете выбрать, как настроен ваш реализующий класс, и без конструктора использовать весь базовый класс или ни один из него.

Решение только для небольшого подмножества вариантов использования, но, тем не менее,:в C # 6.0 параметр "только для чтения" автоматически добавляется для переопределенных свойств, доступных только для получения.

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

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

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

Потому что на уровне IL свойство чтения / записи преобразуется в два метода (getter и setter).

При переопределении вы должны продолжать поддерживать базовый интерфейс.Если бы вы могли добавить сеттер, вы бы фактически добавили новый метод, который оставался бы невидимым для внешнего мира, насколько это касается интерфейса ваших классов.

Правда, добавление нового метода само по себе не нарушило бы совместимость, но поскольку оно оставалось бы скрытым, решение запретить это имеет смысл.

Потому что это нарушило бы концепцию инкапсуляции и скрытия реализации.Рассмотрим случай, когда вы создаете класс, отправляете его, а затем потребитель вашего класса получает возможность установить свойство, для которого вы изначально предоставляете только средство получения.Это эффективно нарушило бы любые инварианты вашего класса, на которые вы можете положиться в своей реализации.

Потому что класс, который имеет свойство только для чтения (без установщика), вероятно, имеет для этого вескую причину.Например, может отсутствовать какое-либо базовое хранилище данных.Разрешение вам создать установщик нарушает контракт, установленный классом.Это просто плохое ООП.

Свойство, доступное только для чтения в базовом классе, указывает, что это свойство представляет значение, которое всегда может быть определено изнутри класса (например, значение enum, соответствующее контексту объекта (db-)).Таким образом, ответственность за определение значения остается в пределах класса.

Добавление установщика вызвало бы здесь неловкую проблему:Ошибка проверки должна возникнуть, если вы установите значение, отличное от единственного возможного значения, которое у него уже есть.

Однако у правил часто есть исключения.Вполне возможно, что, например, в одном производном классе контекст сужает возможные значения enum до 3 из 10, но пользователю этого объекта все равно нужно решить, какое из них правильное.Производный класс должен делегировать ответственность за определение значения пользователю этого объекта.Важно осознавать это пользователь этого объекта должен быть хорошо осведомлен об этом исключении и возьмите на себя ответственность установить правильное значение.

Моим решением в подобных ситуациях было бы оставить свойство доступным только для чтения и добавить новое свойство чтения-записи в производный класс для поддержки исключения.Переопределение исходного свойства просто вернет значение нового свойства.Новое свойство может иметь собственное имя, правильно указывающее контекст этого исключения.

Это также подтверждает правильность замечания:"сделайте так, чтобы возникло как можно больше недоразумений" Гишу.

В этом нет ничего невозможного.Вам просто нужно использовать ключевое слово "new" в вашем свойстве.Например,

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

Похоже, что такой подход удовлетворяет обе стороны этого обсуждения.Использование "new" разрывает контракт между реализацией базового класса и реализацией подкласса.Это необходимо, когда класс может иметь несколько контрактов (либо через интерфейс, либо через базовый класс).

Надеюсь, это поможет

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top