👨🎓️ Самоучитель по 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
.
На этом мы завершаем вторую часть статьи, важно понять принципы ООП и в дальнейшем их корректно применять на практике.