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

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

< Лекция 13 || Лекция 14: 123 || Лекция 15 >

Многоуровневая иерархия

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

15_02

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 DemoShape : DemoPoint
{
  protected int z;
  new public void Show()
  {
    Console.WriteLine("точка в пространстве: ({0}, {1}, {2})", x, y, z);
  }
  public DemoShape(int x, int y, int z):base(x, y) 
  {
    this.z=z;
  }
}

class DemoLine : DemoPoint
{
  protected int x2;
  protected int y2;
  new public void Show()
  {
    Console.WriteLine("отрезок на плоскости: ({0}, {1})-({2},{3})",x,y, x2, y2);
  }
  public DemoLine(int x1, int y1, int x2, int y2):base(x1, y1) 
  {
    this.x2 = x2; this.y2 = y2;
  }
}

class DemoTriangle: DemoLine
{
  protected int x3;
  protected int y3;
  new public void Show()
  {
    Console.WriteLine("треугольник на плоскости: ({0}, {1})-({2},{3})-({4},{5})",x,y, x2, y2, x3, y3);
  }
  public DemoTriangle(int x1, int y1, int x2, int y2, int x3, int y3):base(x1, y1, x2, y2)    
  {
    this.x3 = x3; this.y3 = y3;
  }
}
  
class Program
{
  static void Main()
  {
    DemoPoint point = new DemoPoint(1,1);
    point.Show();
    DemoShape pointShape = new DemoShape(1,1,1);
    pointShape.Show();
    DemoLine line = new DemoLine( 2, 2, 10, 10);
    line.Show();
    DemoTriangle triangle = new DemoTriangle (0,0,0,3,4,0);
    triangle.Show();
  }
}

Переменные базового класса и производного класса

С# является языком со строгой типизацией, в нем требуется строгое соблюдение совместимости типов с учетом стандартных преобразований типов. Из чего следует, что переменная одного типа обычно не может ссылаться на объект другого ссылочного типа. За одним небольшим исключением - ссылочная переменная базового класса может ссылаться на объект любого производного класса. Продемонстрируем это на примере:

class DemoPoint
  {
    public int x;
    public int y;
    public void Show()
    {
      Console.WriteLine("точка на плоскости: ({0}, {1})",x, y);
    }
    public DemoPoint (int x, int y)
    {
      this.x=x;  this.y=y;
    }
  }

class DemoShape : DemoPoint
  {
    public int z;
    new public void Show()
    {
      Console.WriteLine("точка в пространстве: ({0}, {1}, {2})", x, y, z);
    }
    public DemoShape(int x, int y, int z):base(x, y) 
    {
      this.z=z;
    }
}

class Program
  {
    static void Main()
    {
      DemoPoint point1 = new DemoPoint(0,1);
      Console.WriteLine("({0}, {1})",point1.x,point1.y);
      DemoShape pointShape = new DemoShape(2,3,4);
      Console.WriteLine("({0}, {1}, {2})",pointShape.x, pointShape.y, pointShape.z);
      DemoPoint point2=pointShape; //допустимая операция
      //ошибка - не соответствие типов указателей
      //pointShape=point1; 
      Console.WriteLine("({0}, {1})", point2.x, point2.y);
      //ошибка, т.к. в классе DemoPoint нет поля z
      //Console.WriteLine("({0}, {1}, {2})", point2.x, point2.y, point2.z);
    }
  }

Ошибка возникнет и при попытке через объект point2 обратиться к методу Show. Например, point2.Show(). В этом случае компилятор не сможет определить, какой метод Show вызвать - для базового или для производного класса. Для решения данной проблемы можно воспользоваться таким понятием как полиморфизм, который основывается на механизме виртуальных методов.

Виртуальные методы

Виртуальный метод - это метод, который объявлен в базовом классе с использованием ключевого слова virtual, и затем переопределен в производном классе с помощью ключевого слова override. При этом если реализована многоуровневая иерархия классов, то каждый производный класс может иметь свою собственную версию виртуального метода. Этот факт особенно полезен в случае, когда доступ к объекту производного класса осуществляется через ссылочную переменную базового класса. В этой ситуации С# сам выбирает какую версию виртуального метода нужно вызвать. Этот выбор производится по типу объекта, на которую ссылается данная ссылка. Например:

class DemoPoint //базовый класс
  {
    protected int x;
    protected int y;
    public virtual void Show()  //виртуальный метод
    {
      Console.WriteLine("точка на плоскости: ({0}, {1})",x, y);
    }
    public DemoPoint (int x, int y)
    {
      this.x=x;  this.y=y;
    }
  }
  class DemoShape : DemoPoint //производный класс
  {
    protected int z;
    public override void Show() //перегрузка виртуального метода
    {
      Console.WriteLine("точка в пространстве: ({0}, {1}, {2})", x, y, z);
    }

    public DemoShape(int x, int y, int z):base(x, y) //конструктор производного класса
    {
      this.z=z;
    }

  }
  class DemoLine : DemoPoint //производный класс
  {
    protected int x2;
    protected int y2;
    public override void Show()  //перегрузка виртуального метода
    {
      Console.WriteLine("отрезок на плоскости: ({0}, {1})-({2},{3})",x,y, x2, y2);
    }
    public DemoLine(int x1, int y1, int x2, int y2):base(x1, y1) 
    {
      this.x2 = x2; this.y2 = y2;
    }
  }
  
  class Program
  {
    static void Main()
    {
      DemoPoint point1 = new DemoPoint(0,1);
      point1.Show();
      DemoShape pointShape = new DemoShape(2,3,4);
      pointShape.Show();
      DemoLine line = new DemoLine(0,0, 10, 10);
      line.Show();
      Console.WriteLine();
      //использование ссылки базового класса на объекты производных классов
      DemoPoint point2=pointShape;
      point2.Show();
      point2=line;
      point2.Show(); 
    }
  }

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

Абстрактные методы и классы

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

Абстрактный метод создается с помощью модификатора abstract. Он не имеет тела и, следовательно, не реализуется базовым классом, а производные классы должны его обязательно переопределить. Абстрактный метод автоматически является виртуальным, однако использовать спецификатор virtual не нужно. Более того, если вы попытаетесь использовать два спецификатора одновременно, abstract и virtual, то компилятор выдаст сообщение об ошибке.

Задание. Подумайте, можно ли спецификатор abstract применять в сочетании со спецификатором static. И объясните почему?

Если класс содержит один или несколько абстрактных классов, то его также нужно объявить как абстрактный, используя спецификатор abstract перед class. Поскольку абстрактный класс полностью не реализован, то невозможно создать экземпляр класса с помощью операции new. Например, если класс Demo определен как абстрактный, то попытка создать экземпляр класса Demo повлечет ошибку:

Demo a = new Demo();

Однако, можно создать массив ссылок, используя этот же абстрактный класс:

Demo [] Ob=new Demo[5];

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

Рассмотрим пример использования абстрактных методов и классов.

abstract class Demo //абстрактный класс
  {
    abstract public void Show();//абстрактный метод
    abstract public double Dlina();//абстрактный метод
  }

  class DemoPoint:Demo //производный класс от абстрактного
  {
    protected int x;
    protected int y;
    public DemoPoint (int x, int y)
    {
      this.x=x;  this.y=y;
    }
    public override void Show() //переопределение абстрактного метода
    {
      Console.WriteLine("точка на плоскости: ({0}, {1})",x, y);
    }
    public override double Dlina()  //переопределение абстрактного метода
    {
      return Math.Sqrt(x*x+y*y);
    }
  }

  class DemoShape : DemoPoint //производный класс 
  {
    protected int z;
    public DemoShape(int x, int y, int z):base(x, y) 
    {
      this.z=z;
    }
    public override void Show()  //переопределение абстрактного метода
    {
      Console.WriteLine("точка в пространстве: ({0}, {1}, {2})", x, y, z);
    }
    public override double Dlina()  //переопределение абстрактного метода
    {
      return Math.Sqrt(x*x+y*y+z*z);
    }
  }

  class DemoLine : DemoPoint //производный класс
  {
    protected int x2;
    protected int y2;
    public DemoLine(int x1, int y1, int x2, int y2):base(x1, y1) 
    {
      this.x2 = x2; this.y2 = y2;
    }
    public override void Show()  //переопределение абстрактного метода
    {
      Console.WriteLine("отрезок на плоскости: ({0}, {1})-({2},{3})",x,y, x2, y2);
    }
    public override double Dlina()  //переопределение абстрактного метода
    {
      return Math.Sqrt((x-x2)*(x-x2)+(y-y2)*(y-y2));
    }
  }
  
  class Program
  {
    static void Main()
    {
      Demo [] Ob=new Demo[5]; //массив ссылок
      //заполнения массива ссылками на объекты производных классов
      Ob[0]=new DemoPoint(1,1);
      Ob[1]=new DemoShape(1,1,1);
      Ob[2]=new DemoLine(0,3,4,0);
      Ob[3]=new DemoLine(2,1,2,10);
      Ob[4]=new DemoPoint(0,100);
      foreach (Demo a in Ob) //просмотр массива
      {
        a.Show();
        Console.WriteLine("Dlina: {0:f2}\n", a.Dlina());
      }
    }
  }

Запрет наследования

В С# есть ключевое слово sealed, позволяющее описать класс, от которого запрещено наследование. Например:

sealed class Demo {   … }
class newDemo: Demo {   …  }  // ошибка
Задание. Подумайте:
  1. для чего может создаваться класс, от которого нельзя наследовать?
  2. можно ли использовать сочетание спецификаторов sealed и abstract при описании класса, и почему?
< Лекция 13 || Лекция 14: 123 || Лекция 15 >
Sahib Dadashev
Sahib Dadashev
Россия
Роман Казимирчик
Роман Казимирчик
Беларусь