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

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


Я много писал на смежные темы, вроде концепции MVC, Entity Framework, паттерна «Репозиторий» и т.п. Моим приоритетом всегда было полное раскрытие темы, чтобы читателю не приходилось гуглить недостающие детали. Этот цикл статей опишет абсолютно все концепции ООП, которые могут интересовать начинающих разработчиков. Однако эта статья предназначена не только для тех, кто начинает свой путь в программировании: она написана и для опытных программистов, которым может потребоваться освежить свои знания.

Сразу скажу, далеко в теорию мы вдаваться не будем — нас интересуют специфичные вопросы. Где это будет нужно, я буду сопровождать повествование кодом на C#.

Что такое ООП и в чём его плюсы?

«ООП» значит «Объектно-Ориентированное Программирование». Это такой подход к написанию программ, который основывается на объектах, а не на функциях и процедурах. Эта модель ставит в центр внимания объекты, а не действия, данные, а не логику. Объект — реализация класса. Все реализации одного класса похожи друг на друга, но могут иметь разные параметры и значения. Объекты могут задействовать методы, специфичные для них.

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

Что за концепции ООП?

Сейчас коротко о принципах, которые мы позже рассмотрим в подробностях:

  • Абстракция данных: подробности внутренней логики скрыты от конечного пользователя. Пользователю не нужно знать, как работает те или иные классы и методы, чтоб их использовать. Подходящим примером из реальной жизни будет велосипед — когда мы ездим на нём или меняем деталь, нам не нужно знать, как педаль приводит его в движение или как закреплена цепь.
  • Наследование: самый популярный принцип ООП. Наследование делает возможным повторное использование кода — если какой-то класс уже имеет какую-то логику и функции, нам не нужно переписывать всё это заново для создания нового класса, мы можем просто включить старый класс в новый, целиком.
  • Инкапсуляция: включение в класс объектов другого класса, вопросы доступа к ним, их видимости.
  • Полиморфизм: «поли» значит «много», а «морфизм» — «изменение» или «вариативность», таким образом, «полиморфизм» — это свойство одних и тех же объектов и методов принимать разные формы.
  • Обмен сообщениями: способность одних объектов вызывать методы других объектов, передавая им управление.

Ладно, тут мы коснулись большого количества теории, настало время действовать. Я надеюсь, это будет интересно.

Полиморфизм

В этой статье мы рассмотрим буквально все сценарии использования полиморфизма, использование параметров и разные возможные типы мышления во время написания кода.

Перегрузка методов

  • Давайте создадим консольное приложение InheritanceAndPolymorphism и класс Overload.cs с тремя методами DisplayOverload с параметрами, как ниже:
    public class Overload
      {
          public void DisplayOverload(int a){
              System.Console.WriteLine("DisplayOverload " + a);
          }
          public void DisplayOverload(string a){
              System.Console.WriteLine("DisplayOverload " + a);
          }
          public void DisplayOverload(string a, int b){
              System.Console.WriteLine("DisplayOverload " + a + b);
          }
      }

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

    class Program
        {
            static void Main(string[] args)
            {
                Overload overload = new Overload();
                overload.DisplayOverload(100);
                overload.DisplayOverload("method overloading");
                overload.DisplayOverload("method overloading", 100);
                Console.ReadKey();
            }
        }

    И теперь, когда мы это запустим, вывод будет следующим:

    DisplayOverload 100
    DisplayOverload method overloading
    DisplayOverload method overloading100

    Класс Overload содержит три метода, и все они называются DisplayOverload, они различаются только типами параметров. В C# (как и в большистве других языков) мы можем создавать методы с одинаковыми именами, но разными параметрами, это и называется «перегрузка методов». Это значит, что нам нет нужды запоминать кучу имён методов, которые совершают одинаковые действия с разными типами данных.

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

  • Если же мы запустим следующий код:
    public void DisplayOverload() { }
    public int DisplayOverload(){ }

    Мы получим ошибку компиляции:

    Error: Type ‘InheritanceAndPolymorphism.Overload’ already defines a member called ‘DisplayOverload’ with the same parameter types

    Здесь вы можете видеть две функции, которые различаются только по возвращаемому типу, и скомпилировать это нельзя.

    Что нужно запомнить: метод не идентифицируется по возвращаемому типу, это не полиморфизм.

  • Если мы попробуем скомпилировать
    static void DisplayOverload(int a)  {   }
    public void DisplayOverload(int a) {   }
    public void DisplayOverload(string a){  }

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

    Error: Type ‘InheritanceAndPolymorphism.Overload’ already defines a member called ‘DisplayOverload’ with the same parameter types

    Здесь присутствуют два метода, принимающих целое число в качестве аргумента, с той лишь разницей, что один из них помечен как статический.

    Что нужно запомнить: модификаторы вроде static также не являются свойствами, идентифицирующими метод.

  • Если мы запустим нижеследующий код, в надежде, что теперь-то идентификаторы у методов будут разными:
    private void DisplayOverload(int a) {   }
    
    private void DisplayOverload(out int a)
            {
                a = 100;
            }
    
    private void DisplayOverload(ref int a) {   }

    То нас ждёт разочарование:

    Error: Cannot define overloaded method ‘DisplayOverload’ because it differs from another method only on ref and out

    Что нужно запомнить: на идентификатор метода оказывают влияние только его имя и параметры (их тип, количество). Модификаторы доступа не влияют. Двух методов с одинаковыми идентификаторами существовать не может.

Роль ключевого слова params в полиморфизме

Параметры могут быть четырёх разных видов:

  • переданное значение;
  • преданная ссылка;
  • параметр для вывода;
  • массив параметров.

С первыми тремя мы, вроде, разобрались, теперь подробнее взглянем на четвёртый.

  • Если мы запустим следующий код:
public void DisplayOverload(int a, string a)  {   }

        public void Display(int a)
        {
            string a;
        }

То получим две ошибки:

Error1: The parameter name ‘a’ is a duplicate

Error2: A local variable named ‘a’ cannot be declared in this scope because it would give a different meaning to ‘a’, which is already used in a ‘parent or current’ scope to denote something else

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

  • Теперь попробуем запустить следующий код:

Overload.cs

public class Overload
    {
        private string name = "Akhil";

        public void Display()
        {
            Display2(ref name, ref name);
            System.Console.WriteLine(name);
        }

        private void Display2(ref string x, ref string y)
        {
            System.Console.WriteLine(name);
            x = "Akhil 1";
            System.Console.WriteLine(name);
            y = "Akhil 2";
            System.Console.WriteLine(name);
            name = "Akhil 3";
        }
    }

Program.cs

class Program
    {
        static void Main(string[] args)
        {
            Overload overload = new Overload();
            overload.Display();
            Console.ReadKey();
        }
    }

Мы получим следующий вывод:

Akhil
Akhil 1
Akhil 2
Akhil 3

Мы можем передавать одинаковые ссылочные параметры столько раз, сколько захотим. В методе Display строка name имеет значение «Akhil». Когда мы меняем значение x на «Akhil1», на самом деле мы меняем значение name, т.к. через параметр x передана ссылка именно на него. То же и с y — все эти три переменных ссылаются на одно место в памяти.

  • Теперь самое интересное:

Overload.cs

public class Overload
    {
        public void Display()
        {
            DisplayOverload(100, "Akhil", "Mittal", "OOP");
            DisplayOverload(200, "Akhil");
            DisplayOverload(300);
        }

        private void DisplayOverload(int a, params string[] parameterArray)
        {
            foreach (string str in parameterArray)
               Console.WriteLine(str + " " + a);
        }
    }

Program.cs

class Program
    {
        static void Main(string[] args)
        {
            Overload overload = new Overload();
            overload.Display();
            Console.ReadKey();
        }
    }

Это даст нам такой вывод:

Akhil 100
Mittal 100
OOP 100
Akhil 200

Нам часто может потребоваться передать методу n параметров. В C# такую возможность предоставляет ключевое слово params.

Важно: это ключевое слово может быть применено только к последнему аргументу метода, так что метод ниже работать не будет:

private void DisplayOverload(int a, params string[] parameterArray, int b) {  }
  • В случае DisplayOverload первый аргумент должен быть целым числом, а остальные — сколь угодно много строк или наоборот, ни одной.
    public class Overload
    {
        public void Display()
        {
            DisplayOverload(100, 200, 300);
            DisplayOverload(200, 100);
            DisplayOverload(200);
        }
    
        private void DisplayOverload(int a, params int[] parameterArray)
        {
            foreach (var i in parameterArray)
                Console.WriteLine(i + " " + a);
        }
    
    }
    
    //Program.cs тот же, что и в предыдущем примере

    Вывод:

    200 100
    300 100
    100 200

    Важно запомнить: C# достаточно умён, чтоб разделить обычные параметры и массив параметров, даже если они одного типа.

  • Посмотрите на следующие два метода:
    private void DisplayOverload(int a, params string[][] parameterArray)  {     }
    private void DisplayOverload(int a, params string[,] parameterArray)    {    }

    Разница между ними в том, что первый запустится, и такая синтаксическая конструкция будет подразумевать, что в метод будет передаваться n массивов строк. Вторая же выдаст ошибку:

    Error: The parameter array must be a single dimensional array

    Запомните: массив параметров должен быть одномерным.

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

Overload.cs

public class Overload
    {
        public void Display()
        {
            string[] names = {"Akhil", "Ekta", "Arsh"};
            DisplayOverload(3, names);
        }

        private void DisplayOverload(int a, params string[] parameterArray)
        {
            foreach (var s in parameterArray)
                Console.WriteLine(s + " " + a);
        }

    }

Program.cs

class Program
    {
        static void Main(string[] args)
        {
            Overload overload = new Overload();
            overload.Display();
            Console.ReadKey();
        }
    }

Вывод будет следующим:

Akhil 3
Ekta 3
Arsh 3

Однако такой код:

public class Overload
    {
        public void Display()
        {
           string [] names = {"Akhil","Arsh"};
           DisplayOverload(2, names, "Ekta");
        }

        private void DisplayOverload(int a, params string[] parameterArray)
        {
            foreach (var str in parameterArray)
                Console.WriteLine(str + " " + a);
        }

    }

Уже вызовет ошибку:

Error: The best overloaded method match for ‘InheritanceAndPolymorphism.Overload.DisplayOverload(int, params string[])’ has some invalid arguments

Error:Argument 2: cannot convert from ‘string[]’ to ‘string’

Думаю, тут всё понятно — или, или. Смешивать передачу отдельными параметрами и одним массивом нельзя.

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

Overload.cs

public class Overload
    {
        public void Display()
        {
            int[] numbers = {10, 20, 30};
            DisplayOverload(40, numbers);
            Console.WriteLine(numbers[1]);
        }

        private void DisplayOverload(int a, params int[] parameterArray)
        {
            parameterArray[1] = 1000;
        }

    }

Program.cs

class Program
    {
        static void Main(string[] args)
        {
            Overload overload = new Overload();
            overload.Display();
            Console.ReadKey();
        }
    }

После её выполнения мы получим в консоли:

1000

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

public class Overload
    {
        public void Display()
        {
            int number = 102;
            DisplayOverload(200, 1000, number, 200);
            Console.WriteLine(number);
        }

        private void DisplayOverload(int a, params int[] parameterArray)
        {
            parameterArray[1] = 3000;
        }

    }

Результатом выполнения такого кода будет

102

Ведь из переданных параметров C# автоматически формирует новый, временный массив.

  • Теперь поговорим о приоритете языка в выборе методов. Предположим, у нас есть такой код:
public class Overload
    {
        public void Display()
        {
            DisplayOverload(200);
            DisplayOverload(200, 300);
            DisplayOverload(200, 300, 500, 600);
        }

        private void DisplayOverload(int x, int y)
        {
            Console.WriteLine("The two integers " + x + " " + y);
        }

        private void DisplayOverload(params int[] parameterArray)
        {
            Console.WriteLine("parameterArray");
        }

    }
///Program.cs всё тот же

C# рассматривает методы с массивом параметров последними, так что во втором случае будет вызван метод, принимающий два целых числа. В первом и третьем случае будет вызван метод с params, так как ничего кроме него запустить невозможно. Таким образом, на выходе мы получим:

parameterArray
The two integers 200 300
parameterArray

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

Overload.cs

public class Overload
    {
        public static void Display(params object[] objectParamArray)
        {
            foreach (object obj in objectParamArray)
            {
                Console.Write(obj.GetType().FullName + " ");
            }
            Console.WriteLine();

        }
    }

Program.cs

class Program
    {
        static void Main(string[] args)
        {
            object[] objArray = { 100, "Akhil", 200.300 }; //Массив 
            object obj = objArray; //Массив как объект
            Overload.Display(objArray);
            Overload.Display((object)objArray); //Массив, приведённый к объекту
            Overload.Display(obj);
///Почему бы не пойти глубже? 😀
            Overload.Display((object[])obj); //Массив, как объект, приведённый к массиву
            Console.ReadKey();

        }
    }

В консоли мы увидим:

System.Int32 System.String System.Double
System.Object[] System.Object[] System.Int32 System.String System.Double

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

В заключение

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

  • Метод идентифицируется не только по имени, но и по его параметрам.
  • Метод не идентифицируется по возвращаемому типу.
  • Модификаторы вроде static также не являются свойствами, идентифицирующими метод.
  • На идентификатор метода оказывают влияние только его имя и параметры (их тип, количество). Модификаторы доступа не влияют. Двух методов с одинаковыми идентификаторами существовать не может.
  • Имена параметров должны быть уникальны. Также не могут быть одинаковыми имя параметра метода и имя переменной, созданной в этом же методе.
  • Ключевое слово params может быть применено только к последнему аргументу метода.
  • C# достаточно умён, чтоб разделить обычные параметры и массив параметров, даже если они одного типа.
  • Массив параметров должен быть одномерным.

Источник: «Diving in OOP (Day 1) : Polymorphism and Inheritance (Early Binding/Compile Time Polymorphism)»