👨🎓️ Самоучитель по C# для начинающих за 30 минут. Часть 2: ООП и коллекции
В этой статье рассмотрим основные принципы объектно-ориентированного программирования, коллекции и другие аспекты на языке C#, без которых программировать будет не совсем комфортно.
ООП
Процедурное программирование строится на написании процедур или методов, которые выполняют операции над данными, а объектно-ориентированное программирование на создании объектов, содержащих как данные, так и методы.
ООП основано на использовании трёх основных принципов:
- Инкапсуляция – это механизм, позволяющий скрыть код класса (поля, методы, функции) и ограничить доступ к коду и данным из других участков кода.
- Наследование – это механизм, позволяющий классу наследовать свойства другого класса, принимая все поля и функции членов базового типа.
- Полиморфизм – это свойство, которое позволяет использовать один интерфейс для общего класса действий. Эта концепция часто выражается как "один интерфейс, несколько действий".
Объектно-ориентированное программирование имеет ряд преимуществ перед процедурным программированием, основными из них являются: быстродействие и многократное использование кода.
Классы и объекты
Язык C# является объектно-ориентированным языком программирования, это позволяет разрабатывать на нём большие программные системы, с возможностью модульной заменой многократно используемого кода.
В C# классы и объекты, а также их атрибуты и методы, являются базовыми инструментами для реализации ООП. Например, смартфон – это объект, имеющий атрибуты, такие как диагональ экрана, установленный процессор, цвет корпуса и методы, такие как вывод на экран, отправка/принятие звонка и другие.
C# поддерживает описание, реализацию и использование классов в одном файле кода и раздельно. Будем использовать именно раздельный подход.
Члены класса: поля и методы
Для добавления класса в проект, в его контекстном меню выбираем пункт Add new item/Добавить новый элемент и в появившемся окне выбираем – Class/Класс и вводим его имя.
В результате будет создан файл .cs с именем класса Phone. В нём будет объявлен класс с помощью ключевого слова class и объявление класса может иметь следующий вид:
class Phone
{
public string manufacturer;
public string model;
public double screenSize;
public void unlockDisplay()
{
Console.WriteLine("Экран разблокирован");
}
}
Класс может включать в себя поля (fields) – переменные и методы, например, у класса Phone есть два поля и один метод. Рассмотрим простой пример использования класса.
using System;
namespace SimpleOOP
{
class Program
{
static void Main()
{
Phone apple = new Phone();
apple.manufacturer = "Apple";
apple.model = "IPhone 13";
apple.screenSize = 5.8;
Phone samsung = new Phone();
samsung.manufacturer = "Samsung";
samsung.model = "Galaxy S21";
samsung.screenSize = 6.5;
Console.WriteLine("Смартфон: " + apple.manufacturer + " " + apple.model);
apple.unlockScreen();
Console.WriteLine("Смартфон: " + samsung.manufacturer + " " + samsung.model);
samsung.blockScreen();
Console.ReadKey();
}
}
}
Поля и методы являются членами класса, в нашем случае они объявлены с модификатором public, чтобы их можно было использовать в коде программы (класс Program). Для создания объектов используется конструктор по умолчанию new Phone().
В консоль будет выведено следующее:
В результате выполнения выведены значения полей и отработаны методы класса. Но для более эффективного программирования классов используются конструкторы.
Конструкторы
Конструктор – метод, имеющий имя класса, который используется для создания и инициализации объектов. Конструктор вызывается при создании объекта класса и его можно использовать для инициализации начальных значений полей.
Существуют следующие типы конструкторов:
- конструктор без параметров;
- параметризованный конструктор;
- конструктор по умолчанию.
Конструктор по умолчанию создается в языке C#, если в классе не реализованы конструкторы.
Имя конструктора должно совпадать с именем класса и не может иметь тип возвращаемого значения.
Модифицируем класс Phone, добавив в него конструктор без параметров и с параметрами:
class Phone
{
public string manufacturer;
public string model;
public Phone()
{
manufacturer = "Apple";
model = "IPhone X";
}
public Phone(string manufacturer, string model)
{
this.manufacturer = manufacturer;
this.model = model;
}
}
class Program
{
static void Main()
{
Phone apple = new Phone();
Phone samsung = new Phone("Samsung","Galaxy S20 Ultra");
Console.WriteLine("Смартфон: " + apple.manufacturer + " " + apple.model);
Console.WriteLine("Смартфон: " + samsung.manufacturer + " " + samsung.model);
Console.ReadKey();
}
}
Из примера видно, что класс Phone содержит два конструктора, поэтому у объектов будет разная инициализация, apple будет иметь значения из конструктора. Для инициализации объекта samsung используется конструктор с параметрами, которые задаются при его вызове. Ниже приведён результат работы примера.
Конструкторы, как и другие методы можно перегружать, используя разные параметры.
Модификаторы доступа
Модификаторы доступа используется для установки уровня доступа/видимости для классов, полей, методов и свойств. К модификаторам доступа относятся такие ключевые слова, как public, private, protected и internal, ранее использовался нами только public.
| Модификатор | Описание |
| public | Код доступен для всех классов |
| private | Код доступен только в пределах одного класса |
| Protected | Код доступен в пределах одного класса или в классе, который наследуется от этого класса |
| Internal | Код доступен только в пределах собственной сборки, но не из другой сборки |
Если мы объявим поля нашего класса Phone или методы как private, то они станут недоступны для использования в других классах, например в Program. Ограничение доступа и является основой принципа ООП – инкапсуляции.
Инкапсуляция и свойства
Инкапсуляция позволяет скрыть конфиденциальные данные от пользователей. Чтобы достичь этого, необходимо:
- объявить поля/переменные как
private; - обеспечить
public getиsetметоды через свойства для доступа и изменения значенияprivateполя.
Доступ к private переменным возможен только внутри одного и того же класса. Иногда нужно получить к ним доступ вне класса – и это можно сделать с помощью свойств.
Свойство похоже на комбинацию переменной и метода и имеет два метода: get() и set().
class Phone
{
private string manufacturer;
private string model;
public string Manufacturer
{
get { return manufacturer; }
set { manufacturer = value; }
}
public string Model
{
get { return model; }
set { model = value; }
}
}
class Program
{
static void Main()
{
Phone apple = new Phone();
apple.Manufacturer = "Apple";
apple.Model = "IPhone";
Console.WriteLine("Смартфон: " + apple.Manufacturer + " " + apple.Model);
Console.ReadKey();
}
}
Свойства, Manufacturer и Model связаны с полями manufacturer и model соответственно.
Рекомендуется использовать одно и то же имя как для свойства, так и для частного поля, но с первой буквой в верхнем регистре.
Метод get() возвращает значение приватной переменной. Метод set() присваивает значение переменной. Ключевое слово value представляет значение, которое мы присваиваем свойству.
Теперь можно получать и задавать значения свойств вне класса их содержащим. Результат выполнения примера, следующий:
Поля можно сделать доступными только для чтения, используя get() или только для записи, используя set().
C# позволяет использовать автоматические свойства, где не нужно определять поле для свойства, а достаточно написать get; и set; внутри свойства. Изменив класс Phone, следующим образом:
class Phone
{
public string Manufacturer
{
get; set;
}
public string Model
{
get; set;
}
}
Получим тот же результат в консоли программы.
Инкапсуляция улучшает контроль над членами класса и снижает вероятность повреждения кода.
Наследование
В C# поля и методы могут наследоваться от одного класса к другому. В наследовании используются два понятия:
- Базовый класс – класс, от которого наследуется.
- Производный класс – класс, который наследуется от базового класса.
На примере ниже класс Smartphone наследует поля и методы от класса Phone:
class Phone
{
public string manufacturer = "Apple";
public string model = "IPhone XR";
public void print()
{
Console.WriteLine("Мир Apple!");
}
}
class Smartphone : Phone
{
public string color = "Red";
}
class Program
{
static void Main()
{
Smartphone apple = new Smartphone();
apple.print();
Console.WriteLine("Смартфон: " + apple.manufacturer + " " + apple.model + "\nЦвет корпуса: " + apple.color);
Console.ReadKey();
}
}
Как видно из кода, класс Smartphone наследует от класса Phone поля и метод, что позволяет использовать их в программе, инициализировав только объект Smartphone(). Результат работы примера приведён ниже:
Результат показывает, что класс Smartphone успешно унаследовал от класса Phone поля и метод.
Наследование позволяет существенно сократить объем кода, так как не требуется повторная реализация участков кода в других классах.
Полиморфизм и перегрузка методов
Принцип полиморфизма позволяет методам классов иметь не одну, а несколько форм, и он необходим, когда у нас есть много классов, связанных друг с другом путем наследования.
Как было сказано выше, наследование позволяет наследовать поля и методы от другого класса. Полиморфизм использует эти методы для выполнения различных задач. Это позволяет нам выполнять одно и то же действие разными способами.
Например, базовый класс Device, у которого есть метод enabledScreen(). Производными классами устройств могут быть смартфоны, планшеты и у них также есть собственная реализация вывода информации на экран:
class Device
{
public virtual void enableScreen()
{
Console.WriteLine("Screen Enabled!");
}
}
class Phone : Device
{
public override void enableScreen()
{
Console.WriteLine("Hello, I am Phone!");
}
}
class Tablet : Device
{
public override void enableScreen()
{
Console.WriteLine("Hello, I am Tablet!");
}
}
class Program
{
static void Main(string[] args)
{
Device device = new Device();
Phone phone = new Phone();
Tablet tablet = new Tablet();
device.enableScreen();
phone.enableScreen();
tablet.enableScreen();
}
}
В базовом классе объявлен метод screenEnabled(), как virtual. В классах наследниках, используя ключевое слово override, перегружаем метод для каждого класса по-своему.
Все объекты успешно выполнили свои методы, благодаря их перегрузке.
Абстрактные классы и методы
Абстракция данных – это процесс сокрытия определенных деталей и показа пользователю только важной информации. Абстракция может быть достигнута либо с помощью абстрактных классов, либо с помощью интерфейсов.
Ключевое слово abstract используется для классов и методов:
- абстрактный класс – это ограниченный класс, который нельзя использовать для создания объектов (для доступа к нему он должен быть унаследован от другого класса);
- абстрактный метод: может использоваться только в абстрактном классе и не имеет тела. Тело предоставляется производным классом.
Абстрактный класс может иметь как абстрактные, так и обычные методы. Создавать объекты абстрактных классов нельзя. Для создания объектов должен использоваться класс наследник.
Рассмотрим на примере:
abstract class Device
{
public abstract void enableScreen();
public virtual void powerOff()
{
Console.WriteLine("Device Disabled!");
}
}
class Phone : Device
{
public override void enableScreen()
{
Console.WriteLine("Hello, I am Phone!");
}
}
class Tablet : Device
{
public override void enableScreen()
{
Console.WriteLine("Hello, I am Tablet!");
}
}
class Program
{
static void Main(string[] args)
{
Phone phone = new Phone();
Tablet tablet = new Tablet();
phone.enableScreen();
phone.powerOff();
tablet.enableScreen();
tablet.powerOff();
}
}
Пример выше показывает, что класс Device теперь абстрактный и его нельзя использовать для создания объектов напрямую. Но унаследованные от него классы Phone и Tablet без проблем используют его методы. Большей абстракции можно добиться, используя интерфейсы.
Интерфейсы
В C# можно добиться абстракции, используя интерфейсы. Interface – это полностью абстрактный класс, который может содержать только абстрактные методы и свойства (без реализации). Правильнее писать имя интерфейса с буквы I, так легче распознать, что это интерфейс, а не класс. По умолчанию члены интерфейса являются abstract и public.
Интерфейсы могут содержать свойства и методы, но не поля.
Чтобы получить доступ к методам интерфейса, интерфейс должен быть реализован в другом классе. Чтобы реализовать интерфейс, используйте : символ (так же, как с наследованием). Тело метода интерфейса предоставляется классом «реализовать». Обратите внимание, что вам не нужно использовать ключевое слово override при реализации интерфейса:
interface IDevice
{
void enableScreen();
}
class Phone : IDevice
{
public void enableScreen()
{
Console.WriteLine("Hello, I am Phone!");
}
}
class Tablet : IDevice
{
public void enableScreen()
{
Console.WriteLine("Hello, I am Tablet!");
}
}
class Program
{
static void Main(string[] args)
{
Phone phone = new Phone();
Tablet tablet = new Tablet();
phone.enableScreen();
tablet.enableScreen();
}
}
Использование интерфейса в примере показывает, что с их помощью можно добиться большего уровня абстракции.
В этом разделе статьи мы рассмотрели основные принципы ООП, необходимые для понимания работы готовых библиотечных классов и коллекций.
Коллекции
В первой части самоучителя были рассмотрены одномерные и многомерные массивы и их реализация в C#, но массивы используют фиксированную размерность и не позволяют динамически изменять размер.
Для работы с динамическими данными в C# используются коллекции. Они позволяют динамически изменять размерность данных и без каких либо проблем добавлять/изменять наборы данных.
Пространство имён System.Collections.Generic включает в себя набор коллекций для широкого спектра задач. Рассмотрим некоторые из них: List<T> и Dictoniary<T,V>.
Список
List<T> – список.
Список по структуре напоминает массив, который имеет возможность динамического расширения. При объявлении списка можно указывать встроенный или пользовательский тип данных.
Синтаксис объявления пустого списка, следующий:
List<Type> list = new List<Type>();
Если необходимо, то можно указать в скобках начальный размер списка:
List<Type> list = new List<Type> (25);
Можно сразу при объявлении проинициализировать его, добавив, например, строковые элементы:
List<string> names = new List<string> {“Michael”, “Stefan”, “Mary”};
У класса List имеются свойства, необходимые для работы со списком:
Count– получает количество элементов списка;Capacity– возвращает или задаёт количество элементов, которое список может вместить без изменения размера;Item[int32]– возвращает или задает элемент по указанному индексу.
Имеются и методы, организующие выполнение различных операций над списком, перечислим некоторые из них:
Add(T)– добавляет элемент к списку;Clear()– очистка списка;IndexOf(T)– возвращает индекс переданного элемента;ElementAt(Int32)– возвращает элемент по индексу;Insert(Int32, T)– вставляет элемент в указанную позицию;Remove(T)– удаляет указанный элемент из списка;RemoveAt(Int32)– удаляет элемент из заданной позиции;Sort()– сортирует список;Reverse()– меняет порядок расположения элементов на противоположный.
Ниже на примере рассмотрим использование методов класса List:
class Phone
{
public string Manufacturer { get; set; }
public string Model { get; set; }
}
class Program
{
static void Main(string[] args)
{
List<Phone> phones = new List<Phone> {
new Phone { Manufacturer = "Apple", Model = "IPhone X"},
new Phone { Manufacturer = "Microsoft", Model = "Lumia 535"},
new Phone{ Manufacturer ="Samsung", Model = "Galaxy S20"}
};
Console.WriteLine("Количество элементов в phones:{0}", phones.Count);
//Добавим новый элемент списка players
phones.Insert(1, new Phone { Manufacturer = "Huawei", Model = "Mate X" });
//Посмотрим на все элементы списка
phones.ForEach(p => Console.WriteLine($"{p.Manufacturer}, {p.Model}"));
//Удалим элемент из списка по индексу
phones.RemoveAt(3);
// Перевернем список
phones.Reverse();
Console.WriteLine("--------------------");
foreach (Phone phone in phones)
{
Console.WriteLine($"{phone.Manufacturer} {phone.Model}");
}
}
}
Рассмотрены некоторые методы List, результат работы ниже:
Создали список с элементами, добавили новый элемент, удалили его по индексу, перевернули список в обратном порядке. С остальными методами предлагаю поэкспериментировать самостоятельно.
Словарь
Dictoniary<T, V> – словарь.
Класс Dictionary реализует структуру данных Отображение, которую иногда называют Словарь или Ассоциативный массив. Идея довольно проста: в обычном массиве доступ к данным мы получаем через целочисленный индекс, а в словаре используется ключ, который может быть числом, строкой или любым другим типом данных, который реализует метод GetHashCode(). При добавлении нового объекта в такую коллекцию для него указывается уникальный ключ, который используется для последующего доступа к нему.
Пустой словарь можно объявить так:
var dict = new Dictionary<string, float>();
Словарь с набором элементов можно объявить так:
var numbers = new Dictionary<string, int>()
{
["one"] = 1,
["two"] = 2,
["three"] = 3
};
Console.WriteLine($"one : {numbers["one"]}");
Рассмотрим некоторые свойства и методы класса Dictionary<TKey, TValue>. Более подробно ознакомиться со всеми возможностями класса можно на официальном сайте Microsoft.
Свойства класса Dictionary:
Count– количество объектов в словаре;Keys– ключи словаря;Values– значения элементов словаря.
Методы класса Dictoniary:
Add(TKey, TValue)– добавляет в словарь элемент с заданным ключом и значением;Clear()– удаляет из словаря все ключи и значения;ContainsValue(TValue)– проверяет наличие в словаре указанного значения;ContainsKey(TKey)– проверяет наличие в словаре указанного ключа;GetEnumerator()– возвращает перечислитель для перебора элементов словаря;Remove(TKey)– удаляет элемент с указанным ключом;TryAdd(TKey, TValue)– метод, реализующий попытку добавить в словарь элемент с заданным ключом и значением;TryGetValue(TKey, TValue)– метод, реализующий попытку получить значение по заданному ключу.
На примере ниже рассмотрим работу со словарём:
using System;
namespace SimpleOOP
{
class Program
{
static void Main(string[] args)
{
Dictionary<string, string> phoneList = new Dictionary<string, string>();
phoneList.Add("Иван Гор", "+79856340978");
phoneList.Add("Олеся Марьина", "+79206318537");
phoneList.Add("Максим Хрусталев", "+74998006644");
Console.WriteLine("Количество элелементов: {0}", phoneList.Count);
phoneList["Илькин Запашный"] = "+79098576432";
if (!phoneList.ContainsKey("Максим Хрусталев"))
{
phoneList["Максим Хрусталев"] = "+79456882321";
}
if (!phoneList.ContainsValue("+79098576432"))
{
Console.WriteLine("Элемент найден!");
}
Console.WriteLine("Вывод контактов тел.книги:");
foreach (KeyValuePair<string, string> author in phoneList)
{
Console.WriteLine("Ключ: {0}, Значение: {1}", author.Key, author.Value);
}
phoneList.Remove("Олеся Марьина");
phoneList.Clear();
}
}
}
В результате выполнения в консоль будет выведено следующее:
Продемонстрировано использование основных свойств и методов класса Dictoniary.
На этом мы завершаем вторую часть статьи, важно понять принципы ООП и в дальнейшем их корректно применять на практике.