C# - Могат ли публично наследените методи да бъдат скрити (напр. частни за производен клас)

Да предположим, че имам BaseClass с публични методи A и B и създавам DerivedClass чрез наследяване.

e.g.

public DerivedClass : BaseClass {}

Сега искам да разработя метод C в DerivedClass, който използва A и B. Има ли начин да отменя методи A и B, за да бъдат частни в DerivedClass, така че само метод C да е изложен на някой, който иска да използва моя DerivedClass?


person Lyndon    schedule 19.09.2008    source източник


Отговори (10)


Не е възможно, защо?

В C# ви се налага, че ако наследите публични методи, трябва да ги направите публични. В противен случай те очакват да не произлизате от класа на първо място.

Вместо да използвате връзката е-а, ще трябва да използвате връзката има-а.

Езиковите дизайнери не позволяват това нарочно, така че да използвате наследяването по-правилно.

Например, някой може случайно да обърка клас Car с произлизащ от клас Engine, за да получи неговата функционалност. Но двигателят е функционалност, която се използва от автомобила. Така че бихте искали да използвате връзката има-а. Потребителят на автомобила не желае да има достъп до интерфейса на двигателя. И самата кола не трябва да бърка методите на двигателя със своите собствени. Нито бъдещите производни на Car.

Така че те не му позволяват да ви защити от лоши йерархии на наследяване.

Какво трябва да направите вместо това?

Вместо това трябва да внедрите интерфейси. Това ви оставя свободни да имате функционалност, използвайки релацията има-а.

Други езици:

В C++ просто указвате модификатор преди базовия клас private, public или protected. Това прави всички членове на базата, които са били публични, до това определено ниво на достъп. Струва ми се глупаво, че не можете да направите същото в C#.

Преструктурираният код:

interface I
{
    void C();
}

class BaseClass
{
    public void A() { MessageBox.Show("A"); }
    public void B() { MessageBox.Show("B"); }
}

class Derived : I
{
    public void C()
    {
        b.A();
        b.B();
    }

    private BaseClass b;
}

Разбирам, че имената на горните класове са малко спорни :)

Други предложения:

Други предложиха да направят A() и B() публични и да хвърлят изключения. Но това не прави приятелски клас, който хората да използват и всъщност няма смисъл.

person Brian R. Bondy    schedule 19.09.2008
comment
Интерфейсите определено са един препоръчителен начин, но не трябва да са единствената стъпка. Все още трябва да сте наясно дали вашият код нарушава принципа на единичната отговорност или не. Дори внедряването на интерфейси не ви пречи да пишете монолитни класове, които са големи и тромави. - person Jason Olson; 20.09.2008
comment
Друга причина да не криете публичните методи е, че това може да наруши договорите за интерфейс с потребителите. Например скриването на метода Remove() от базов клас, който имплементира IList, би нарушило всеки код, който очаква този метод да присъства въз основа на интерфейса. - person Peter Gluck; 05.07.2012
comment
За съжаление, невъзможността да направите това означава, че когато използвате нечия друга библиотека с лош дизайн, е невъзможно да изолирате кодовата си база от нея. Чудесен пример е класът WebControl в System.Web --- той има тонове публични свойства, които злоупотребяват с вградените стилове, и тъй като те прехвърлиха всичко това в базовия клас, няма начин да се създаде клас CleanWebControl, който го наследява и скрива лош дизайн. Скриването на лош дизайн на някой друг би било полезно, ако беше възможно, особено когато лошият дизайн е в самата .NET Framework. - person Sean Werkema; 01.10.2012

Когато например се опитате да наследите от List<object> и искате да скриете директния член Add(object _ob):

// the only way to hide
[Obsolete("This is not supported in this class.", true)]
public new void Add(object _ob)
{
    throw NotImplementedException("Don't use!!");
}

Всъщност не е най-предпочитаното решение, но върши работа. Intellisense все още приема, но по време на компилация получавате грешка:

грешка CS0619: „TestConsole.TestClass.Add(TestConsole.TestObject)“ е остаряла: „Това не се поддържа в този клас.“

person nono    schedule 01.07.2010
comment
Красив! Това ми трябваше. Имам зле структуриран лош клас, който не мога да променя (притежаван от някой друг), който има членове, които трябва да скрия. Това работи чудесно. Имайте предвид, че C++ ви позволява да направите това. - person Mark Lakata; 01.10.2012

Това звучи като лоша идея. Лисков няма да бъде впечатлен.

Ако не искате потребителите на DerivedClass да имат достъп до методите DeriveClass.A() и DerivedClass.B(), бих предложил DerivedClass да внедри някакъв публичен интерфейс IWhateverMethodCIsAbout и потребителите на DerivedClass всъщност трябва да говорят с IWhateverMethodCIsAbout и да знаят изобщо нищо за внедряването на BaseClass или DerivedClass.

person Hamish Smith    schedule 19.09.2008
comment
Направих малко проучване на интерфейсите и може би пропускам нещо голямо тук, но ако дефинирах интерфейса, така или иначе бих искал да го внедря с помощта на методи A и B. A и B са помощни функции за внедряване на по-големи функции в производните класове. - person Lyndon; 20.09.2008
comment
Линдън, надяваме се, че кодът в приетия отговор изяснява, че DerivedClass все още е подклас на BaseClass и все още можете да използвате функция A и B в DerivedClass, за да имплементирате C, но външният свят не може да взаимодейства с DerivedClass като DerivedClass и вместо това използва интерфейса - person Hamish Smith; 20.09.2008

Това, от което се нуждаете, е композиция, а не наследяване.

class Plane
{
  public Fly() { .. }
  public string GetPilot() {...}
}

Сега, ако имате нужда от специален вид самолет, като такъв, който има PairOfWings = 2, но иначе прави всичко, което един самолет може... Вие наследявате самолет. С това вие декларирате, че вашето извличане отговаря на договора на основния клас и може да бъде заменено без мигане, където се очаква основен клас. напр. LogFlight(Plane) ще продължи да работи с екземпляр на BiPlane.

Ако обаче просто се нуждаете от поведението Fly за нова птица, която искате да създадете, и не желаете да поддържате пълния договор за базов клас, вие вместо това композирате. В този случай преработете поведението на методите за повторно използване в нов тип Flight. Сега създайте и задръжте препратки към този клас както в Plane, така и в Bird. Вие не наследявате, защото Bird не поддържа пълния договор за базов клас... (напр. не може да предостави GetPilot()).

По същата причина не можете да намалите видимостта на методите на базовия клас, когато замените... можете да замените и да направите основен частен метод публичен в извличането, но не и обратното. напр. В този пример, ако извлека тип самолет "BadPlane" и след това заменя и "скривам" GetPilot() - правя го частен; клиентски метод LogFlight(Plane p) ще работи за повечето самолети, но ще избухне за "BadPlane", ако внедряването на LogFlight случайно се нуждае/извика GetPilot(). Тъй като всички деривации на базов клас се очаква да бъдат „заменими“ навсякъде, където се очаква параметър на базов клас, това трябва да бъде забранено.

person Gishu    schedule 20.09.2008
comment
необходимостта да скриете методите и свойствата на базовия клас възниква, когато нямате контрол върху източника на базовия клас. Да предположим, че трябва да замените цял механизъм от базовия клас, който не отговаря на вашите нужди. Вие създавате свои собствени методи, заменяте други, но също така искате да скриете някои, които принадлежат към оригиналния механизъм и които могат да бъдат използвани от други програмисти. Може да изберете агрегиране/обвиване на базовия клас, но това също не винаги е приемливо, защото прекъсва веригата на наследяване. Маркирането на такива методи като остарели е най-доброто решение - person Radu Simionescu; 17.11.2015

@Brian R. Bondy ме насочи към интересна статия за скриването чрез наследяване и ключовата дума new.

http://msdn.microsoft.com/en-us/library/aa691135(VS.71).aspx

Така че като заобиколно решение бих предложил:

class BaseClass
{
    public void A()
    {
        Console.WriteLine("BaseClass.A");
    }

    public void B()
    {
        Console.WriteLine("BaseClass.B");
    }
}

class DerivedClass : BaseClass
{
    new public void A()
    {
        throw new NotSupportedException();
    }

    new public void B()
    {
        throw new NotSupportedException();
    }

    public void C()
    {
        base.A();
        base.B();
    }
}

По този начин код като този ще изведе NotSupportedException:

    DerivedClass d = new DerivedClass();
    d.A();
person Jorge Ferreira    schedule 19.09.2008
comment
Ако използвате DerivedClass d = new DerivedClass(); d.A(), вие все още ще имате достъп до изпълнението A() на базовия клас. - person Brian R. Bondy; 20.09.2008
comment
Хорхе, току-що опитах това и както Брайън посочи, А и Б пак ще бъдат изложени. - person Lyndon; 20.09.2008
comment
ако прехвърлите екземпляр на DerivedClass към BaseClass, ще извикате A и B от базовия клас. Ще работи по-добре с виртуални методи в базовия клас - person Radu Simionescu; 17.11.2015

Единственият начин да направите това, за който знам, е да използвате връзка Has-A и да приложите само функциите, които искате да изложите.

person Nescio    schedule 19.09.2008

Криенето е доста хлъзгав наклон. Основните проблеми, IMO, са:

  • Зависи от типа декларация по време на проектиране на екземпляра, което означава, че ако направите нещо като BaseClass obj = new SubClass(), след това извикате obj.A(), скриването е победено. Ще се изпълни BaseClass.A().

  • Скриването може много лесно да скрие поведението (или промените в поведението) в основния тип. Това очевидно е по-малко притеснително, когато притежавате и двете страни на уравнението или ако извикването на „base.xxx“ е част от вашия подчлен.

  • Ако действително правите притежавате и двете страни на уравнението на базата/подкласа, тогава трябва да можете да измислите по-управляемо решение от институционализираното скриване/засенчване.
person Jared    schedule 20.09.2008

Бих казал, че ако имате кодова база, с която искате да направите това, това не е най-добре проектираната кодова база. Обикновено това е знак за клас в едно ниво на иерархията, който се нуждае от определен публичен подпис, докато друг клас, получен от този клас, не се нуждае от него.

Една предстояща парадигма на кодиране се нарича „композиция над наследяване“. Това играе директно на принципите на обектно-ориентираното развитие (особено принципа на единичната отговорност и принципа отворено/затворено).

За съжаление, начинът, по който много от нас, разработчиците, бяха научени на обектна ориентация, сме създали навика веднага да мислим за наследяване вместо за композиция. Склонни сме да имаме по-големи класове, които имат много различни отговорности, просто защото могат да се съдържат в един и същ обект от „Реалния свят“. Това може да доведе до класови йерархии, които са дълбоки 5+ нива.

Неприятен страничен ефект, за който разработчиците обикновено не се замислят, когато се занимават с наследяване, е, че наследяването формира една от най-силните форми на зависимости, които някога можете да въведете във вашия код. Вашият производен клас вече е силно зависим от класа, от който е наследен. Това може да направи кода ви по-крехък в дългосрочен план и да доведе до объркващи проблеми, при които промяната на определено поведение в базов клас прекъсва производните класове по неясни начини.

Един от начините да разбиете кода си е чрез интерфейси, както е споменато в друг отговор. Това все пак е умно нещо, тъй като искате външните зависимости на класа да се свързват с абстракции, а не с конкретни/производни типове. Това ви позволява да променяте имплементацията, без да променяте интерфейса, без да засягате ред код във вашия зависим клас.

Предпочитам да поддържам система със стотици/хиляди/дори повече класове, всичките малки и слабо свързани, отколкото да се занимавам със система, която използва силно полиморфизъм/наследяване и има по-малко класове, които са по-тясно свързани.

Може би най-добрият ресурс за обектно-ориентирана разработка е книгата на Robert C. Martin, Гъвкаво разработване на софтуер, принципи, модели и практики.

person Jason Olson    schedule 20.09.2008

Ако те са дефинирани публични в оригиналния клас, не можете да ги замените, за да бъдат частни във вашия производен клас. Можете обаче да накарате публичния метод да хвърля изключение и да приложите своя собствена частна функция.

Редактиране: Хорхе Ферейра е прав.

person Serafina Brocious    schedule 19.09.2008

Въпреки че отговорът на въпроса е „не“, има един съвет, който искам да посоча за другите, които пристигат тук (като се има предвид, че OP някак намекваше за достъп до асемблиране от трети страни). Когато други препращат към сборка, Visual Studio трябва да уважава следния атрибут, така че да не се показва в intellisense (скрит, но ВСЕ ОЩЕ може да бъде извикан, така че внимавайте):

[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]

Ако нямате друг избор, трябва да можете да използвате new на метод, който скрива метод от основен тип, да върнете => throw new NotSupportedException(); и да го комбинирате с атрибута по-горе.

Друг трик зависи от НЕ наследяването от базов клас, ако е възможно, където базата има съответен интерфейс (като IList<T> за List<T>). Внедряването на интерфейси "изрично" също ще скрие тези методи от intellisense за типа клас. Например:

public class GoodForNothing: IDisposable
{
    void IDisposable.Dispose() { ... }
}

В случай на var obj = new GoodForNothing(), методът Dispose() няма да бъде наличен на obj. Въпреки това, той ЩЕ бъде достъпен за всеки, който изрично преобразува от obj до IDisposable.

Освен това можете също така да обвиете базов тип, вместо да наследявате от него, след което да скриете някои методи:

public class MyList<T> : IList<T>
{
    List<T> _Items = new List<T>();
    public T this[int index] => _Items[index];
    public int Count => _Items.Count;
    public void Add(T item) => _Items.Add(item);
    [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
    void ICollection<T>.Clear() => throw new InvalidOperationException("No you may not!"); // (hidden)
    /*...etc...*/
}
person James Wilkins    schedule 27.02.2018