C#. Классы и объекты. Начальное знакомство с ООП
Язык C# является объектно-ориентированным языком программирования. В рамках данного урока мы познакомимся с этой парадигмой, и с тем, как создавать и работать с классами и объектами в C#.
Основы Объектно-Ориентированного Программирования
Объектно-Ориентированное Программирование (ООП) является одной из наиболее популярных парадигм в мире промышленной разработки программного обеспечения. Из других парадигм программирования следует выделить – структурное программирование (основной представитель этого направления – это язык C) и функциональное программирование (к этой группе относятся языки Haskell, F#, Clojure).
Основными строительными элементами ООП являются классы и объекты. Для интуитивного понимания этих понятий приведем такой пример: аналогом класса в реальной жизни является чертеж здания или автомобиля, т.е. некоторый шаблон. Объект – это непосредственно реализация класса в виде некоторой сущности, в нашей аналогии – это конкретное здание или конкретный автомобиль, выполненный по чертежу.
Выделяют три основных “столпа” ООП- это инкапсуляция, наследование и полиморфизм.
Инкапсуляция
Инкапсуляция предполагает два основных момента:
“Житейским” примером первого аспекта – сокрытия деталей реализации, может служить автомобиль. Вся его сложность скрыта от пользователя, и нет необходимости разбираться в том, как автомобиль работает, чтобы им пользоваться. Связываение данных и методов предполагает, что в рамках одного класса располагаются данные, определяющие некоторые свойства сущности (например, имя и возраст, если сущность – это человек), и методы для их обработки, получения и изменения.
Наследование
Наследование – это концепция, которая предполагает, что один класс может наследовать функции и данные другого класса. Класс, от которого производится наследование называется родительским или базовым классом, класс который наследует – наследником. Отношение между классом наследником и базовым классом можно определить словом “является”.
Например, представим, что у нас есть базовый класс Фигура, и этот класс содержит только одно свойство – Цвет. Тогда про класс Круг – наследник класса Фигура, можно сказать так: Круг “является” Фигурой. Чего нельзя сказать про отношение между Автомобилем и Двигателем, т.е. Автомобиль не является Двигателем. Это означает, что создание иерархии наследования, в которой Автомобиль – это неследник от Двигателя было бы ошибочной (такой тип отношений назвается композиция).
Полиморфизм
Говоря про полиморфизм в общем, можно сказать, что это возможность обработки данных разных типов одной и той же функцией. Различают параметрический полиморфизм и ad–hoc полиморфизм. Параметрический полиморфизм предполагает, что один и тот же код в функции может работать с данными разных типов. Ad–hoc полиморфизм предполагает создание различных реализаций функций в зависимости от типа аргумента(ов), при этом их сигнатура (без учета типов входных аргументов) остается одной и той же.
В рамках данного урока мы обзорно познакомимся с основными синтаксическими конструкциями языка C# для разработки в рамках ООП парадигмы. В следующих уроках, будет более глубокое погружение в аспекты этой методологии, и ее применение в C#.
Классы
Класс в языке C# объявляется с помощью ключевого слова class перед ним могут стоять несколько модификаторов, после располагается имя класса. Если предполагается, что класс является наследником другого класса или реализует один или несколько интерфейсов, то они отделяются двоеточием от имени класса и перечисляются через запятую.
class Building { }
Внутри себя, класс может содержать методы, поля и свойства. Методы похожи на функции из языков группы структурного программирования. Фактически они определяют то, как можно работать с данным классом или объектами класса. Поля – это переменные, связанные с данным классом, а свойства – это конструкции специального вида, которые упрощают работу с полями (в первом приближении такого понимания будет достаточно).
class DemoClass { // Поле класса int field = 0; // Свойство класса public int Property {get;set;} // Метод класса public void Method() { Console.WriteLine("Method"); } }
Далее, мы подробно остановимся на каждом из перечисленных составляющих класса.
Объект класса создается также как переменная любого, уже известного вам по предыдущим урокам, типа данных. Напомним, что класс является типом данных, который мы можем самостоятельно объявить. Все встроенные типы являются определенными классами, например, переменная типа int – это объект класса System.Int32.
Создадим объект класса DemoClass:
// Создание объекта класса DemoClass
DemoClass demo = new DemoClass();
// Вызов метода Method объекта demo
demo.Method();
С методами встроенных типов мы уже встречались ранее, например, метод поиска элемента в строке, или преобразование строки в число и т.п.
Модификаторы доступа
Модификаторы доступа определяют область видимости как непосредственно самого класса, так и его составляющих (поля, свойства, методы).
В C# доступны следующие модификаторы доступа: public, private, protected, internal, protected internal, private protected. На текущий момент нас будут интересовать public и private, с остальными разберемся в одном из следующих уроков.
- для класса: указывает на то, что класс доступен снаружи из сборки;
- для составляющих класса: указывает на то, что они могут быть доступны вне класса, к ним можно получить доступ из любого места в программе.
- для класса: указывает на то, что класс доступен только внутри сборки.
- для составляющих класса: указывает на то, что они доступны только внутри класса.
Если модификатор не указан, то будет использован private.
Конструктор класса
Конструктор класса – это специальный метод, который вызывается при инициализации объекта с помощью ключевого слова new. Имя конструктора должно совпадать с именем класса, в сигнатуре конструктора не указывается тип возвращаемого значения.
Вот так будет выглядеть самый простой вариант конструктора для класса DemoClass:
// Конструктор класса public DemoClass() {}
Про конструктор класса нужно знать следующее:
- Конструктор можно не создавать явно, тогда для класса будет создан конструктор по умолчанию, он выглядит следующим образом:
public DemoClass() {}
- Конструкторов может быть несколько, они должны отличаться по количеству и типу принимаемых аргументов:
public DemoClass(int field) { this.field = field; } public DemoClass(int field, int prop) { this.field = field; Property = prop; }
- Если вы создадите конструктор, который содержит набор аргументов, то конструктор по умолчанию уже не будет создан для класса. Если вы хотите создавать объекты без указания аргументов, то необходимо добавить в класс соответствующий конструктор.
Инициализация объектов класса
Инициализация объекта класса производится с помощью ключевого слова new. Создадим объект класса DemoClass:
DemoClass demo = new DemoClass();
Если у класса есть несколько конструкторов, то при инициализации можно выбрать один из существующих:
DemoClass d2 = new DemoClass(1); d2.Method(); // field: 1, Property: 0 DemoClass d3 = new DemoClass(1, 2); d3.Method(); // field: 1, Property: 2
Доступна возможность использования неявного объявления с помощью ключевого слова var:
var d4 = new DemoClass();
Если у класса есть публичные свойства, то им можно присвоить значения при инициализации:
var d5 = new DemoClass(10) { Property = 11 }; d5.Method(); // field: 10, Property: 11
Методы
Методом класса называют функцию или процедуру, которая принадлежит классу или объекту. Отличие функции от процедуры в том, что функция возвращает значение, а процедура нет. В общем виде синтаксис объявления метода выглядит следующим образом:
модификатор(ы) тип_возвращаемого_значения имя_функции(аргументы)
Модификаторы определяют область видимости, принадлежность метода объекту или классу, является ли метод переопределением и т.п. Тип возвращаемого значения – это любой доступный в C# тип. В качестве типа возвращаемого значения не может использоваться ключевое слово var. Если метод не возвращает ничего, то указывается тип void. Метод может содержать ноль или более аргументов, которые также могут иметь специальные модификаторы, указывающие на то является ли аргумент входным или выходным и т.п. Более подробно про все эти аспекты будет рассказано в одном из уроков, посвященных более глубокому изучению ООП в C#. В рамках данного урока, наша задача – это на интуитивном уровне научиться принципам работы с классами в C#.
Работа с модификатором доступа
Если метод объявлен с модификатором public, то его можно использовать вне класса, например метод Printer из DemoClass:
public void Printer() { Console.WriteLine(quot;field: {field}, Property: {Property}"); }
Такой метод может вызываться в любом месте программы у соответствующих объектов:
var d6 = new DemoClass(11) { Property = 12 }; d6.Printer(); // field: 11, Property: 12
Если мы объявим метод с модификатором private или без модификатора (тогда, по умолчанию, будет принят private), то его уже нельзя будет вызвать снаружи класса:
class DemoClass { // ... private void PrivateMethod() { Console.WriteLine(quot;Secret method"); } // ... }
var d7 = new DemoClass(); d7.PrivateMethod(); // Ошибка компиляции!!!
Но при этом внутри класса его вызвать можно:
class DemoClass { // ... public void PublicMethod() { Console.WriteLine(quot;Public method"); PrivateMethod(); } // ... }
Статические методы и методы объекта
Различают статические методы и методы объекта. Статические имеют модификатор static перед именем метода и принадлежат классу. Для вызова таких методов не обязательно создавать экземпляры класса, мы уже пользовались такими методами из класса Console – это методы Write и WriteLine. Для вызова метода объекта, необходимо предварительно создать экземпляр класса, пример – это метод PublicMethod и Priter у класса DemoClass. Добавим статический метод и метод класса в DemoClass:
class DemoClass { // ... public static void StaticMethod() { Console.WriteLine("Message from static method"); } public void NoneStaticMethod() { Console.WriteLine("Message from non static method"); } // ... }
Вызовем эти методы из класса DemoClass в методе Main:
DemoClass.StaticMethod(); // Message from static method var d8 = new DemoClass(); d8.NoneStaticMethod(); // Message from none static method
Методы принимающие аргументы и возвращающие значения
Как было сказано в начале данного раздела, методы могут принимать данные через аргументы и возвращать значения, продемонстрируем эту возможность на примере:
class DemoClass { // ... public int MulField(int value) { return field * value; } // ... }
var d8 = new DemoClass(10); Console.WriteLine(quot;MulField() result: {d8.MulField(2)}"); // MulField() result: 20
Поля
Поле представляет собой переменную любого типа, объявленную внутри класса. Через модификатор доступа можно управлять уровнем доступа к полю (так же как для методов), через ключевое слово static можно определять принадлежность поля объекту либо классу:
class DemoClass { // ... int field = 0; public int publicField = 0; public static int publicStaticField = 0; // ... }
var d9 = new DemoClass(); // Доступ к private полям запрещен // Console.WriteLine(quot;Get private field: {d9.field}"); // Compile ERROR // Доступ к полю объекта d9.publicField = 123; Console.WriteLine(quot;Get public field: {d9.publicField}"); // Get public field: 123 // Доступ к статическому полю класса DemoClass.publicStaticField = 456; Console.WriteLine(quot;Get public static field: {DemoClass.publicStaticField}"); // Get public static field: 456
Работать с открытыми полями напрямую (поля, которые имеют модификатор public) является плохой практикой. Если необходимо читать и изменять значение какого-либо поля, то лучше это делать через getter’ы и setter’ы – специальные методы, которые выполняют эту работу.
Создадим для класса Building методы для доступа и модификации значения поля height:
class Building { float height; public float GetHeight() => height; public float SetHeight(float height) => this.height = height; }
Для работы с этим классом воспользуемся следующим кодом:
var b1 = new Building(); b1.SetHeight(12); Console.WriteLine(quot;Height of building: {b1.GetHeight()}");
Создание специальных методов для работы с полями – возможный вариант, но в C# принят подход работы через свойства. Им посвящен следующий раздел.
Свойства
Основная причина создания getter’ов и setter’ов – это внедрение дополнительной логики, которая может потребоваться перед тем как выдать значение поля или присвоить ему новое значение. Для упрощения процесса решения такой задачи язык C# предлагает альтернативу – Свойства. С точки зрения пользователя класса работа с свойствами похожа на работу с открытыми полями. Но внутри, “под капотом”, это организовано так, что мы можем добавлять логику при чтении и записи.
Добавим в класс Building следующую конструкцию:
public float Height { get => height; set => height = value; }
Она описывает свойство Height. У свойства есть два ключевых слова – это get, оно определяет блок кода, который выполнится при чтении значения свойства, и set – выполнится при присвоении значения свойству. Причем, при необходимости, объявить можно только одно из них, например, если у свойства будет только get, то ему нельзя будет присвоить значение.
Для демонстрации работы с этим свойством напишем следующий код:
var b2 = new Building(); b2.Height = 456; Console.WriteLine(quot;Height of building: {b2.Height}"); // Height of building: 456
Если для поля не предполагается, в текущий момент, какой-то дополнительной логики обработки при работе с ним, и это поле должно быть открытым, то в любом случае, лучший вариант – сделать свойство с пустыми get и set, так как в будущем ситуация может измениться. Добавим в наш класс Building ещё два свойства, которые пока не связаны с какими-то конкретными полями:
public float Width { get; set; } public float Length { get; set; }
С ними можно работать также как и с Height.
Ключевое слово this
Ключевое слово this используется внутри класса для ссылки на текущий экземпляр класса. Чаще всего его приходится использовать в методах для доступа к полям класса. Например, если у класса Building, с которым мы работали в предыдущем разделе, есть поле height, метод SetHeight был создан нами для того, чтобы присваивать этому полю значение, аргумент этого метода – переменная с именем height:
В теле данного метода мы должны как-то явно задать, что значение этого аргумента будет присвоено приватному полю height, записать height = height мы не можем, так как в этом случае нет четкого понимания, что чему будет присваиваться. В этом случае, для явного указания, что мы хотим присвоить значение полю класса, следует использовать ключевое слово this так, как мы это реализовали в методе SetHeight:
public float SetHeight(float height) => this.height = height;
Ключевое слово static
В предыдущих разделах мы уже сталкивались с понятием статического метода и поля. Ещё раз обговорим этот момент. Если метод, поле или свойство имеет модификатор static, то это означает что они принадлежат уровню класса, а не объекту. То есть для работы с соответствующими элементами не нужно создавать экземпляр класса, можно работать напрямую через имя класса. Если класс объявлен с ключевым словом static, то такой класс называют статическим, он обладает следующими особенностями:
- у такого класса все методы должны быть статическими;
- его нельзя наследовать (класс является запечатанным);
- класс не может содержать конструкторы экземпляров;
- нельзя создавать экземпляры таких классов.
Примером статического класса может быть класс Math из стандартной библиотеки C#.