Предотвращение наследования методов - PullRequest
5 голосов
/ 08 апреля 2009

У меня есть базовый класс Foo, который является конкретным и содержит 30 методов, которые относятся к его подклассам.

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

Class Foo 
{
   /* ... inheritable methods ... */

   /* non-inheritable method */
   public bool FooSpecificMethod()
   { 
      return true;
   } 
}

Class Bar : Foo
{
    /* Bar specific methods */
}

var bar = new Bar();
bar.FooSpecificMethod(); /* is there any way to get this to throw compiler error */

РЕДАКТИРОВАТЬ

Я не уверен, что изначально был ясен.

Я понимаю принципы наследования и понимаю принцип замещения Лискова. В этом случае есть единственное исключение, которое касается ТОЛЬКО случая «не наследуется», и поэтому я не хотел создавать подкласс «uninheritedFoo».

Я спрашивал, технически возможно ли создать ситуацию, когда foo.FooSpecificMethod () является допустимым и общедоступным методом, но subclassoffoo.FooSpecificMethod () вызывает ошибку компилятора.

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

Ответы [ 9 ]

11 голосов
/ 08 апреля 2009

Я бы переосмыслил необходимость в этом.

Если вы используете наследование, вы предлагаете, чтобы "Bar" был "Foo". Если «Bar» всегда является «Foo», методы, работающие с «Foo», должны также работать с «Bar».

Если это не так, я бы переделал это как частный метод. Публично, Бар всегда должен быть Foo.


Просто сделать еще один шаг -

Если бы вы могли сделать это, все было бы очень сложно. Возможны ситуации, когда:

Foo myBar = new Bar(); // This is legal
myBar.FooSpecificMethod(); // What should this do?  
                           // It's declared a Foo, but is acutally a Bar

Вы действительно можете заставить это поведение, используя отражение, хотя. Я думаю, что это плохая идея, но FooSpecificMethod () может проверить тип этого, и, если это не typeof (Foo), выдать исключение. Это может быть очень запутанным и иметь очень неприятный запах.


Изменить в ответ на редактирование вопроса:

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

Например, вы можете рассмотреть возможность использования событий или делегатов для расширения поведения вместо того, чтобы разрешить объекту быть подклассами.

Попытка сделать то, что вы делаете, - это попытка предотвратить основные цели наследования.

6 голосов
/ 08 апреля 2009

Брайан прав насчет замещения Лискова (изменено). И Рид прав насчет «есть-а» (также измененный); на самом деле они оба говорят вам одно и то же.

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

Подкласс Foo означает, что вы говорите, что подкласс, например Бар, всегда допустимо использовать там, где можно использовать Foo. В частности, это означает, что вы не наследуете (необязательно) реализацию Foo (вы можете переопределить это, или Foo может быть абстрактным и не давать конкретной реализации для метода).

Наследование реализации (если есть) является деталью; То, что вы действительно наследуете , - это общедоступный интерфейс, контракт, обещание пользователям, что Bar может использоваться как Foo.

Действительно, они могут даже не знать, что у них есть Bar, а не Foo: если я создаю FooFactory и пишу его Foo * getAFoo (), чтобы вернуть указатель на Bar, они могут никогда не узнать, и не должен .

Когда вы нарушаете этот контракт, вы нарушаете Ориентацию на объект. (А классы Java Collection, создавая исключения NotSupported, полностью разрушают OO - пользователи больше не могут полиморфно использовать так называемые подклассы. Это плохой, плохой дизайн, который вызвал серьезные головные боли для многих многих пользователей Java, не что-то подражать.)

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

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

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

4 голосов
/ 08 апреля 2009

Нет, это нарушит принцип замены Лискова .

Прагматически, вы можете использовать метод "throw NotImplementedException ()" в Bar или удалить метод из Foo и переместить его к подклассам, к которым он применяется.

2 голосов
/ 08 апреля 2009

Если функция будет закрыта, она не будет вызываться подклассами напрямую.

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

Даже если это частная функция, ее все равно можно вызвать с помощью отражения.

Вы также можете объявить функцию в интерфейсе и явно реализовать интерфейс в классе, что вынудит вас привести ее к интерфейсу для использования функции.

1 голос
/ 08 апреля 2009

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

0 голосов
/ 26 марта 2018

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

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

public class Parent
{
    public CHILD1 Child1;
    public CHILD2 Child2;

    public Parent(params args){
        Child1 = new CHILD1(arg1, arg2);
        Child2 = new CHILD2(arg1, arg2);
    }
    //split here
    public Parent(object arg1, object arg2) {
        PropertyInfo[] list = this.GetType().GetProperties();
        foreach (var pi in list) {
            pi.SetValue(this, BL.Method(arg1, arg2, this.GetType().Name, pi.Name) // get data
        }
    }
}

public class CHILD1 : Parent
{
    public CHILD1(object arg1, object arg2) : base(arg1, arg2) { }

    public object C1Property1 { get; set; }
    public object C1Property2 { get; set; }
    // ..
}

public class CHILD2 : Parent
{
    public CHILD2(object arg1, object arg2) : base(arg1, arg2) { }

    public object C2Property1 { get; set; }
    public object C2Property2 { get; set; }
    // ..
}

Теперь я могу позвонить parent.Child1.C1Property1. Но код также допускает вызов следующего: parent.Child1.Child1.Property1, что не имеет смысла и приведет к исключению, потому что свойство Child1.Child1 всегда будет null.

Чтобы это исправить, все, что мне нужно было сделать, это разделить код родительского класса на два класса:

  • Один, содержащий свойства Child и вызывающий дочерние конструкторы
  • и один с логикой конструктора для заполнения свойств.
0 голосов
/ 14 мая 2014

Вы должны рассмотреть Композицию [http://en.wikipedia.org/wiki/Composite_pattern} над наследованием, если вы хотите добиться того, что вы пытаетесь сделать, и предоставить методы из частного объекта (то есть того, от которого вы в настоящее время наследуете).

0 голосов
/ 27 июля 2013

Существует две причины, по которым производный класс может скрывать членов базового класса, не нарушая принцип подстановки Лискова:

  1. Если рассматриваемый член является «защищенным», он, по сути, представляет собой контракт только между родительским классом и производным классом. Как таковой, он не накладывает никаких обязательств на производный класс, за исключением случаев, предусмотренных договором с родителем. Если открытые члены базового класса полагаются на некоторый непубличный конкретный член внешних экземпляров, имеющий какое-то определенное значение, производный класс, который изменяет значение этого члена, может нарушать LSP, даже если сам член не является публичным, поскольку изменение его значения может изменить поведение публичных членов, которые его используют. Однако в тех случаях, когда базовый класс определяет защищенный член, но не использует его, производный класс может делать с этим членом все что угодно, не нарушая LSP. Ключевым моментом, который нужно понимать с защищенными членами, является то, что причина, по которой «ToyotaCar» не должен изменять семантику открытых членов «Car», заключается в том, что ссылка на «ToyotaCar» может быть дана к коду, который скорее ожидает «Car» чем "ToyotaCar". С другой стороны, если `ToyotaCar` изменяет поведение защищенного метода` Car` способом, который не доступен никому из открытых участников, единственным кодом, который может заметить такое изменение, является код, который находится внутри либо ToyotaCar или его производное, и такой код будет знать, что у него есть ToyotaCar. Код, который находится в некоторой другой производной от «Car» (например, «FordCar»), может получить доступ к защищенным членам «Car», но гарантированно * нет * для доступа к этим методам в экземпляре «ToyotaCar».
  2. Иногда полезно, чтобы базовый тип или интерфейс представляли несколько членов, которые не все будут применимы к каждому типу реализации. Хотя принцип сегрегации интерфейсов предполагает, что следует избегать интерфейсов «кухонная раковина», существуют случаи, когда такие интерфейсы могут быть более практичными, чем любая альтернатива. Например, некоторые виды «потокового» класса могут поддерживать разные режимы рукопожатия, а другие - нет. Если потоки ссылок будут передаваться через код, который не будет заботиться о рукопожатии, может быть проще иметь класс Stream, включающий свойство выбора рукопожатия, которое может или не может быть использовано, чем требовать, чтобы код пытался привести переданный в обратитесь к IHandshakeSelector и установите IHandshakeSelector.HandshakeMode, если это так. С другой стороны, если конкретный потоковый класс будет вести себя одинаково для всех режимов рукопожатия, кроме `XonXoff`, что вызовет исключение, если код попытается установить это, может иметь смысл для этого класса скрыть свойство` HandshakeMode`.

Я бы сказал, что хотя сокрытие членов базового класса часто является признаком плохого дизайна, бывают случаи, когда сокрытие членов базового класса было бы более уместным, чем оставлять их открытыми; если производный класс не может возможно правильно использовать защищенный метод (как было бы в случае с методом MemberwiseClone, доступным для производных List<T>), нет никаких причин выставлять его. К сожалению, нет абсолютно чистого способа скрыть таких участников. Лучшее, что можно сделать, - это объявить открытый член с тем же именем (используя модификатор «new»), который заблокирует весь доступ к члену базового класса. Например, код может определять вложенный общедоступный статический класс с именем MemberwiseClone без членов. Атрибут EditorBrowseable можно использовать для предотвращения показа Intellisense этого вложенного класса, но даже если код попытался использовать этот класс, у него нет членов, и нельзя создать хранилище этого типа, поэтому оно должно быть довольно безопасным. .

0 голосов
/ 13 мая 2009

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

Большое спасибо за все ваши советы, это заставило меня переосмыслить именно то, что мне было нужно из этой договоренности.

...