👨‍🎓️ Самоучитель по C# для начинающих за 30 минут. Часть 2: ООП и коллекции

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

ООП

Процедурное программирование строится на написании процедур или методов, которые выполняют операции над данными, а объектно-ориентированное программирование на создании объектов, содержащих как данные, так и методы.

ООП основано на использовании трёх основных принципов:

  1. Инкапсуляция – это механизм, позволяющий скрыть код класса (поля, методы, функции) и ограничить доступ к коду и данным из других участков кода.
  2. Наследование – это механизм, позволяющий классу наследовать свойства другого класса, принимая все поля и функции членов базового типа.
  3. Полиморфизм – это свойство, которое позволяет использовать один интерфейс для общего класса действий. Эта концепция часто выражается как "один интерфейс, несколько действий".

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

Классы и объекты

Язык 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# поля и методы могут наследоваться от одного класса к другому. В наследовании используются два понятия:

  1. Базовый класс – класс, от которого наследуется.
  2. Производный класс – класс, который наследуется от базового класса.

На примере ниже класс 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.

На этом мы завершаем вторую часть статьи, важно понять принципы ООП и в дальнейшем их корректно применять на практике.

***

Материалы по теме

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

matyushkin
18 марта 2020

ТОП-10 книг по C#: от новичка до профессионала

Отобрали актуальные книги по C#, .NET, Unity c лучшими оценками. Расположил...
Библиотека программиста
25 августа 2019

Почему C# программисты скоро будут нарасхват

C# программисты становятся более востребованными благодаря развивающейся эк...
Библиотека программиста
12 марта 2018

Видеокурс по C# с нуля: от основ до полноценного приложения

Подробный видеокурс для изучающих C# с нуля. Пройдем путь от основ до напис...