Введение в ООП с примерами на C#. Часть вторая. Все, что нужно знать о наследовании

Рассказывает Akhil Mittal


Вступление

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

Давайте сразу тезисно опишем, что такое наследование:

  • Это механизм создания нового класса на основе уже существующего старого.
  • Старый класс называется «родительским», «предком» («super class»).
  • Новый класс называется «дочерним», «наследником» («sub class»).
  • Наследование нужно для повторного использования кода, которое облегчает следование принципу DRY (Don’t Repeat Yourself — Не повторяйся).
  • Дочерний класс содержит методы и переменные родительского.

Рассмотрим наследование в действии

Создайте консольное приложение и назовите его InheritanceAndPolymorphism. Добавьте два класса, с названиями ClassA и ClassB, как показано ниже:

ClassA:

   class ClassA
     {
        
     }

ClassB:

    class ClassB
    {
        public int x = 100;
        public void Display1()
        {
            Console.WriteLine("ClassB Display1");
        }
        public void Display2()
        {
            Console.WriteLine("ClassB Display2");
        }
    }

Как вы можете видеть, класс A пуст, а в B мы добавили два метода и переменную x со значением 100.

Теперь в главном методе Program.cs напишите следующее:

class Program
  {
      static void Main(string[] args)
      {

          ClassA a = new ClassA();
          a.Display1();
          Console.ReadKey();
      }
  }

Разумеется, этот код вызовет ошибку:

Error: ‘InheritanceAndPolymorphism.ClassA’ does not contain a definition for ‘Display1’ and no extension method ‘Display1’ accepting a first argument of type ‘InheritanceAndPolymorphism.ClassA’ could be found (are you missing a using directive or an assembly reference?)

Очевидно, причина в том, что в классе А нет метода, который мы вызываем. Однако он есть у класса B. Было бы здорово, если бы мы могли получить доступ ко всему коду в B из A!

Теперь измените описание первого класса на следующее:

ClassA:

  class ClassA:ClassB
    {
        
    }

Теперь после выполнения программы мы получим:

ClassB Display1

Т.е. теперь ClassA наследует публичные методы из ClassB, это то же самое, если бы мы скопировали весь код из B в A. Всё, что объект класса B может делать, может и объект класса A. ClassA — дочерний класс, а ClassB — родительский.

Что нужно запомнить: как сын получается похожим на отца, наследует его черты, так и дочерний класс имеет параметры родительского.

Теперь давайте представим, что ClassA тоже имеет метод Display1:

class ClassA:ClassB
    {
        public void Display1()
        {
            System.Console.WriteLine("ClassA Display1");
        }
    }

Что будет, если мы запустим код теперь? Каким будет вывод? И будет ли вывод вообще или выйдет ошибка компиляции? Давайте проверим.

ClassA Display1

Однако мы также получим предупреждение:

Warning: ‘InheritanceAndPolymorphism.ClassA.Display1()’ hides inherited member ‘InheritanceAndPolymorphism.ClassB.Display1()’. Use the new keyword if hiding was intended.

Что нужно запомнить: ничто не может помешать создать в дочернем классе такой же метод, как и в родительском.

Когда мы вызываем a.Display1(), C# сначала ищет Display1() в ClassA, а только потом в ClassB. Поскольку в A такой метод есть, вызывается именно он.

Что нужно запомнить: методы дочерних классов имеют приоритет при выполнении.

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

ClassA:

  class ClassA:ClassB
    {
        public void Display1()
        {
            Console.WriteLine("ClassA Display1");
            base.Display1();
        }
    }

В таком случае вывод будет:

ClassA Display1
ClassB Display1

Что нужно запомнить: ключевое слово base может быть использовано для обращения к методам класса-предка.

Что же, вверх по иерархии мы обращаться можем. Давайте попробуем сделать наоборот:

/// <summary>
/// ClassB: выступает в роли класса-предка
/// </summary>
class ClassB
 {
     public int x = 100;
     public void Display1()
     {
         Console.WriteLine("ClassB Display1");
     }
 }

 /// <summary>
 /// ClassA: выступает в роли класса-наследника
 /// </summary>
 class ClassA : ClassB
 {
     public void Display2()
     {
         Console.WriteLine("ClassA Display2");
     }
 }

 /// <summary>
 /// Program: используется для выполнения кода.
 /// Contains Main method.
 /// </summary>
 class Program
 {
     static void Main(string[] args)
     {
         ClassB b = new ClassB();
         b.Display2();
         Console.ReadKey();
     }
 }

Error: ‘InheritanceAndPolymorphism.ClassB’ does not contain a definition for ‘Display2’ and no extension method ‘Display2’ accepting a first argument of type ‘InheritanceAndPolymorphism.ClassB’ could be found (are you missing a using directive or an assembly reference?)

Что нужно запомнить: наследование не работает в обратном направлении.

Когда класс A наследуется от B, он получает все его методы и может их использовать. Однако методы, которые были добавлены в A, не загружаются наверх в B, наследование не имеет обратной совместимости. Если попытаться вызвать из класса-родителя метод, который создан в классе-наследнике, вы получите ошибку.

Что нужно запомнить: кроме конструкторов и деструкторов, дочерний класс получает от родителя абсолютно всё.

Если класс С, будет унаследован от класса B, который, в свою очередь, будет унаследован от класса A, то класс C унаследует члены как от класса B, так и от класса A. Это транзитивное свойство наследования. Потомок перенимает все члены родителей и не может исключить какие-либо. Он может «спрятать» их, создав свой метод с тем же именем. Конечно, это никак не повлияет на родительский класс, просто в дочернем метод не будет виден.

Члены класса могут быть двух типов — статический, который принадлежит именно классу, или обычный, который доступен только из реализаций класса (его объектов). Чтобы сделать член статическим мы должны использовать ключевое слово static.

Если мы не наследуем класс ни от какого другого, подразумевается, что мы наследуем его от класса object. Это — родитель всех классов, и он единственный не унаследован ни от чего. Таким образом, такой код:

public class ClassB
 {
 }

 public class ClassA : ClassB
 {
 }

Автоматически воспринимается C# так:

public class ClassB:object
{
}

public class ClassA : ClassB
{
}

Таким образом, по свойству транзитивности, ClassA также является наследником object.

Теперь ещё один момент. Если мы захотим сделать так:

public class ClassW : System.ValueType
  {
  }

  public class ClassX : System.Enum
  {
  }

  public class ClassY : System.Delegate
  {
  }

  public class ClassZ : System.Array
  {
  }

То у нас это не получится:

‘InheritanceAndPolymorphism.ClassW’ cannot derive from special class ‘System.ValueType’
‘InheritanceAndPolymorphism.ClassX’ cannot derive from special class ‘System.Enum’
‘InheritanceAndPolymorphism.ClassY’ cannot derive from special class ‘System.Delegate’
‘InheritanceAndPolymorphism.ClassZ’ cannot derive from special class ‘System.Array’

Заметили словосочетание «special class»? Такие классы нельзя расширять.

Что нужно запомнить: ваши классы не могут быть унаследованы от встроенных классов вроде System.ValueType, System.Enum, System.Delegate, System.Array и т.д.

И ещё кое-что:

public class ClassW
  {
  }

  public class ClassX
  {
  }

  public class ClassY : ClassW, ClassX
  {
  }

Выше мы описали три класса: ClassW, ClassX и ClassY, который наследуется от первых двух. Теперь попробуем это скомпилировать:

Compile time Error: Class ‘InheritanceAndPolymorphism.ClassY’ cannot have multiple base classes: ‘InheritanceAndPolymorphism.ClassW’ and ‘ClassX’.

Что ещё нужно запомнить: класс может иметь только одного родителя, множественное наследование в C# не поддерживается (оно поддерживается у интерфейсов, но в этой статье мы о них речи не ведём).

Если мы попробуем обойти это правило таким образом:

public class ClassW:ClassY
 {
 }

 public class ClassX:ClassW
 {
 }

 public class ClassY :  ClassX
 {
 }

То это не пройдёт:

Error: Circular base class dependency involving ‘InheritanceAndPolymorphism.ClassX’ and ‘InheritanceAndPolymorphism.ClassW’.

Что нужно запомнить: классы не могут наследоваться циклически (1-й от 2-го, 2-й от 3-го 3-й от 1-го), что, в общем-то, логично.

Операции с объектами

ClassB:
public class ClassB
    {
        public int b = 100;
    }

ClassA:

    public class ClassA
    {
        public int a = 100;
    }
/// <summary>
/// Program: используется для запуска кода.
/// Contains Main method.
/// </summary>
public class Program
{
    private static void Main(string[] args)
    {
        ClassB classB = new ClassB();
        ClassA classA = new ClassA();
        classA = classB;
        classB = classA;
    }
}

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

Cannot implicitly convert type ‘InheritanceAndPolymorphism.ClassB’ to ‘InheritanceAndPolymorphism.ClassA’

Cannot implicitly convert type ‘InheritanceAndPolymorphism.ClassA’ to ‘InheritanceAndPolymorphism.ClassB’

Однако у нас это плохо получается. Даже несмотря на то, что они имеют одинаковые поля с одинаковыми значениями. Даже если бы эти поля имели одинаковые названия. C# работает с типами очень чётко — вы не можете приравнять два объекта от двух независимых классов. Однако, если бы класс A наследовался от B:

public class ClassA:ClassB
 {
     public int a = 100;
 }

…мы бы продвинулсь немногим дальше:

Error: Cannot implicitly convert type ‘InheritanceAndPolymorphism.ClassB’ to ‘InheritanceAndPolymorphism.ClassA’. An explicit conversion exists (are you missing a cast?)

Как я уже говорил, C# подходит к вопросам типов очень дотошно. Класс A унаследован от B, значит, имеет все его поля и методы — при назначении переменной типа B объекта типа A проблем не возникает. Однако вы уже знаете, что в обратную сторону это не работает — в классе B нет полей и методов, которые могут быть в A.

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

Здесь нам наконец-то представляется шанс обмануть правило:

public class ClassB
    {
        public int b = 100;
    }

    public class ClassA:ClassB
    {
        public int a = 100;
    }

    /// <summary>
    /// Program: используется для запуска кода
    /// Contains Main method.
    /// </summary>
    public class Program
    {
        private static void Main(string[] args)
        {
            ClassB classB = new ClassB();
            ClassA classA = new ClassA();
            classB=classA;
            classA = (ClassA)classB;
        }
    }

Приведение типа здесь сработает, но только потому, что эти классы находятся в наследственных отношениях. Два обособленных непримитивных типа привести друг к другу нельзя.

Итак, наш последний блок кода:

/// <summary>
/// Program: используется для запуска программы.
/// Contains Main method.
/// </summary>
public class Program
{
    private static void Main(string[] args)
    {
        int integerA = 10;
        char characterB = 'A';
        integerA = characterB;
        characterB = integerA;
    }
}

Error: Cannot implicitly convert type ‘int’ to ‘char’. An explicit conversion exists (are you missing a cast?)

Что нужно запомнить: можно конвертировать char в int. Нельзя конвертировать int в char (причина в том, что диапазон целого числа больше, чем символа).

Заключение

В этой части мы рассмотрели наследование. Мы попробовали запускать разные варианты кода, чтобы возможно глубже понять суть этого принципа. *этот текст будет изменён после перевода следующей статьи* In my next article, we’ll be discussing about run time polymorphism. Inheritance plays a very important role in run time polymorphism.

Вот что вы должны были запомнить за сегодня:

  • как сын получается похожим на отца, наследует его черты, так и дочерний класс имеет параметры родительского;
  • ничто не может помешать создать в дочернем классе такой же метод, как и в родительском;
  • методы дочерних классов имеют приоритет при выполнении;
  • ключевое слово base может быть использовано для обращения к методам класса-предка;
  • наследование не работает в обратном направлении;
  • кроме конструкторов и деструкторов, дочерний класс получает от родителя абсолютно всё;
  • ваши классы не могут быть унаследованы от встроенных классов вроде System.ValueType, System.Enum, System.Delegate, System.Array и т.д.;
  • класс может иметь только одного родителя, множественное наследование классов в C# не поддерживается;
  • классы не могут наследоваться циклически (1-й от 2-го, 2-й от 3-го 3-й от 1-го), это невозможно чисто логически;
  • вы можете назначить переменной родительского типа объект дочернего, но не наоборот;
  • можно конвертировать char в int. Нельзя конвертировать int в char (причина в том, что диапазон целого числа больше, чем символа).

Напоминаем вам, что в первой статье этой серии вы можете прочитать о полиморфизме. Продолжайте учиться программировать с нами!

Перевод статьи «Diving in OOP (Day 2): Polymorphism and Inheritance (Inheritance)»