Опубликован: 19.02.2009 | Уровень: специалист | Доступ: платный
Лекция 14:

Иерархия классов

< Лекция 13 || Лекция 14: 123 || Лекция 15 >
Аннотация: Лекция рассматривает иерархию классов: наследование, его виды, практические примеры.

14.1 Иерархия

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

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

  1. исключения из программы повторяющихся фрагментов кода;
  2. упрощения модификации программы;
  3. упрощения создания новых программ на основе существующих.

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

Кроме механизма наследования в данном разделе мы рассмотрим такие важные понятия ООП как полиморфизм и инкапсуляцию (см. "Технология объектно-ориентированного программирования" ), которые также принимают участие в формировании иерархии классов.

Наследование

Класс в С# может иметь произвольное количество потомков и только одного предка. При описании класса имя его предка записывается в заголовке класса после двоеточия. Если имя предка не указано, предком считается базовый класс всей иерархии System.Object. Синтаксис наследования:

[атрибуты] [спецификаторы] class имя_класса [: предки]
{ тело_класса}

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

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

Рассмотрим наследование классов на примере геометрических фигур на плоскости. В качестве базового класса создадим класс DemoPoint (точка на плоскости), в качестве производного класса от DemoPoint класс DemoLine (отрезок на плоскости):

class DemoPoint //базовый класс
  {
    public int x;
    public int y;
    public void Show()
    {
      Console.WriteLine("({0}, {1})", x, y);
    }
  }

  class DemoLine : DemoPoint //производный класс
  {
    public int xEnd;
    public int yEnd;
    public  void Show()
    {
      Console.WriteLine("({0}, {1})-({2}, {3})", x, y ,xEnd, yEnd);
    }
  }

  class Program
  {
    static void Main()
    {
      DemoPoint point = new DemoPoint();
      point.x = 0;
      point.y = 0;
      point.Show();
      DemoLine line = new DemoLine();
      line.x = 2;  line.y = 2;
      line.xEnd = 10;  line.yEnd = 10;
      line.Show();
    }
  }

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

Чтобы избежать подобного предупреждения необходимо перед одноименным членом производного класса, в данном случае перед методом Show в классе DemoLine, поставить спецификатор new. Данный спецификатор скрывает одноименный член базового класса и предупреждений выдаваться не будет.

Использование защищенного доступа

В нашем примере поля x и у базового класса были открыты для доступа ( public ). Если убрать public, то поля автоматически станут закрытыми для доступа ( private ), в том числе и для доступа из производного класса. Решить проблему доступа к закрытым полям базового класса из производного можно двумя способами: используя свойства класса или спецификатор protected. При объявлении какого-то члена класса с помощью спецификатора protected, он становится закрытым для всех классов, кроме производных.

class DemoPoint
  {
      protected int x;
      protected int y;
      public void Show()
      {
        Console.WriteLine("({0}, {1})",x, y);
      }
    }

    class DemoLine : DemoPoint
    {
      public int xEnd;
      public int yEnd;
      public new void Show()
      {
        x=2; y=2; //доступ к закрытым полям базового класса
        Console.WriteLine("({0}, {1})-({2}, {3})", x, y, xEnd, yEnd);
      }
    }
   
    class Program
    {
      static void Main()
      {
        DemoPoint point = new DemoPoint();
        point.Show();
        DemoLine line = new DemoLine();
        //line.x = 2; line.y = 2; //доступ к полям закрыт
        line.xEnd = 10;    line.yEnd = 10;
        line.Show();
      }
    }
  }

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

Наследование конструкторов

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

В предыдущем примере классы создавались за счет автоматического вызова средствами С# конструктора по умолчанию. Добавим конструктор только в производный класс DemoLine:

class DemoPoint
{
  protected int x;
  protected int y;
  public void Show()
  {
    Console.WriteLine("({0}, {1})",x, y);
  }
}

class DemoLine : DemoPoint
{
  public int xEnd;
  public int yEnd;
  new public void Show()
  {
    Console.WriteLine("({0}, {1})-({2}, {3})", x, y, xEnd, yEnd);
  }

  public DemoLine(int x1, int y1, int x2, int y2) //конструктор производного класса
  {
    x = x1;    y = y1;
    xEnd = x2; yEnd = y2;
  }
}

class Program
{
  static void Main()
  {
    DemoPoint point = new DemoPoint(); //вызывается конструктор по умолчанию 
    point.Show();
    DemoLine line = new DemoLine(2, 2, 10, 10); //вызывается собственный конструктор 
    line.Show();
  }
}

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

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

  1. позволяет вызвать конструктор базового класса:

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

    конструктор_производного_класса (список_параметров) : base (список_аргументов)
    { тело конструктора }
    
    где с помощью элемента списка аргументов передаются параметры конструктору базового класса. Например: 
    
    class DemoPoint
        {
          protected int x;
          protected int y;
          public void Show()
          {
            Console.WriteLine("({0}, {1})",x, y);
    }
          public DemoPoint (int x, int y)//конструктор базового класса
          {
            this.x=x;  this.y=y;
          }
        }
    
    class DemoLine : DemoPoint
        {
          public int xEnd;
          public int yEnd;
          new public void Show()
          {
            Console.WriteLine("({0}, {1})-({2}, {3})", x, y, xEnd, yEnd);
          }
    
          public DemoLine(int x1, int y1, int x2, int y2):base(x1, y1) //конструктор производного класса
          {
            xEnd = x2; yEnd = y2;
          }
        }
    
    class Program
        {
          static void Main()
          {
            DemoPoint point= new DemoPoint(5, 5);
            point.Show();
            DemoLine line = new DemoLine( 2, 2, 10, 10);
            line.Show();
          }
        }
    Задание. Объясните, почему в конструкторе базового класса для инициализации полей используется параметр this, а в конструкторе производного класса нет.

    В общем случае с помощью ключевого слова base можно вызвать конструктор любой формы, определенный в базовом классе. Реально же выполнится тот конструктор, параметры которого будут соответствовать переданным при вызове аргументам. Например:

    class DemoPoint
    {
      protected int x;
      protected int y;
      public void Show()
      {
        Console.WriteLine("({0}, {1})",x, y);
    }
      public DemoPoint () //конструктор базового класса по умолчанию
      {
        this.x=1;  this.y=1;
      }
      public DemoPoint (int x, int y) //конструктор базового класса с параметрами
      {
        this.x=x;  this.y=y;
      }
    }
    
    class DemoLine : DemoPoint
    {
      public int xEnd;
      public int yEnd;
      new public void Show()
      {
        Console.WriteLine("({0}, {1})-({2}, {3})", x, y, xEnd, yEnd);
      }
      public DemoLine() //конструктор производного класса по умолчанию
      {
        xEnd = 100; yEnd = 100;
      }
      public DemoLine(int x2, int y2) //конструктор производного класса с двумя параметрами
      {
        xEnd = x2; yEnd = y2;
      }  
      //конструктор производного класса с четырьмя параметрами
      public DemoLine(int x1, int y1, int x2, int y2):base(x1, y1)
      {
        xEnd = x2; yEnd = y2;
      }
    }
    
    class Program
    {
      static void Main()
      {
        DemoPoint point1= new DemoPoint(); //вызов конструктора по умолчанию
        DemoPoint point2= new DemoPoint(5, 5); //вызов конструктора с параметрами
        point1.Show();
        point2.Show();
        DemoLine line1 = new DemoLine();//вызов конструктора по умолчанию
        DemoLine line2 = new DemoLine(4, 4);   //вызов конструктора с двумя параметрами
        //вызов конструктора с четырьмя параметрами
        DemoLine line3 = new DemoLine(2, 2, 10, 10);
        line1.Show();
        line2.Show();
        line3.Show();
      }
    }
    Задание. Объясните, как при вызове конструкторе производного класса инициируется вызов конструктора базового класса.
  2. позволяет получить доступ к члену базового класса, который скрыт "за" членом производного класса.

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

    base.член_класса

    Здесь в качестве элемента член_класса можно указывать либо метод, либо поле экземпляра. Эта форма ссылки base наиболее применима в тех случаях, когда имя члена в производном классе скрывает член с таким же именем в базовом классе.

    class DemoPoint
      {
        protected int x;
        protected int y;
        public void Show()
        {
          Console.Write("({0}, {1})",x, y);
        }
        public DemoPoint (int x, int y)//конструктор базового класса
        {
          this.x=x;  this.y=y;
        }
      }
    
      class DemoLine : DemoPoint
      {
        public int xEnd;
        public int yEnd;
        new public void Show()
        {
          base.Show(); //вызов члена базового класса
          Console.WriteLine("-({0}, {1})", xEnd, yEnd);
        }
    
        public DemoLine(int x1, int y1, int x2, int y2):base(x1, y1) //конструктор производного класса
        {
          xEnd = x2; yEnd = y2;
        }
      }
    
      class Program
      {
        static void Main()
        {
          DemoLine line = new DemoLine( 2, 2, 10, 10);
          line.Show();
        }
      }

    Несмотря на то, что метод Show в классе DemoLine скрывает одноименный метод в классе DemoPoint, ссылка base позволяет получить доступ к методу Show в базовом классе. Аналогично с помощью ссылки base можно получить доступ к одноименным полям базового класса.

< Лекция 13 || Лекция 14: 123 || Лекция 15 >
Sahib Dadashev
Sahib Dadashev
Россия
Роман Казимирчик
Роман Казимирчик
Беларусь