C#. Наследование. Базовые понятия. Преимущества и недостатки. Общая форма. Простейшие примеры. Модификатор доступа protected

Наследование. Базовые понятия. Преимущества и недостатки. Общая форма. Простейшие примеры. Модификатор доступа protected


Содержание


1. Что собой представляет наследование в программировании?

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

 

2. Преимущества использования наследования в программах

Правильное использование механизма наследования дает следующие взаимосвязанные преимущества:

  • эффективное построение сложных иерархий классов с возможностью их удобной модификации. Работу классов в иерархии можно изменять путем добавления новых унаследованных классов в нужном месте иерархии;
  • повторное использование ранее написанного кода с дальнейшей его модификацией под выполняемую задачу. В свою очередь, новосозданный код также может использоваться на низлежащих иерархиях классов;
  • удобство в сопровождении (дополнении) программного кода путем введения новых классов с новыми возможностями;
  • уменьшение количества логических ошибок при разработке сложных программных систем. Повторно используемый код чаще тестируется, а, значит, меньшая вероятность наличия в нем ошибок;
  • легкость в согласовании разных частей программного кода путем использования интерфейсов. Если два класса унаследованы от общего потомка, поведение этих классов будет одинаковым во всех случаях. Это утверждение выходит из требования, что похожие объекты должны иметь похожее поведение. Само использование интерфейсов предопределяет схожесть поведения объектов;
  • создание библиотек кода, которые можно использовать и дополнять собственными разработками;
  • возможность реализовывать известные шаблоны проектирования для построения гибкого кода, который не изменяет предыдущих разработок;
  • использование преимуществ полиморфизма невозможно без наследования. Благодаря полиморфизму обеспечивается принцип: один интерфейс – несколько реализаций;
  • обеспечение исследовательского программирования (быстрого макетирования). Такое программирование применяется в случаях, когда цели и требования к программной системе в начале расплывчасты. Сначала создается макет структуры, затом этот макет поэтапно усовершенствуется путем наследования предыдущего. Процесс длится до получения требуемого результата;
  • лучшее понимание структуры программной системы программистом благодаря естественному представлению механизма наследования. Если при построении сложных иерархий пробовать использовать другие принципы, то это может значительно усложнить понимание всей задачи и приведет к увеличению количества ошибок.

 

3. Недостатки наследования

При использовании наследования в программах были замечены следующие недостатки:

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

 



4. Что такое базовый класс? Что такое производный класс?

Базовый класс (base class) – это класс, программный код которого используется в унаследованных (производных) классах. Производный класс (derived class) – это класс, который использует программный код базового класса и изменяет (расширяет) его под свои потребности.

В других языках программирования (например, Java) базовый класс еще называется суперкласс (superclass), а производный класс называется подкласс (subclass).

 

5. Синтаксис наследования в случае двух классов. Общая форма

Если один класс наследует другой базовый класс, то общая форма объявления такого класса следующая:

class derived_class : base_class
{
  // тело класса
  // ...
}

где

  • derived_class – имя производного класса;
  • base_class – имя базового класса.

Например.

// базовый класс
class Base
{
  // поля, методы класса
  // ...
}

// класс, унаследованный от класса Base
class Derived : Base
{
  // поля, методы класса
  // ...
}

В вышеописанном примере Base – базовый класс, Derived – класс, который наследует возможности класса Base. В классе Derived непосредственно доступны все элементы (поля, свойства, методы, индексаторы и прочее) которые описываются с модификаторами доступа protected, public и internal. В свою очередь, класс Derived может быть базовым для другого класса нижнего уровня.

 

6. Доступ к элементам базового класса которые объявлены с модификаторами доступа private, protected, public, internal, protected internal

В производном классе доступны элементы базового класса, которые объявлены с модификаторами доступа protected, public, internal и protected internal.

Все элементы базового класса, которые объявлены с модификатором private недоступны в производном классе.

Пример. В примере демонстрируется влияние модификаторов доступа на доступ к элементам базового и производного классов.

using System;

namespace ConsoleApp1
{
  // Базовый класс
  class Base
  {
    // внутренние поля класса Base
    private int private_Item;
    protected int protected_Item;
    internal int internal_Item;
    protected internal int protIntern_Item;
    public int public_Item;
  }

  // Производный класс от класса Base
  class Derived : Base
  {
    // метод, который изменяет поля класса Base
    void Method()
    {
      protected_Item = 10;
      internal_Item = 20;
      protIntern_Item = 30;
      public_Item = 40;
      // private_item = 50; - ошибка, элемент недоступен
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // Доступ с экземпляру класса
      // 1. Базовый класс Base
      // 1.1. Создать экземпляр базового класса Base
      Base bs = new Base();

      // 1.2. Доступ к полям экземпляра bs
      // bs.private_Item = 10; - доступ запрещен
      // bs.protected_Item = 20; - доступ запрещен
      bs.internal_Item = 30; // internal доступ разрешен
      bs.public_Item = 40; // public - разрешен
      bs.protIntern_Item = 50; // protected internal - разрешен

      // 2. Производный класс Derived - доступ к полям базового класса Base
      // 2.1. Создать экземпляр производного класса Derived
      Derived dr = new Derived();
      // dr.private_Item - private-доступ запрещен
      // dr.protected_Item - protected-доступ запрещен
      dr.internal_Item = 30; // internal - разрешен
      dr.public_Item = 40; // public - разрешен
      dr.protIntern_Item = 50; // protected internal - разрешен
    }
  }
}

 

7. Сколько классов одновременно может быть унаследовано от базового класса?

Язык программирования C# не поддерживает множественного наследования (в отличие от языка C++). Из конкретного класса одновременно может быть унаследована только один класс. Тем не менее, класс может наследовать любое количество интерфейсов. Более подробно об интерфейсах можно прочитать здесь.

 

8. Особенности применения модификаторов доступа protected и protected internal. Пример

Каждый элемент класса может иметь разные уровни доступа: private, public, protected, internal, protected internal. Если в программе используется механизм наследования, то особое внимание заслуживают модификаторы доступа protected и protected internal.

Если элемент класса (поле, метод, свойство и т.п.) реализован с модификатором доступа protected, то к нему выполняются следующие правила:

  • элемент доступен в пределах класса, в котором он объявлен, а также в унаследованных классах;
  • элемент недоступен из экземпляра класса.

Модификатор доступа protected internal объединяет ограничение модификатора protected и модификатора internal (см. пример ниже). Здесь возможны два случая:

  1. Ситуация, когда класс с protected internal элементом и создаваемый экземпляр этого класса находятся в одной сборке. В этом случае доступ из экземпляра класса к protected internal элементу есть (расширение ключевого слова internal). Также есть доступ из производного класса (расширение ключевого слова protected).
  2. Ситуация, когда класс  с protected internal элементом объявлен в одной сборке, а экземпляр этого класса создается в другой сборке. В этом случае экземпляр не имеет доступа к protected internal элементу (ограничение internal). Но можно создать производный класс и из этого класса получить доступ к protected internal элементу (расширение protected).

Пример. Демонстрация использования модификаторов доступа на примере трех классов A, B, C.

using System;
namespace ConsoleApp1
{
  // Базовый класс
  class A
  {
    // Защищенные поля класса A
    protected int a;
    protected internal int aa;
  }

  // Производный класс B
  class B : A
  {
    // Защищенные поля класса B
    protected int b;
    protected internal int bb;

    public void Method()
    {
      // есть доступ к protected и
      // protected internal элементам класса A
      a = 25;
      aa = 30;
    }
  }

  // Производный класс C
  class C : B
  {
    // Защищенные поля класса C
    protected int c;
    protected internal int cc;

    public void Method()
    {
      // есть доступ к protected и
      // protected internal элементам классов A, B
      a = 10;
      aa = 20;
      b = 30;
      bb = 40;
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      // Доступа из экземпляров классов к
      // protected-элементам нет
      A objA = new A(); // экземпляр класса A
      B objB = new B();
      C objC = new C();

      // objA.a = 30; - доступа нет
      // objB.a = 20; - доступа нет
      // objB.b = 20; - доступа нет
      // objC.a = 20; - доступа нет
      // objC.b = 20; - доступа нет
      // objC.c = 20; - доступа нет

      // Однако, есть доступ к protected internal
      // экземплярам классов A, B, C,  
      // поскольку в данном примере классы A, B, C и их
      // экземпляры objA, objB, objC объявлены в одной сборке
      objA.aa = 40;
      objB.aa = 50;
      objB.bb = 50;
      objC.aa = 10;
      objC.bb = 20;
      objC.cc = 30;
    }
  }
}

 


Связанные темы