Почему я не могу использовать виртуальные/переопределенные переменные класса, как я могу использовать методы?

В следующем примере я могу создать виртуальный метод Show() в унаследованном классе, а затем переопределить его в наследующем классе. сильный> класс.

Я хочу сделать то же самое с защищенной переменной класса prefix, но получаю сообщение об ошибке:

Модификатор «виртуальный» недействителен для этого предмета.

Но поскольку я не могу определить эту переменную как virtual/override в своих классах, я получаю предупреждение компилятора:

TestOverride234355.SecondaryTransaction.prefix скрывает унаследованный член TestOverride234355.Transaction.prefix. Используйте новое ключевое слово, если предполагалось скрытие.

К счастью, когда я добавляю ключевое слово new, все работает нормально, и это нормально, поскольку я получаю ту же функциональность, но возникает два вопроса:

  1. Почему я могу использовать virtual/override для методов, но не для защищенных переменных класса?

  2. Какая на самом деле разница между подходом виртуального/переопределения и подходом скрытия с новым, поскольку, по крайней мере, в этом примере они предлагают одинаковую функциональность?

Код:

using System;

namespace TestOverride234355
{
    public class Program
    {
        static void Main(string[] args)
        {
            Transaction st1 = new Transaction { Name = "name1", State = "state1" };
            SecondaryTransaction st2 = 
                new SecondaryTransaction { Name = "name1", State = "state1" };

            Console.WriteLine(st1.Show());
            Console.WriteLine(st2.Show());

            Console.ReadLine();
        }
    }

    public class Transaction
    {
        public string Name { get; set; }
        public string State { get; set; }

        protected string prefix = "Primary";

        public virtual string Show()
        {
            return String.Format("{0}: {1}, {2}", prefix, Name, State);
        }
    }

    public class SecondaryTransaction : Transaction
    {
        protected new string prefix = "Secondary";

        public override string Show()
        {
            return String.Format("{0}: {1}, {2}", prefix, Name, State);
        }
    }
}

person Edward Tanguay    schedule 04.03.2010    source источник
comment
В вашем вопросе говорится о переменных, но в заголовке говорится о свойствах. Это очень разные вещи. Я предлагаю вам отредактировать либо вопрос, либо (что более вероятно) заголовок.   -  person Jon Skeet    schedule 04.03.2010


Ответы (8)


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

В вашем случае вы можете установить prefix в конструкторе наследующего класса:

// Base class field declaration and constructor
protected string prefix;

public Transaction()
{
  prefix = "Primary";
}

// Child class constructor
public SecondaryTransaction()
{
  prefix = "Secondary";
}

Вы также можете создать свойство вместо поля и сделать свойство виртуальным. Это позволит вам изменить поведение геттера и сеттера для свойства в наследующем классе:

// Base class
public virtual string Prefix { get { /* ... */ } set { /* ... */ } }

// Child class
public override string Prefix { get { /* ... */ } set { /* ... */ } }

EDIT: Что касается вашего вопроса об использовании переменной в базовом конструкторе до того, как наследующий класс установил ее, один из способов решить эту проблему — определить метод инициализации в базовом классе, переопределить его в наследующем class и вызовите его из базового конструктора перед доступом к каким-либо полям:

// Base class
public class Base
{
  protected string prefix;

  public Base()
  {
    Initialize();
    Console.WriteLine(prefix);
  }  

  protected virtual void Initialize()
  {
    prefix = "Primary";
  }
}

// Inheriting class
public class Child : Base
{
  public override void Initialize()
  {
    prefix = "Secondary";
  }
}

РЕДАКТИРОВАТЬ 2: вы также спросили, в чем разница между виртуальным/переопределением и сокрытием имени (новое ключевое слово в методах), следует ли его избегать и может ли оно быть полезным.

Сокрытие имен — это функция, которая прерывает наследование в случае сокрытия виртуальных методов. Т.е., если скрыть метод Initialize() в дочернем классе, базовый класс его не увидит и не вызовет. Кроме того, если бы метод Initialize() был общедоступным, внешний код, который вызывал бы Initialize() для ссылки базового типа, вызывал бы Initialize() для базового типа.

Сокрытие имени полезно, когда метод не является виртуальным в базовом классе, а дочерний элемент хочет предоставить собственную другую реализацию. Обратите внимание, однако, что это НЕ то же самое, что виртуальный/переопределенный. Ссылки базового типа будут вызывать реализацию базового типа, а ссылки дочернего типа будут вызывать реализацию дочернего типа.

person Håvard S    schedule 04.03.2010
comment
правильно, но в моем фактическом коде я вызываю базовый конструктор унаследованного класса из конструктора моего наследующего класса (например, public SecondaryTransaction() : base() ), и базовый конструктор использует эту переменную, которая в этот момент не установлена. - person Edward Tanguay; 04.03.2010
comment
Это другой вопрос, чем тот, который вы представили здесь. Пожалуйста, задайте вопрос, на который вы хотите получить ответ. - person Håvard S; 04.03.2010
comment
@Edward, в таком случае, как насчет наличия абстрактного (и под абстрактным я имел в виду виртуальный) метода. Инициализируйте, который вы вызываете в ctor вашего базового класса, прежде чем делать что-либо, реализуйте его в своем дочернем классе и установить там значение? Я добавлю пример этого в свой ответ - person Rob; 04.03.2010
comment
@HåvardS да, теперь я понимаю, что этот момент немного меняет вопрос, то есть это причина, по которой мне в первую очередь нужно переопределить переменную класса. - person Edward Tanguay; 04.03.2010
comment
@Edward Также ответил на ваш вопрос о сокрытии имени и переопределении и предоставил пример использования. - person Håvard S; 04.03.2010
comment
@HåvardS, ваш последний абзац о скрытии с новым имеет смысл, то есть почему это не то же самое, что виртуальный / переопределенный (базовый класс будет вызывать свой собственный), спасибо, интересно. - person Edward Tanguay; 04.03.2010
comment
Фреймворк мог бы поддерживать переопределение полей, если бы существовал слот vtable, указывающий относительное смещение поля внутри типа. Однако при отсутствии множественного наследования не так много причин, по которым поле производного типа не должно занимать тот же адрес, что и поле базового типа. Самое большое, что может предоставить средство виртуального поля, — это возможность иметь интерфейс, предоставляющий поле, которое может быть передано как ref параметр (например, в Interlocked методы). Вероятно, недостаточно пользы, чтобы быть стоящей. Еще более крутая концепция, если бы ее разрешили... - person supercat; 12.08.2013
comment
... было бы средством для того, чтобы свойство выставляло значение lvalue по ссылке, особенно если можно было бы указать код, который должен запускаться после его изменения. - person supercat; 12.08.2013

Статический или невиртуальный метод или свойство — это просто адрес памяти (для упрощения). Виртуальный метод или свойство идентифицируется записью в таблице. Эта таблица зависит от класса, определяющего метод или свойство. Когда вы переопределяете виртуальный член в производном классе, вы фактически изменяете запись в таблице для производного класса, чтобы она указывала на переопределяющий метод. Во время выполнения доступ к такому элементу всегда осуществляется через таблицу. Таким образом, запись может быть переопределена любым производным классом.

Для полей такого механизма нет, поскольку они предназначены для быстрого доступа.

Использование «нового» для члена означает, что вы не хотите переопределять запись в таблице, но вам нужен новый член (с тем же именем, что и у существующего виртуального, плохая практика, если вы спросите меня).

Если вы получаете доступ к виртуальному члену через указатель на базовый класс, вы никогда не получите доступ к члену, определенному как «новый» в производном классе, что является разницей, упомянутой во второй части вашего вопроса.

person Timores    schedule 04.03.2010

Скорее создайте свойство для члена префикса - таким образом вы можете установить для свойства виртуальный/абстрактный

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

Вы также не можете помещать переменные экземпляра в интерфейс

person saret    schedule 04.03.2010
comment
Вы имеете в виду сделать его Getter (обычный метод), например, protected string GetPrefix() { return Secondary; }, да, это, вероятно, то, что я собираюсь сделать, который имеет ту же функциональность, но просто больше кода, просто интересно, какое преимущество он имеет по сравнению с приведенным выше примером скрытия с новым. - person Edward Tanguay; 04.03.2010
comment
Да, но вы всегда можете заставить родительский класс взять префикс в конструкторе (переданный по подтипу) и просто иметь для него один геттер в базовом классе - (отредактированный ответ, чтобы покрыть часть вопроса) - person saret; 04.03.2010
comment
хорошо, это заставляет меня задаться вопросом, почему вариант скрытия с новым вообще возможен, я чувствую, что тот факт, что он работал для меня в этой ситуации, заманил меня к написанию плохого кода (сокрытие внутренних унаследованных переменных класса), как вы описываете, это просто плохое использование hide-with-new или следует вообще избегать hide-with-new? - person Edward Tanguay; 04.03.2010
comment
@Edward: В C# я бы рекомендовал использовать свойства вместо обычных методов GetX. - person Svish; 04.03.2010
comment
Параметр скрытия с новым существует, чтобы подтипы могли снова использовать это имя переменной (даже если оно общедоступно в базовом классе) и заставлять любую ссылку на префикс ссылаться на собственный член префикса подтипов, а не на тот, который выставлен из базового класса. любой более производный подтип исходного подтипа, который определил новый префикс, будет ссылаться на новое определенное поле при использовании поля префикса, как и в поле префикса корневых базовых классов. - person saret; 04.03.2010
comment
@Svish, да, я понимаю, что вы имеете в виду, я поставил защищенный виртуальный List‹string› BlockMarkers { get; } в моем базовом классе и в Visual Studio, когда я переопределяю его в своем наследующем классе, он открывает раздел получения, где я могу определить свое свойство, это имеет смысл. - person Edward Tanguay; 04.03.2010

В вашем примере, если вы не переопределили "Show" в классе SecondaryTransaction, то вызов Show для экземпляра SecondaryTransaction фактически будет вызывать метод в базовом классе (Transaction), который, следовательно, будет использовать "Show" в базе класс, в результате чего выводится:

Primary: name1, state1 
Primary: name1, state1

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

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

Что бы я сделал (если бы я абсолютно не хотел/не мог использовать свойства):

public class Transaction
{
    public string Name { get; set; }
    public string State { get; set; }
    protected string prefix = "Primary";
    public virtual string Show()
    {
        return String.Format("{0}: {1}, {2}", prefix, Name, State);
    }
}
public class SecondaryTransaction : Transaction
{ 
    public SecondaryTransaction()
    {
        prefix = "Secondary";
    }
    public override string Show()
    {
        return String.Format("{0}: {1}, {2}", prefix, Name, State);
    }
}

Изменить: (согласно моему комментарию к другому ответу)

Если вы вызываете ctor вашего базового класса и вам нужно установить значение, вам, вероятно, придется изменить транзакцию, возможно, так:

public class Transaction
{
    public string Name { get; set; }
    public string State { get; set; }
    protected string prefix = "Primary";
    // Declared as virtual ratther than abstract to avoid having to implement "TransactionBase"
    protected virtual void Initialise()
    { }
    public Transaction()
    {
        Initialise();
    }
    public virtual string Show()
    {
        return String.Format("{0}: {1}, {2}", prefix, Name, State);
    }
}
public class SecondaryTransaction : Transaction
{ 
    protected override void Initialise()
    {
        prefix = "Secondary";
    }
    public override string Show()
    {
        return String.Format("{0}: {1}, {2}", prefix, Name, State);
    }
}
person Rob    schedule 04.03.2010
comment
хорошо, это поучительно, теперь я знаю, что в приведенном выше случае лучше всего создать метод получения свойств для каждой переменной, которую я хочу либо переопределить, либо оставить как есть в моих наследуемых классах. Я ДУМАЛ, что наконец-то нашел применение функции скрытия с новым в C#, но если такое ее использование — такая плохая практика, то я не понимаю, смогу ли я когда-либо использовать ее на практике. - person Edward Tanguay; 04.03.2010
comment
@ Эдвард, я так и не нашел ему применения. Это не значит, что его нет :) - person Rob; 04.03.2010

Почему вы хотите переопределить защищенную переменную, ведь все, что вам нужно сделать, это установить для нее что-то еще в переопределяющем классе (возможно, в конструкторе)?

person Paddy    schedule 04.03.2010
comment
Это довольно распространено: так же, как я наследую класс, а затем переопределяю определенные методы с пользовательской функциональностью, я также хочу наследовать класс и переопределять определенные переменные с определенными значениями. Делать каждую из этих маленьких переменных полными методами получения кажется излишним, если все, что они возвращают, является значением, и если у меня есть 20 из них, я хочу, чтобы они все были в блоке в верхней части класса, чтобы я мог просто переопределить те, которые мне нужны для определенный наследующий класс, не нужно работать с 20 методами получения, которые расширяют пару экранов. - person Edward Tanguay; 04.03.2010
comment
Если это так, то мне интересно, не нужно ли рассматривать ваш дизайн в целом - и, увидев некоторые другие комментарии, которые вы здесь сделали, я думаю, что это может иметь место. - person Paddy; 04.03.2010
comment
что я получил из этого, так это не скрывать переменные класса с помощью new, а использовать свойства С#, которые можно правильно установить как виртуальные и переопределить, имеет смысл - person Edward Tanguay; 04.03.2010

Вы не можете, потому что это бесполезно. Чего бы вы добились, переопределив поле?

person Bart van Heukelom    schedule 04.03.2010

Переопределение поля - это нонсенс. Отметив поля как защищенные, вы автоматически можете получить к ним доступ в производных классах. Вы можете переопределить функции, свойства, потому что он использует функции внутри.

person necrostaz    schedule 04.03.2010
comment
почему скрытие поля с новым возможно даже в С#, если это такая плохая практика? - person Edward Tanguay; 04.03.2010
comment
хорошо, теперь я понимаю, что вы будете использовать скрытие с новым, чтобы дать новую функциональность унаследованному методу, который не является виртуальным, но ему не хватает преимущества переопределения, поскольку наследующий класс по-прежнему будет вызывать его собственный метод. - person Edward Tanguay; 04.03.2010

Вы не можете, потому что это бесполезно. Чего бы вы добились, переопределив поле? Простой:

class Animal{

    public class Head {
        int eye = 8;
    } 
    Head head = new Head()
    public void Test() 
    { 
        print head.eye; 
        DoOtherStufs(); 
    } //8

    protected virtual void DoOtherStufs() 
    {}
}

class Cat : Animal{
    class new Head : Animal.Head 
    {
        int mouth;
    } 
    Head head = new Head()
    protected override void DoOtherStufs() 
    { 
        print head.eye; 
    }
} 
Cat cat = new Cat();

cat.head.eye = 9;
print cat.Test() 

Это напечатает 8 9 вместо 9 9, как ожидалось. Мне нужен функционал базового класса, но мне также нужен унаследованный класс, с помощью которого я мог бы манипулировать (увеличивать) vars внутри vars внутренних групповых классов. Это невозможно!!

person Vasco Martins    schedule 03.09.2019