Перейти к основному содержимому

Классы в C#.

1. Введение

1.1. Определение класса

  • Класс — это шаблон или описание объекта, представляющего совокупность данных (полей) и поведения (методов).
    • Объект — это экземпляр класса, конкретная реализация на основе описания, предоставленного классом.
    • Состояние объекта определяется значениями его полей.
    • Поведение объекта определяется его методами, которые могут изменять состояние объекта.
  • Классы являются фундаментальными элементами объектно-ориентированного программирования (ООП), где они используются для моделирования реальных сущностей.
  • Пример простого класса в C#:
    public class Person
    {
    public string Name;
    public int Age;

    public void Greet()
    {
    Console.WriteLine($"Hello, my name is {Name}.");
    }
    }

1.2. Основные отличия классов от структур (struct)

  • Ключевые различия между классами и структурами в C#:

    • Классы — это ссылочные типы, а структурызначимые типы.
      • Ссылочные типы хранятся в управляемой куче, а их ссылки — в стеке.
      • Значимые типы хранятся непосредственно в стеке (или в куче, если они являются частью объекта).
    • При передаче объектов классов передается ссылка на объект, тогда как при передаче структур передается копия данных.
    • Классы могут поддерживать наследование, тогда как структуры не могут (но могут реализовать интерфейсы).
    • По умолчанию, структуры являются иммутабельными (нельзя изменять поля напрямую), в то время как классы могут быть изменяемыми.

    Пример структуры:

    public struct Point
    {
    public int X;
    public int Y;
    }
  • В каких случаях выбирать структуры:

    • Когда требуется представление простых данных, таких как координаты, цвет и т.д.
    • Когда важна производительность при частых копированиях данных.
  • В каких случаях выбирать классы:

    • Когда необходимо моделировать сложные сущности с большим количеством полей и методов.
    • Когда важны такие аспекты ООП, как наследование и полиморфизм.

1.3. Применение классов в объектно-ориентированном программировании (ООП)

  • Классы — ключевой инструмент в ООП, реализующий три основных принципа:
    • Инкапсуляция — возможность скрывать внутренние детали реализации (данные) и предоставлять интерфейсы для работы с объектами.
    • Наследование — создание новых классов на основе уже существующих (базовых) классов с возможностью добавления или изменения функциональности.
    • Полиморфизм — возможность работать с разными объектами через общий интерфейс или базовый класс, используя их уникальное поведение.
  • Пример инкапсуляции и наследования:
    public class Animal
    {
    private string name;

    public Animal(string name)
    {
    this.name = name;
    }

    public void Speak()
    {
    Console.WriteLine($"{name} is making a sound.");
    }
    }

    public class Dog : Animal
    {
    public Dog(string name) : base(name) { }

    public void Bark()
    {
    Console.WriteLine("Dog is barking.");
    }
    }
  • Классы помогают структурировать и организовывать программы таким образом, чтобы каждая часть кода отвечала за конкретную ответственность и функциональность, что делает код более читаемым, тестируемым и поддерживаемым.

1.4. Место классов в языке C#

  • C# — это язык программирования с поддержкой объектно-ориентированного подхода, где классы играют ключевую роль в создании приложений.

  • Классы используются для организации кода, определения типов данных, создания библиотек и фреймворков.

  • Важная роль классов в системе типов C#:

    • Классы наследуются от базового класса System.Object, который предоставляет базовые методы, такие как ToString(), Equals(), GetHashCode() и GetType().
  • Пример использования метода ToString():

    public class Car
    {
    public string Model { get; set; }
    public override string ToString()
    {
    return $"Car model: {Model}";
    }
    }
  • Классы являются строительными блоками большинства программ на C#, и понимание их устройства важно для создания эффективных и современных приложений.

2. Объявление и синтаксис класса

2.1. Общая структура класса

  • Класс — это основной строительный блок в C#, который описывает объект. Он может содержать поля (переменные), свойства, методы, события и другие члены.
  • Общая структура класса:
    • Модификаторы доступа (опционально) — указывают на область видимости класса.
    • Ключевое слово class — указывает, что объявляется класс.
    • Имя класса — задает уникальное имя класса.
    • Тело класса — блок кода, заключенный в фигурные скобки {}, содержащий объявления полей, методов, свойств и других компонентов.
    • Пример общей структуры класса:
    [модификаторы доступа] class ИмяКласса
    {
    // Поля
    // Свойства
    // Конструкторы
    // Методы
    // События
    }

2.2. Ключевое слово class

  • Ключевое слово class используется для объявления класса.
  • После ключевого слова class идет имя класса, которое должно соответствовать соглашениям об именах в C# (PascalCase).
  • Пример:
    public class Car
    {
    public string Model { get; set; }
    public void Drive()
    {
    Console.WriteLine("The car is driving");
    }
    }

2.3. Модификаторы доступа классов и их компонентов

  • Модификаторы доступа определяют видимость и доступность классов, методов, полей и других членов. В C# предусмотрены несколько модификаторов доступа:

Основные модификаторы доступа:

  1. public:
    • Означает, что класс или его члены доступны из любого места.
    • Пример:
      public class Car
      {
      public string Model;
      }
  2. private:
    • Означает, что класс или его члены доступны только внутри этого класса.
    • Пример:
      public class Car
      {
      private string model;
      }
  3. protected:
    • Члены доступны внутри текущего класса и в производных классах.
    • Пример:
      public class Vehicle
      {
      protected int speed;
      }

Дополнительные модификаторы доступа:

  1. internal:
    • Класс или его члены доступны только в пределах текущей сборки (assembly).
    • Пример:
      internal class Engine
      {
      internal int HorsePower;
      }
  2. protected internal:
    • Доступ к членам класса возможен как из текущей сборки, так и из производных классов, даже если они находятся в другой сборке.
    • Пример:
      protected internal class Mechanism
      {
      protected internal void Start() {}
      }
  3. private protected:
    • Доступ к членам возможен только внутри текущего класса и его производных классов, но только если они находятся в одной сборке.
    • Пример:
      private protected class Sensor
      {
      private protected int Sensitivity;
      }

Пример модификаторов доступа:

    public class BankAccount
{
private decimal balance; // Доступен только внутри класса.

public void Deposit(decimal amount)
{
balance += amount; // Метод доступен всем.
}

protected void UpdateAccountStatus() // Доступен наследникам.
{
Console.WriteLine("Updating account status...");
}
}

Пример использования разных модификаторов доступа:

public class Parent
{
public int PublicField; // Доступен везде
private int PrivateField; // Доступен только в этом классе
protected int ProtectedField; // Доступен только в этом классе и в его наследниках
internal int InternalField; // Доступен только в пределах сборки
protected internal int ProtIntField; // Доступен в пределах сборки или в производных классах
private protected int PrivProtField; // Доступен только в пределах сборки и в производных классах
}

2.4. Имя класса и соглашения об именах

  • Имя класса должно быть уникальным в рамках его области видимости.
  • В C# применяются следующие соглашения по именованию:
    • Использование PascalCase для названия классов и методов. Каждое слово в названии класса начинается с заглавной буквы.
    • Имя класса должно быть существительным, четко описывающим, какую сущность представляет класс.
    • Пример правильного имени класса: Employee, Car, BankAccount.
    • Пример некорректного имени класса: myClass, emp, 1stClass.
  • Пример корректного объявления:
    public class Person
    {
    public string Name;
    }

2.5. Примеры объявления класса

  1. Простой класс с полем и методом:

    public class Car
    {
    public string Model;

    public void StartEngine()
    {
    Console.WriteLine($"{Model} engine started.");
    }
    }
  2. Класс с модификатором private:

    public class BankAccount
    {
    private decimal balance;

    public void Deposit(decimal amount)
    {
    if (amount > 0)
    {
    balance += amount;
    }
    }

    public decimal GetBalance()
    {
    return balance;
    }
    }
  3. Класс с использованием наследования и модификатора protected:

    public class Animal
    {
    protected string name;

    public Animal(string name)
    {
    this.name = name;
    }

    public void Speak()
    {
    Console.WriteLine($"{name} makes a sound.");
    }
    }

    public class Dog : Animal
    {
    public Dog(string name) : base(name) {}

    public void Bark()
    {
    Console.WriteLine($"{name} barks.");
    }
    }
  4. Класс с различными модификаторами доступа:

    public class Employee
    {
    public string Name { get; private set; }
    internal int ID { get; private set; }
    protected int Age { get; set; }

    public Employee(string name, int id)
    {
    Name = name;
    ID = id;
    }

    public void DisplayInfo()
    {
    Console.WriteLine($"Employee: {Name}, ID: {ID}");
    }
    }

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

3. Члены класса

3.1. Поля класса (Fields)

Поля (Fields) класса

  • Поле класса — это переменная, объявленная внутри класса, которая хранит данные (состояние) для объектов этого класса. Поля могут быть как нестатическими (принадлежащими объекту), так и статическими (принадлежащими классу в целом).
  • Поля определяют состояние объекта и могут содержать различные типы данных, такие как числа, строки, объекты других классов и т.д.
  • Поля обычно имеют модификаторы доступа для контроля того, какие компоненты программы могут обращаться к ним.

Объявление полей

  • Поле объявляется как переменная внутри класса, с указанием типа, имени и, опционально, значения по умолчанию.
  • Синтаксис объявления поля:
    [модификатор доступа] [тип] [имя_поля];
  • Поля могут иметь модификаторы доступа public, private, protected, internal и другие, аналогично методам и свойствам.
    • Пример:
      public class Person
      {
      public string Name; // Публичное поле
      private int age; // Приватное поле
      }
  • Поля могут быть проинициализированы значением сразу при объявлении или в конструкторе класса.
    • Пример:
      public class Car
      {
      public string Model = "Unknown"; // Инициализация при объявлении
      private int speed;
      }

Статические и нестатические поля

Нестатические поля
  • Нестатические поля (или экземплярные поля) хранят данные, уникальные для каждого экземпляра (объекта) класса.
  • Каждый объект класса имеет свои собственные копии таких полей, и изменения в поле одного объекта не влияют на другие объекты.
  • Пример:
    public class Car
    {
    public string Model; // Нестатическое поле, уникальное для каждого объекта
    private int speed;

    public Car(string model, int speed)
    {
    this.Model = model;
    this.speed = speed;
    }
    }
Статические поля
  • Статические поля принадлежат самому классу, а не конкретному объекту. Это означает, что у всех объектов этого класса будет общее значение для статического поля.
  • Статические поля инициализируются один раз и существуют на протяжении всего времени работы программы. Они используются, когда нужно хранить информацию, общую для всех экземпляров класса.
  • Для доступа к статическим полям не требуется создавать объект класса, они доступны через сам класс.
  • Пример:
    public class Company
    {
    public static string CompanyName = "Tech Corp"; // Статическое поле
    public string EmployeeName; // Нестатическое поле

    public Company(string employeeName)
    {
    EmployeeName = employeeName;
    }
    }
    • В данном примере поле CompanyName одно для всех объектов класса Company, тогда как EmployeeName уникально для каждого экземпляра.
Отличия статических и нестатических полей:
  • Статические поля:
    • Общие для всех экземпляров класса.
    • Обращение через класс (например, Company.CompanyName).
  • Нестатические поля:
    • Уникальные для каждого экземпляра класса.
    • Обращение через объект (например, employee.EmployeeName).

Примеры использования:

  1. Пример с нестатическими полями:

    public class Person
    {
    public string Name; // Нестатическое поле, уникальное для каждого объекта
    public int Age;

    public Person(string name, int age)
    {
    Name = name;
    Age = age;
    }
    }

    Person person1 = new Person("Alice", 25);
    Person person2 = new Person("Bob", 30);
  2. Пример со статическими полями:

    public class Counter
    {
    public static int Count = 0; // Статическое поле, общее для всех объектов

    public Counter()
    {
    Count++; // Увеличивается при создании каждого объекта
    }
    }

    Counter c1 = new Counter(); // Count = 1
    Counter c2 = new Counter(); // Count = 2

Эти аспекты, такие как различия между статическими и нестатическими полями, а также их применение, важны для понимания работы с объектами в C# и организации данных внутри классов.

3.2. Методы класса (Methods)

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

Статические и нестатические методы

Нестатические методы
  • Нестатические методы принадлежат объектам класса и работают с их конкретными данными (состоянием).
  • Чтобы вызвать нестатический метод, нужно создать экземпляр класса.
  • Пример:
    public class Car
    {
    public string Model;

    public void StartEngine()
    {
    Console.WriteLine($"{Model} engine started.");
    }
    }

    Car myCar = new Car();
    myCar.Model = "Toyota";
    myCar.StartEngine(); // Вызов нестатического метода через объект
Статические методы
  • Статические методы принадлежат самому классу, а не его экземплярам, и работают только со статическими полями и переменными, либо с параметрами, переданными в метод.
  • Для вызова статического метода не требуется создание объекта класса.
  • Пример:
    public class Calculator
    {
    public static int Add(int a, int b)
    {
    return a + b;
    }
    }

    int result = Calculator.Add(5, 3); // Вызов статического метода напрямую через имя класса

Аргументы методов (передача по значению и по ссылке)

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

Передача по значению
  • По умолчанию все параметры передаются по значению.
  • Это означает, что метод получает копию значения, и изменения внутри метода не влияют на оригинальную переменную.
  • Пример:
    public void Increment(int x)
    {
    x++;
    }

    int number = 5;
    Increment(number); // Значение number не изменится
Передача по ссылке (ref, out, in)
  • Передача по ссылке позволяет методу работать с оригинальной переменной, а не с ее копией.
  1. ref:

    • Указывает, что параметр передается по ссылке, и любые изменения внутри метода будут влиять на оригинальную переменную.
    • Переменная должна быть инициализирована до вызова метода.
    • Пример:
      public void Increment(ref int x)
      {
      x++;
      }

      int number = 5;
      Increment(ref number); // Значение number станет 6
  2. out:

    • Похож на ref, но переменная не должна быть инициализирована до передачи в метод.
    • Метод должен присвоить значение переменной до выхода.
    • Пример:
      public void Initialize(out int x)
      {
      x = 10; // Обязательно нужно присвоить значение
      }

      int number;
      Initialize(out number); // Значение number станет 10
  3. in:

    • Указывает, что параметр передается по ссылке, но только для чтения.
    • Метод не может изменять значение переменной.
    • Пример:
      public void DisplayValue(in int x)
      {
      Console.WriteLine(x); // Можно только читать
      }

      int number = 5;
      DisplayValue(in number); // Передача по ссылке для чтения

Возвращаемые типы методов

  • Методы могут возвращать значения разных типов: примитивных, классов, структур, массивов и других объектов.

  • Если метод не возвращает значение, используется тип void.

  • Пример возвращаемого значения:

    public int Add(int a, int b)
    {
    return a + b;
    }
  • Пример метода без возвращаемого значения:

    public void PrintMessage()
    {
    Console.WriteLine("Hello!");
    }

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

  • Перегрузка методов позволяет создавать несколько методов с одинаковым именем, но с разными параметрами.
  • Перегруженные методы различаются по:
    • Количеству параметров.
    • Типам параметров.
    • Порядку параметров.
  • Пример перегрузки методов:
    public class Calculator
    {
    public int Add(int a, int b)
    {
    return a + b;
    }

    public double Add(double a, double b)
    {
    return a + b;
    }

    public int Add(int a, int b, int c)
    {
    return a + b + c;
    }
    }

Ключевые слова static, ref, out, in

  1. static:

    • Используется для обозначения статических методов и полей, принадлежащих классу, а не конкретным экземплярам.
    • Статический метод может обращаться только к статическим данным и другим статическим методам.
    • Пример:
      public static void PrintMessage()
      {
      Console.WriteLine("This is a static method.");
      }
  2. ref:

    • Используется для передачи аргументов по ссылке, чтобы изменения внутри метода влияли на оригинальные данные.
    • Переменная должна быть инициализирована до передачи.
    • Пример:
      public void Modify(ref int number)
      {
      number = 100;
      }
  3. out:

    • Используется для передачи параметра по ссылке, когда метод обязан присвоить значение переменной.
    • Переменная может быть не инициализирована перед вызовом метода.
    • Пример:
      public void SetValue(out int number)
      {
      number = 42;
      }
  4. in:

    • Используется для передачи аргументов по ссылке для только чтения.
    • Метод не может изменять значение переменной.
    • Пример:
      public void Print(in int number)
      {
      Console.WriteLine(number);
      }

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

3.3. Свойства (Properties)

Свойства в C# позволяют предоставлять доступ к полям класса с помощью методов доступа — геттеров (get) и сеттеров (set). Они обеспечивают контроль над доступом и изменением значений полей объекта, сохраняя при этом инкапсуляцию.

Автосвойства

  • Автосвойства — это удобный способ быстрого определения свойств без необходимости явно создавать приватные поля.

  • Когда создается автосвойство, компилятор автоматически генерирует закрытое поле для хранения значения.

  • Автосвойства позволяют использовать стандартные геттеры и сеттеры для доступа к значению.

  • Синтаксис:

    public class Person
    {
    public string Name { get; set; }
    public int Age { get; set; }
    }

    В данном примере компилятор создаст приватные поля для хранения значений Name и Age.

  • Автосвойства могут иметь значение по умолчанию:

    public class Person
    {
    public string Name { get; set; } = "Unknown";
    public int Age { get; set; } = 0;
    }

Синтаксис геттеров и сеттеров

  • Свойства в C# определяются через методы get и set. Get используется для получения значения, а set — для его изменения.

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

  • Синтаксис:

    public class Person
    {
    private string name;

    public string Name
    {
    get { return name; }
    set { name = value; }
    }
    }
  • В геттерах и сеттерах можно реализовать дополнительную логику:

    public class Employee
    {
    private int salary;

    public int Salary
    {
    get { return salary; }
    set
    {
    if (value > 0)
    {
    salary = value;
    }
    else
    {
    Console.WriteLine("Salary must be positive.");
    }
    }
    }
    }

Модификаторы доступа для геттеров и сеттеров

  • В C# можно устанавливать разные модификаторы доступа для геттера и сеттера, что позволяет гибко управлять доступом к свойствам.
  • Например, можно сделать геттер публичным, а сеттер — приватным, что делает свойство доступным для чтения, но не для изменения извне.
  • Пример:
    public class BankAccount
    {
    private decimal balance;

    public decimal Balance
    {
    get { return balance; }
    private set { balance = value; } // Сеттер доступен только внутри класса
    }

    public BankAccount(decimal initialBalance)
    {
    Balance = initialBalance;
    }
    }
    В этом примере свойство Balance можно прочитать из любого места, но изменить его можно только внутри класса.

Свойства только для чтения (read-only properties)

  • Свойства могут быть только для чтения, если у них определен только геттер, но отсутствует сеттер.

  • Такие свойства позволяют получить значение, но не позволяют его изменить после инициализации.

  • Свойства только для чтения часто используются для константных или вычисляемых значений.

  • Пример:

    public class Circle
    {
    public double Radius { get; }

    public Circle(double radius)
    {
    Radius = radius; // Значение может быть присвоено только в конструкторе
    }

    public double Area
    {
    get { return Math.PI * Radius * Radius; } // Только для чтения, вычисляемое значение
    }
    }
  • Пример read-only свойства с использованием автосвойств:

    public class Person
    {
    public string Name { get; } // Только для чтения

    public Person(string name)
    {
    Name = name;
    }
    }

Итог

Свойства в C# предоставляют удобный механизм для контроля доступа к данным объектов, позволяя скрывать внутренние детали реализации и предоставлять безопасный доступ через геттеры и сеттеры. Благодаря автосвойствам и модификаторам доступа разработчики могут гибко управлять состоянием объектов и защитой данных, используя свойства только для чтения или ограничивая изменение свойств внутри класса.

3.4. Индексаторы (Indexers)

Индексаторы в C# позволяют объектам класса или структуры вести себя как массивы, предоставляя доступ к элементам по индексу. Это дает возможность обращаться к элементам объекта так же, как к элементам массива или коллекции, используя синтаксис квадратных скобок.

Особенности индексаторов

  • Индексаторы позволяют объекту класса предоставлять доступ к своим данным через индекс, подобно массивам.
  • Индексатор не имеет имени и используется через ключевое слово this.
  • Индексаторы могут принимать один или несколько параметров (например, для реализации многомерного доступа).
  • Как и свойства, индексаторы могут иметь геттер и сеттер для управления доступом к данным.
  • Индексаторы могут быть перегружены для разных типов индексов (например, строковые и числовые индексы).

Синтаксис индексаторов

  • Индексатор объявляется в классе с использованием ключевого слова this, за которым следует список параметров в квадратных скобках.
  • Синтаксис индексатора напоминает синтаксис свойства, но с указанием параметра в квадратных скобках.
Общий синтаксис индексатора:
public тип this[тип_параметра индекс]
{
get
{
// Возврат значения по индексу
}
set
{
// Присваивание значения по индексу
}
}

Примеры использования индексаторов

Пример 1: Индексатор для массива чисел

Создадим класс, который использует индексатор для доступа к внутреннему массиву:

public class NumberArray
{
private int[] numbers = new int[5]; // Внутренний массив

// Индексатор для доступа к элементам массива
public int this[int index]
{
get
{
if (index >= 0 && index < numbers.Length)
{
return numbers[index];
}
throw new IndexOutOfRangeException("Индекс вне диапазона!");
}
set
{
if (index >= 0 && index < numbers.Length)
{
numbers[index] = value;
}
else
{
throw new IndexOutOfRangeException("Индекс вне диапазона!");
}
}
}
}

Использование индексатора:

NumberArray arr = new NumberArray();
arr[0] = 10; // Устанавливаем значение через индексатор
Console.WriteLine(arr[0]); // Читаем значение через индексатор
Пример 2: Индексатор со строковым параметром

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

public class Person
{
private Dictionary<string, string> details = new Dictionary<string, string>();

// Индексатор с строковым индексом
public string this[string key]
{
get
{
if (details.ContainsKey(key))
{
return details[key];
}
return "Ключ не найден!";
}
set
{
details[key] = value;
}
}
}

Использование строкового индексатора:

Person person = new Person();
person["Name"] = "Alice"; // Устанавливаем значение по строковому индексу
Console.WriteLine(person["Name"]); // Читаем значение по строковому индексу

Основные моменты:

  1. Модификаторы доступа:

    • Индексаторы могут иметь различные модификаторы доступа для геттера и сеттера, как и свойства.
    • Например, можно сделать индексатор доступным для чтения извне, но изменяемым только внутри класса.
  2. Многомерные индексаторы:

    • В C# можно создавать индексаторы с несколькими параметрами для работы с многомерными структурами данных.

    • Пример:

      public class Matrix
      {
      private int[,] data = new int[3, 3];

      // Многомерный индексатор
      public int this[int row, int col]
      {
      get { return data[row, col]; }
      set { data[row, col] = value; }
      }
      }

      Использование многомерного индексатора:

      Matrix matrix = new Matrix();
      matrix[0, 0] = 1; // Устанавливаем значение по двум индексам
      Console.WriteLine(matrix[0, 0]); // Получаем значение по двум индексам

Заключение

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

3.5. События (Events)

События в C# — это механизм, который позволяет объектам взаимодействовать между собой, предоставляя способ уведомления о произошедших изменениях или действиях. События широко используются в программировании для реализации шаблона "издатель-подписчик", где один объект (издатель) может уведомлять другие объекты (подписчики) о происходящих изменениях.

Использование событий в классе

  • События определяются в классе и могут быть вызваны при наступлении определенных условий или действий.
  • Класс, содержащий событие, называется издателем. Объекты, подписанные на событие, называются подписчиками.
  • Для работы с событиями в C# используются делегаты, которые определяют сигнатуру методов, которые могут быть вызваны при срабатывании события.
Общий синтаксис объявления события:
public delegate void EventHandler();  // Объявление делегата

public class Publisher
{
public event EventHandler OnChange; // Объявление события

public void ChangeState()
{
// Логика изменения состояния

if (OnChange != null)
{
OnChange(); // Вызов события
}
}
}
Пример использования события:
public delegate void Notify();  // Делегат для события

public class Process
{
public event Notify ProcessCompleted; // Событие

public void StartProcess()
{
Console.WriteLine("Процесс начинается...");
// Логика процесса
OnProcessCompleted();
}

protected virtual void OnProcessCompleted()
{
if (ProcessCompleted != null)
{
ProcessCompleted(); // Вызов события
}
}
}

public class Subscriber
{
public void OnProcessCompleted()
{
Console.WriteLine("Процесс завершен!");
}
}

class Program
{
static void Main(string[] args)
{
Process process = new Process();
Subscriber subscriber = new Subscriber();

// Подписка на событие
process.ProcessCompleted += subscriber.OnProcessCompleted;

process.StartProcess(); // Запуск процесса и вызов события
}
}

В этом примере класс Process является издателем события ProcessCompleted. Объект класса Subscriber подписан на это событие и реагирует на его вызов.

Делегаты и их роль в событиях

  • Делегаты — это типы, которые определяют сигнатуру методов, которые могут быть вызваны при срабатывании события. Делегат описывает форму метода: возвращаемый тип и список параметров.
  • События в C# всегда связаны с делегатами, так как они управляют тем, какие методы могут быть вызваны при наступлении события.
Пример делегата:
public delegate void EventHandler(string message);  // Делегат с параметром

public class Notification
{
public event EventHandler OnNotify; // Событие на основе делегата

public void SendNotification(string message)
{
if (OnNotify != null)
{
OnNotify(message); // Вызов события с параметром
}
}
}
Подписка на событие:
public class Listener
{
public void DisplayMessage(string message)
{
Console.WriteLine(message);
}
}

class Program
{
static void Main()
{
Notification notifier = new Notification();
Listener listener = new Listener();

// Подписка на событие
notifier.OnNotify += listener.DisplayMessage;

notifier.SendNotification("Новое уведомление!"); // Вызов события
}
}

В этом примере делегат EventHandler принимает параметр типа string. Метод DisplayMessage подписывается на событие и будет вызван, когда сработает событие OnNotify, передавая строковое сообщение.

Основные моменты:

  1. Объявление делегатов:

    • Делегат определяет сигнатуру методов, которые могут быть вызваны событием.
    • Делегаты можно определять как явно, так и использовать встроенные делегаты, такие как Action или Func.
  2. Объявление событий:

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

    • Подписка на событие осуществляется с помощью оператора +=, а отписка — с помощью -=.
    • Пример подписки:
      process.OnChange += listener.OnProcessCompleted;
    • Пример отписки:
      process.OnChange -= listener.OnProcessCompleted;
  4. Стандартные делегаты:

    • Для упрощения работы с делегатами в C# есть встроенные типы делегатов:
      • Action — для методов, которые не возвращают значение.
      • Func — для методов, которые возвращают значение.
      • EventHandler — стандартный делегат для событий, который принимает два параметра: object sender и EventArgs.

Пример использования стандартного делегата EventHandler:

public class Button
{
public event EventHandler Click;

public void OnClick()
{
if (Click != null)
{
Click(this, EventArgs.Empty); // Вызов события с параметрами sender и EventArgs
}
}
}

Заключение

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

3.6. Константы и поля только для чтения (Constants and Read-only Fields)

В C# для создания неизменяемых значений используются константы (const) и поля только для чтения (readonly). Хотя они могут показаться схожими, есть важные различия в их использовании и поведении.

Разница между const и readonly

const (Константы)
  • Константы — это значения, которые должны быть известны и определены на этапе компиляции.
  • Они являются неизменяемыми (immutable) и всегда остаются одинаковыми в течение времени выполнения программы.
  • Константы доступны на уровне класса и должны быть инициализированы при объявлении.
  • Константы могут быть только значимыми типами или строками (например, числа, строки, логические значения).
  • Модификатор static добавлять не нужно, так как все константы по умолчанию статичны.
readonly (Поля только для чтения)
  • Поля только для чтения — это поля, которые могут быть инициализированы либо при объявлении, либо в конструкторе класса, что позволяет инициализировать их разными значениями для разных экземпляров класса.
  • После инициализации поля readonly не могут быть изменены в течение времени выполнения программы.
  • readonly могут быть как нестатическими, так и статическими.

Отличия между const и readonly:

Свойствоconstreadonly
ИнициализацияДолжна быть выполнена при объявленииМожно инициализировать при объявлении или в конструкторе
ИзменяемостьНельзя изменить после компиляцииНельзя изменить после инициализации (при запуске программы)
Уровень доступностиВсегда статичные (доступны на уровне класса)Может быть как статическим, так и нестатическим
Типы данныхТолько примитивы и строкиЛюбые типы данных (включая объекты и структуры)
Известно на этапеНа этапе компиляцииНа этапе выполнения (после создания объекта)

Особенности использования и примеры

Пример использования const:
public class MathConstants
{
public const double Pi = 3.14159; // Значение известно на этапе компиляции
public const int MaxValue = 100;

public void DisplayConstants()
{
Console.WriteLine($"Pi: {Pi}, MaxValue: {MaxValue}");
}
}
  • Объяснение:
    • В этом примере Pi и MaxValue — это константы, их значения не могут быть изменены, и они всегда будут доступны на уровне класса. При вызове метода DisplayConstants() всегда будут выведены одни и те же значения.
Пример использования readonly:
public class Circle
{
public readonly double Radius; // Поле только для чтения
public static readonly double Pi = 3.14159; // Статическое поле только для чтения

public Circle(double radius)
{
Radius = radius; // Инициализация поля в конструкторе
}

public double CalculateArea()
{
return Pi * Radius * Radius; // Использование как нестатического, так и статического поля
}
}
  • Объяснение:
    • Radius — это поле только для чтения, которое инициализируется в конструкторе и не может быть изменено после этого.
    • Pi — это статическое поле только для чтения, оно инициализируется при объявлении и также не может быть изменено, но доступно на уровне класса.
Использование readonly для инициализации через конструктор:
public class Person
{
public readonly string Name;
public readonly int Age;

public Person(string name, int age)
{
Name = name; // Инициализация поля только для чтения в конструкторе
Age = age;
}

public void DisplayInfo()
{
Console.WriteLine($"Name: {Name}, Age: {Age}");
}
}
  • В этом примере поля Name и Age инициализируются через конструктор и не могут быть изменены после создания объекта.

Особенности использования

  1. Когда использовать const:

    • Если значение не будет изменяться и известно заранее на этапе компиляции.
    • Например, математические константы, фиксированные настройки или значения, которые одинаковы для всех экземпляров класса.
  2. Когда использовать readonly:

    • Если значение должно быть установлено один раз, но может быть различным для разных экземпляров класса, или если значение можно вычислить только в момент выполнения программы.
    • Это полезно для объектов, которые получают свои значения в зависимости от данных, переданных через конструктор.

Заключение

const и readonly оба позволяют создавать неизменяемые значения, но их применение зависит от контекста. const используется для значений, которые известны на этапе компиляции и никогда не изменяются. readonly подходит для значений, которые должны быть установлены один раз на этапе выполнения программы, что позволяет использовать более сложные типы данных и конструкции.

3.7. Конструкторы (Constructors)

Конструкторы в C# — это специальные методы, которые вызываются при создании объекта класса. Их основная задача — инициализация полей и выполнение начальных операций для объекта.

Типы конструкторов

1. Конструктор по умолчанию (Default Constructor)
  • Если разработчик не определяет ни одного конструктора, C# автоматически создаёт конструктор по умолчанию, который не принимает параметров и не выполняет никакой дополнительной логики, кроме создания объекта.

  • Пример:

    public class Person
    {
    public string Name;
    public int Age;
    }

    Person person = new Person(); // Конструктор по умолчанию
  • Если в классе объявлен хотя бы один конструктор, компилятор не создаст конструктор по умолчанию автоматически.

2. Конструктор с параметрами (Parameterized Constructor)
  • Этот тип конструктора позволяет передавать параметры для инициализации полей объекта при его создании.
  • Пример:
    public class Person
    {
    public string Name;
    public int Age;

    // Конструктор с параметрами
    public Person(string name, int age)
    {
    Name = name;
    Age = age;
    }
    }

    Person person = new Person("Alice", 25); // Инициализация объекта с параметрами
3. Статический конструктор (Static Constructor)
  • Статические конструкторы инициализируют статические поля класса. Они вызываются автоматически единожды перед созданием первого экземпляра класса или первым обращением к его статическим членам.
  • Статический конструктор не принимает параметров и не может быть вызван напрямую.
  • Пример:
    public class Settings
    {
    public static string AppName;

    // Статический конструктор
    static Settings()
    {
    AppName = "My Application";
    }
    }

    Console.WriteLine(Settings.AppName); // Статический конструктор вызывается перед доступом к AppName
4. Конструктор копирования (Copy Constructor)
  • Конструктор копирования используется для создания нового объекта на основе существующего объекта того же класса. Он обычно принимает в качестве параметра другой объект и копирует его поля.
  • Пример:
    public class Person
    {
    public string Name;
    public int Age;

    public Person(string name, int age)
    {
    Name = name;
    Age = age;
    }

    // Конструктор копирования
    public Person(Person other)
    {
    Name = other.Name;
    Age = other.Age;
    }
    }

    Person original = new Person("Alice", 25);
    Person copy = new Person(original); // Создание копии объекта

Инициализация полей через конструкторы

Конструкторы в C# часто используются для инициализации полей объекта, гарантируя, что объект создаётся в валидном состоянии. Поля могут быть инициализированы как непосредственно в конструкторе, так и переданы в качестве параметров.

Пример инициализации полей:

public class Car
{
public string Model;
public int Year;

// Инициализация через конструктор
public Car(string model, int year)
{
Model = model;
Year = year;
}
}

Конструкторы с параметрами по умолчанию

  • В C# можно задавать параметры с значениями по умолчанию для конструктора. Если при вызове конструктора не указаны какие-то параметры, будут использоваться значения по умолчанию.
  • Пример:
    public class Car
    {
    public string Model;
    public int Year;

    // Конструктор с параметрами по умолчанию
    public Car(string model = "Unknown", int year = 2020)
    {
    Model = model;
    Year = year;
    }
    }

    Car car1 = new Car(); // Используются значения по умолчанию: Model = "Unknown", Year = 2020
    Car car2 = new Car("Toyota"); // Year = 2020
    Car car3 = new Car("BMW", 2022); // Оба значения переданы явно

Ключевое слово this для обращения к текущему экземпляру

  • this — это специальное ключевое слово, которое используется для ссылки на текущий экземпляр класса внутри методов и конструкторов.

  • Оно часто используется для различения полей класса и параметров конструктора или метода, если их имена совпадают.

  • Пример:

    public class Person
    {
    public string Name;
    public int Age;

    public Person(string name, int age)
    {
    this.Name = name; // this указывает на текущее поле объекта
    this.Age = age;
    }

    public void DisplayInfo()
    {
    Console.WriteLine($"Name: {this.Name}, Age: {this.Age}"); // Использование this для доступа к полям
    }
    }
  • this также может использоваться для вызова одного конструктора из другого в том же классе, что помогает избежать дублирования кода.

  • Пример:

    public class Person
    {
    public string Name;
    public int Age;

    // Основной конструктор
    public Person(string name, int age)
    {
    this.Name = name;
    this.Age = age;
    }

    // Второй конструктор, вызывающий первый с помощью this
    public Person(string name) : this(name, 0) // Вызов основного конструктора
    {
    }
    }

    Person person = new Person("Alice"); // Используется конструктор с вызовом this

Заключение

Конструкторы в C# играют ключевую роль в инициализации объектов и предоставляют гибкие возможности для работы с объектами через различные типы конструкторов: по умолчанию, с параметрами, статические и копирования. Ключевое слово this помогает в создании более ясного и эффективного кода, обеспечивая доступ к текущему экземпляру класса и переиспользование конструкторов.

3.8. Деструкторы (Destructors)

Принципы работы и роль деструктора

Деструкторы в C# — это специальные методы, которые вызываются при удалении объекта для выполнения завершающих операций, таких как освобождение ресурсов. В отличие от конструкторов, деструкторы вызываются автоматически сборщиком мусора (Garbage Collector) при удалении объекта из памяти.

Основные характеристики деструкторов:

  • Деструктор используется для выполнения финализации — освобождения любых неуправляемых ресурсов, которые не были освобождены другим способом.
  • В C# деструкторы имеют синтаксис, аналогичный конструкторам, но их имя всегда начинается с символа ~ и совпадает с именем класса.
  • Деструктор не может принимать параметры и не возвращает значение.
  • Разработчику не нужно вызывать деструктор явно — он вызывается сборщиком мусора автоматически, когда объект больше не используется.
  • Важно помнить, что использование деструкторов увеличивает время сборки мусора, так как требует дополнительных операций.

Пример использования и ограничения

Пример деструктора:
public class ResourceHolder
{
// Поле для неуправляемого ресурса
private IntPtr unmanagedResource;

// Конструктор
public ResourceHolder()
{
// Инициализация ресурса
unmanagedResource = new IntPtr();
Console.WriteLine("Ресурс выделен.");
}

// Деструктор
~ResourceHolder()
{
// Освобождение ресурса
Console.WriteLine("Деструктор вызван, освобождаем ресурс.");
ReleaseResource(unmanagedResource);
}

// Метод для освобождения ресурса
private void ReleaseResource(IntPtr resource)
{
// Логика освобождения ресурса
Console.WriteLine("Ресурс освобожден.");
}
}

Объяснение:

  • В этом примере класс ResourceHolder выделяет неуправляемый ресурс (например, ресурс операционной системы) в конструкторе и освобождает его в деструкторе.
  • Деструктор выполняет вызов метода ReleaseResource, который освобождает этот ресурс.
Ограничения деструкторов:
  1. Неуправляемые ресурсы:

    • Деструкторы применяются только в случаях, когда необходимо освободить неуправляемые ресурсы (например, дескрипторы файлов, соединения с базой данных или ресурсы операционной системы).
    • Для управляемых ресурсов в C# деструкторы не требуются, так как сборщик мусора автоматически освобождает память для объектов.
  2. Отсутствие контроля над вызовом:

    • Разработчик не может контролировать, когда деструктор будет вызван, так как это зависит от работы сборщика мусора.
    • Сборщик мусора не гарантирует немедленного вызова деструкторов — они могут быть вызваны с задержкой или не вызваны вообще, если программа завершит работу раньше, чем сборщик мусора успеет очистить память.
  3. Плохое влияние на производительность:

    • Использование деструкторов может замедлить работу приложения, так как сборщик мусора выполняет дополнительные операции для объектов с деструкторами (например, перемещает такие объекты в отдельную очередь для финализации).
    • Для освобождения ресурсов лучше использовать интерфейс IDisposable и метод Dispose(), который предоставляет больше контроля над освобождением ресурсов.
Пример использования интерфейса IDisposable:
public class ResourceHolder : IDisposable
{
private IntPtr unmanagedResource;
private bool disposed = false;

public ResourceHolder()
{
unmanagedResource = new IntPtr();
Console.WriteLine("Ресурс выделен.");
}

public void Dispose()
{
// Освобождение ресурсов
Dispose(true);
GC.SuppressFinalize(this); // Отключаем вызов деструктора
}

protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// Освобождение управляемых ресурсов (если есть)
}

// Освобождение неуправляемых ресурсов
ReleaseResource(unmanagedResource);
disposed = true;
}
}

~ResourceHolder()
{
Dispose(false); // Вызывается деструктором
}

private void ReleaseResource(IntPtr resource)
{
Console.WriteLine("Ресурс освобожден.");
}
}

Объяснение:

  • В этом примере вместо использования деструктора напрямую реализуется интерфейс IDisposable, что позволяет разработчику самостоятельно управлять освобождением ресурсов через метод Dispose().
  • Метод Dispose() используется для явного освобождения ресурсов, а деструктор вызовет освобождение ресурсов, если разработчик не вызвал Dispose().

Заключение

  • Деструкторы в C# используются для автоматического освобождения неуправляемых ресурсов, однако их использование имеет ограничения и может негативно влиять на производительность.
  • Для более эффективного управления ресурсами рекомендуется использовать паттерн Dispose() и интерфейс IDisposable, который предоставляет больше контроля над освобождением ресурсов и помогает избежать задержек, связанных с деструкторами.

3.9. Методы расширения (Extension Methods)

Описание методов расширения

Методы расширения (Extension Methods) — это особенность C# (и других языков .NET), которая позволяет добавлять новые методы в существующие типы (классы, структуры или интерфейсы) без необходимости изменять их исходный код. Методы расширения полезны, когда вы хотите расширить функциональность типов, к которым у вас нет доступа для изменения, например, типов в стандартных библиотеках .NET или сторонних библиотеках.

Основные особенности методов расширения:

  • Методы расширения создаются в статических классах.
  • Сам метод должен быть статическим.
  • Первый параметр метода — это тот тип, который будет расширен, с добавлением ключевого слова this перед типом. Этот параметр указывает, к какому типу будет применяться метод расширения.
  • Вызов метода расширения выглядит так, будто это обычный метод экземпляра расширяемого типа.

Пример реализации

Пример 1: Расширение стандартного класса string

Предположим, мы хотим добавить метод к классу string, который инвертирует порядок символов в строке. Для этого можно создать метод расширения.

Шаг 1: Создаём статический класс для хранения метода расширения.

public static class StringExtensions
{
// Метод расширения для класса string
public static string ReverseString(this string input)
{
char[] chars = input.ToCharArray();
Array.Reverse(chars);
return new string(chars);
}
}
  • Здесь:
    • this string input указывает, что метод расширяет тип string.
    • Метод может быть вызван любым объектом типа string как обычный метод.

Шаг 2: Использование метода расширения.

class Program
{
static void Main(string[] args)
{
string original = "Hello World";
string reversed = original.ReverseString(); // Вызов метода расширения

Console.WriteLine(reversed); // Вывод: "dlroW olleH"
}
}
  • Хотя метод ReverseString не является частью стандартного класса string, мы можем вызывать его так, будто он является встроенным методом строки.
Пример 2: Расширение класса List<T>

Допустим, мы хотим добавить метод для вычисления суммы всех элементов в списке целых чисел (List<int>).

Шаг 1: Создание метода расширения.

public static class ListExtensions
{
// Метод расширения для списка целых чисел
public static int SumElements(this List<int> list)
{
int sum = 0;
foreach (int num in list)
{
sum += num;
}
return sum;
}
}

Шаг 2: Использование метода расширения.

class Program
{
static void Main(string[] args)
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
int sum = numbers.SumElements(); // Вызов метода расширения

Console.WriteLine(sum); // Вывод: 15
}
}

Основные моменты:

  1. Место в коде:

    • Методы расширения должны быть объявлены в статическом классе.
    • Они могут быть объявлены в любом пространстве имен и затем подключены с помощью директивы using.
  2. Ключевое слово this:

    • Первым параметром метода всегда должно быть this, за которым следует тип, который вы хотите расширить. Это позволяет вызвать метод на экземпляре данного типа.
  3. Ограничения:

    • Методы расширения не могут заменять существующие методы типов. Если тип уже имеет метод с такой же сигнатурой, метод расширения не будет вызван.
    • Методы расширения не могут напрямую изменять состояние объекта (если это не свойство изменяемого типа). Они работают только с копиями параметров и могут возвращать результаты.

Преимущества методов расширения:

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

Заключение

Методы расширения — это мощный инструмент для улучшения функциональности существующих классов в C#. Они позволяют добавлять новые методы к любым типам без изменения их кода и расширяют возможности повторного использования и модификации программных компонентов.

4. Ключевое слово static

Ключевое слово static в C# используется для определения членов класса, которые принадлежат самому классу, а не его экземплярам. Это касается как методов и полей, так и классов. Статические члены могут быть полезны для хранения общих данных и предоставления функциональности, которая не зависит от конкретного объекта.

4.1. Статические классы и их назначение

  • Статический класс — это класс, в котором все члены являются статическими.
  • Статические классы используются для предоставления общих функций и данных, которые не зависят от состояния конкретных экземпляров.
  • Статические классы не могут быть созданы как объекты, так как они не могут иметь экземпляров.
  • Назначение статических классов:
    • Использование в качестве контейнеров для общих методов, которые не привязаны к состоянию объекта (например, математические функции, утилиты).
    • Упрощение доступа к функциональности без необходимости создания объектов.

Пример статического класса:

public static class MathUtilities
{
public static double Square(double number)
{
return number * number;
}

public static double Cube(double number)
{
return number * number * number;
}
}

Использование:

double result = MathUtilities.Square(5);  // Вызов метода без создания экземпляра класса
Console.WriteLine(result); // Вывод: 25

4.2. Статические методы и поля

Статические методы

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

Пример статического метода:

public class Calculator
{
public static int Add(int a, int b)
{
return a + b;
}
}

Использование:

int sum = Calculator.Add(3, 4);  // Вызов статического метода без создания экземпляра

Статические поля

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

Пример статического поля:

public class Counter
{
public static int TotalCount = 0; // Статическое поле

public Counter()
{
TotalCount++; // Увеличение общего счётчика при создании нового объекта
}
}

Использование:

Counter c1 = new Counter();
Counter c2 = new Counter();
Console.WriteLine(Counter.TotalCount); // Вывод: 2

4.3. Использование статических членов в нестатических классах

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

Пример использования статических членов в нестатическом классе:

public class Employee
{
private static int employeeCount = 0; // Статическое поле
public string Name { get; }

public Employee(string name)
{
Name = name;
employeeCount++; // Увеличение общего числа сотрудников при создании нового объекта
}

public static int GetEmployeeCount() // Статический метод
{
return employeeCount;
}
}

Использование:

Employee e1 = new Employee("Alice");
Employee e2 = new Employee("Bob");

Console.WriteLine(Employee.GetEmployeeCount()); // Вывод: 2

4.4. Порядок инициализации статических членов

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

Статический конструктор

  • Статический конструктор вызывается автоматически один раз перед первым использованием класса и служит для инициализации статических полей.
  • Статический конструктор не принимает параметров и не может быть вызван явно.

Пример статического конструктора:

public class DatabaseConnection
{
public static string ConnectionString;

// Статический конструктор
static DatabaseConnection()
{
ConnectionString = "Server=myServerAddress;Database=myDataBase;";
Console.WriteLine("Статический конструктор вызван.");
}
}

Использование:

Console.WriteLine(DatabaseConnection.ConnectionString);  // Статический конструктор вызывается автоматически

Порядок инициализации

  • Статические члены инициализируются в следующем порядке:
    1. Статические поля, которые имеют явную инициализацию, инициализируются первыми.
    2. Если есть статический конструктор, он вызывается после инициализации статических полей.
    3. Все статические члены инициализируются один раз при первом обращении к классу.

Пример:

public class Example
{
public static int value = 10; // Инициализация статического поля

static Example()
{
Console.WriteLine("Статический конструктор вызван.");
value = 20; // Переинициализация значения в статическом конструкторе
}
}

При первом обращении к классу Example, вывод будет:

Статический конструктор вызван.

Изначально значение value будет 10, но после вызова статического конструктора оно станет 20.


Ключевое слово static в C# играет важную роль в управлении общими данными и функциональностью на уровне класса. Статические классы, методы и поля позволяют организовать логику, которая не зависит от конкретных экземпляров классов, что полезно для работы с глобальными настройками, общими ресурсами и утилитарными функциями. Статические члены инициализируются один раз, и порядок их инициализации строго управляется сборщиком среды выполнения C#.

5. Наследование (Inheritance)

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

5.1. Основные понятия наследования

  • Наследование — это механизм, позволяющий одному классу (производному) наследовать поля и методы другого класса (базового).
  • Главная цель наследования — повторное использование кода и создание иерархий классов.
  • Преимущества наследования:
    • Повышает модульность и повторное использование кода.
    • Упрощает поддержку и расширение существующего кода.
    • Позволяет создавать более общие структуры для решения типовых задач.

5.2. Базовый и производный классы

  • Базовый класс (родительский) — это класс, от которого наследуются другие классы.
  • Производный класс (дочерний) — это класс, который наследует члены базового класса и может добавлять новые поля и методы или изменять существующие.

Пример базового и производного классов:

public class Animal  // Базовый класс
{
public string Name { get; set; }

public void Eat()
{
Console.WriteLine("Eating...");
}
}

public class Dog : Animal // Производный класс
{
public void Bark()
{
Console.WriteLine("Barking...");
}
}

Использование:

Dog dog = new Dog();
dog.Name = "Buddy"; // Наследуемое свойство
dog.Eat(); // Наследуемый метод
dog.Bark(); // Собственный метод класса Dog

5.3. Ключевое слово base и его использование

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

Пример использования base:

public class Animal
{
public string Name { get; set; }

public Animal(string name)
{
Name = name;
Console.WriteLine("Animal created");
}

public void Speak()
{
Console.WriteLine("Animal sound");
}
}

public class Dog : Animal
{
public Dog(string name) : base(name) // Вызов конструктора базового класса
{
Console.WriteLine("Dog created");
}

public void Bark()
{
base.Speak(); // Вызов метода базового класса
Console.WriteLine("Dog is barking");
}
}

Использование:

Dog dog = new Dog("Buddy");  // Вывод: "Animal created", "Dog created"
dog.Bark(); // Вывод: "Animal sound", "Dog is barking"

5.4. Модификатор доступа protected и его роль в наследовании

  • Модификатор protected позволяет членам класса быть доступными внутри самого класса и его производных классов, но недоступными вне этих классов.
  • Это полезно, когда нужно предоставить доступ к данным для производных классов, но ограничить доступ для внешних объектов.

Пример:

public class Animal
{
protected string Name;

public void SetName(string name)
{
Name = name;
}
}

public class Dog : Animal
{
public void Display()
{
Console.WriteLine($"Dog's name is {Name}"); // Доступ к protected полю
}
}

Использование:

Dog dog = new Dog();
dog.SetName("Buddy");
dog.Display(); // Вывод: "Dog's name is Buddy"

5.5. Переопределение методов: ключевые слова virtual, override, new

virtual

  • Методы в базовом классе могут быть отмечены как virtual, что позволяет производным классам переопределять эти методы.
  • По умолчанию метод в базовом классе не является виртуальным, и его нельзя переопределить в производном классе.

Пример:

public class Animal
{
public virtual void Speak() // Виртуальный метод
{
Console.WriteLine("Animal sound");
}
}

override

  • Ключевое слово override используется в производных классах для переопределения виртуального метода базового класса.

Пример:

public class Dog : Animal
{
public override void Speak() // Переопределение метода
{
Console.WriteLine("Dog is barking");
}
}

new

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

Пример:

public class Cat : Animal
{
public new void Speak() // Скрытие метода базового класса
{
Console.WriteLine("Cat is meowing");
}
}

5.6. Абстрактные классы и методы

Определение абстрактного класса

  • Абстрактный класс — это класс, который не может быть создан напрямую и предназначен для того, чтобы быть базовым для других классов.
  • Абстрактные классы могут содержать как обычные методы, так и абстрактные методы (методы без реализации).

Пример:

public abstract class Animal
{
public abstract void MakeSound(); // Абстрактный метод
}

Особенности реализации абстрактных методов

  • Абстрактные методы не содержат тела и должны быть переопределены в производных классах.
  • Производные классы обязаны предоставить реализацию для всех абстрактных методов базового класса.

Пример:

public class Dog : Animal
{
public override void MakeSound() // Обязательное переопределение абстрактного метода
{
Console.WriteLine("Dog is barking");
}
}

Пример использования

public abstract class Animal
{
public abstract void MakeSound();
}

public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Dog is barking");
}
}

public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Cat is meowing");
}
}

Animal dog = new Dog();
dog.MakeSound(); // Вывод: "Dog is barking"

5.7. Проблемы множественного наследования и решение через интерфейсы

  • В C# множественное наследование классов не поддерживается, то есть класс не может наследоваться от более чем одного базового класса.
  • Это решение было принято, чтобы избежать сложностей и конфликтов при объединении функциональности нескольких базовых классов (например, когда два базовых класса имеют методы с одинаковыми именами).

Решение через интерфейсы

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

Пример:

public interface IWalk
{
void Walk();
}

public interface ISwim
{
void Swim();
}

public class Dog : IWalk, ISwim // Реализация нескольких интерфейсов
{
public void Walk()
{
Console.WriteLine("Dog is walking");
}

public void Swim()
{
Console.WriteLine("Dog is swimming");
}
}

Использование:

Dog dog = new Dog();
dog.Walk(); // Вывод: "Dog is walking"
dog.Swim(); // Вывод: "Dog is swimming"

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

6. Полиморфизм

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

6.1. Виртуальные методы и полиморфизм

  • Полиморфизм в C# реализуется через виртуальные методы, которые могут быть переопределены в производных классах. Это позволяет классу изменять поведение, унаследованное от базового класса.
  • Виртуальный метод определяется в базовом классе с ключевым словом virtual, а в производных классах его можно переопределить с помощью ключевого слова override.
  • Это позволяет производным классам реализовывать свою собственную версию метода, при этом сохраняя возможность использовать базовый класс как интерфейс для работы с разными объектами.

6.2. Виртуальные и переопределенные члены (virtual и override)

Виртуальные члены (virtual)

  • Виртуальный метод — это метод, который может быть переопределен в классе-наследнике. Виртуальные методы определяются в базовом классе с ключевым словом virtual.

Пример виртуального метода:

public class Animal
{
public virtual void Speak() // Виртуальный метод
{
Console.WriteLine("Animal makes a sound");
}
}

Переопределение членов (override)

  • Переопределение используется в производных классах для замены или изменения поведения виртуальных методов базового класса. Для этого в производном классе используется ключевое слово override.

Пример переопределения метода:

public class Dog : Animal
{
public override void Speak() // Переопределение метода
{
Console.WriteLine("Dog barks");
}
}

public class Cat : Animal
{
public override void Speak() // Переопределение метода
{
Console.WriteLine("Cat meows");
}
}

6.3. Раннее и позднее связывание (статический и динамический полиморфизм)

Раннее связывание (статический полиморфизм)

  • Раннее связывание — это когда решение о том, какой метод вызывать, принимается на этапе компиляции. Это относится к перегрузке методов (overloading), когда методы различаются по сигнатурам (аргументам) и вызываются в зависимости от переданных параметров.

Пример перегрузки методов:

public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}

public double Add(double a, double b)
{
return a + b;
}
}

Позднее связывание (динамический полиморфизм)

  • Позднее связывание — это когда решение о том, какой метод вызывать, принимается на этапе выполнения программы. Это относится к виртуальным методам и переопределению методов.
  • При работе с объектами базового класса, если метод помечен как virtual и переопределен в производных классах с помощью override, вызов метода будет зависеть от реального типа объекта (то есть, какой класс его переопределяет), даже если он передается как экземпляр базового класса.

Пример позднего связывания:

Animal animal = new Dog();  // Переменная типа базового класса, но содержит объект производного
animal.Speak(); // Вывод: "Dog barks" (выполняется метод класса Dog)

6.4. Пример реализации полиморфизма

Давайте рассмотрим пример, который демонстрирует, как полиморфизм используется для работы с разными типами объектов, используя единый интерфейс — базовый класс Animal.

Пример полиморфизма:

public class Animal
{
public virtual void Speak() // Виртуальный метод
{
Console.WriteLine("Animal makes a sound");
}
}

public class Dog : Animal
{
public override void Speak() // Переопределение метода
{
Console.WriteLine("Dog barks");
}
}

public class Cat : Animal
{
public override void Speak() // Переопределение метода
{
Console.WriteLine("Cat meows");
}
}

public class Program
{
public static void Main()
{
Animal[] animals = new Animal[3]; // Массив типа базового класса
animals[0] = new Animal(); // Базовый класс
animals[1] = new Dog(); // Производный класс Dog
animals[2] = new Cat(); // Производный класс Cat

foreach (Animal animal in animals)
{
animal.Speak(); // Полиморфный вызов методов
}
}
}

Вывод программы:

Animal makes a sound
Dog barks
Cat meows

Объяснение:

  • В примере создается массив объектов типа Animal, который включает экземпляры как самого базового класса Animal, так и его производных классов — Dog и Cat.
  • При вызове метода Speak() для каждого объекта используется полиморфизм. В зависимости от фактического типа объекта вызывается соответствующий метод, даже если он передается как объект базового класса.

Полиморфизм в C# позволяет базовому классу предоставлять общий интерфейс для всех своих производных классов, позволяя каждому из них иметь собственную реализацию методов. Это упрощает работу с объектами, обеспечивает гибкость и расширяемость кода, делает программы легче поддерживаемыми и позволяет работать с объектами разных типов через единый интерфейс.

7. Интерфейсы

7.1. Определение интерфейса и его отличие от класса

Интерфейс — это контракт, который определяет набор методов, свойств, событий или индексаторов, которые класс или структура должны реализовать. Интерфейс сам по себе не содержит реализации этих членов, только их сигнатуры. Класс или структура, реализующие интерфейс, обязаны предоставить конкретную реализацию для всех его членов.

Отличия интерфейса от класса:

  • Интерфейс не может содержать реализации методов (до C# 8.0).
  • Класс может наследоваться только от одного базового класса, но реализовывать множество интерфейсов.
  • Интерфейс не может содержать поля.
  • Интерфейсы определяют только поведение, но не предоставляют его реализацию.
  • Нельзя создать объект интерфейса напрямую, в отличие от классов.

7.2. Объявление интерфейсов

Интерфейс объявляется с использованием ключевого слова interface и может содержать методы, свойства, события и индексаторы, но без реализации.

Пример объявления интерфейса:

public interface IMovable
{
void Move(); // Метод без реализации
int Speed { get; set; } // Свойство без реализации
}

В этом примере интерфейс IMovable требует от любого класса, который его реализует, предоставить реализацию для метода Move() и свойства Speed.

7.3. Реализация интерфейсов в классах

Классы, реализующие интерфейс, обязаны предоставить реализацию всех методов и свойств, определенных в интерфейсе. Это делает интерфейс контрактом, который класс должен соблюдать.

Пример реализации интерфейса:

public class Car : IMovable
{
public int Speed { get; set; }

public void Move()
{
Console.WriteLine($"Car is moving at speed {Speed} km/h.");
}
}

Класс Car реализует интерфейс IMovable и предоставляет конкретную реализацию для метода Move() и свойства Speed.

7.4. Множественная реализация интерфейсов

Классы могут реализовывать несколько интерфейсов одновременно. Это одно из ключевых преимуществ интерфейсов перед классами, так как C# не поддерживает множественное наследование классов, но позволяет множественную реализацию интерфейсов.

Пример множественной реализации интерфейсов:

public interface IFlyable
{
void Fly();
}

public class FlyingCar : IMovable, IFlyable
{
public int Speed { get; set; }

public void Move()
{
Console.WriteLine($"Flying car is moving at speed {Speed} km/h.");
}

public void Fly()
{
Console.WriteLine("Flying car is flying.");
}
}

Класс FlyingCar реализует как интерфейс IMovable, так и интерфейс IFlyable, предоставляя реализацию для методов и свойств обоих интерфейсов.

7.5. Статические члены интерфейсов (в C# 8.0 и выше)

Начиная с версии C# 8.0, интерфейсы могут содержать статические методы, а также методы с реализацией по умолчанию. Это добавляет гибкость в использовании интерфейсов, позволяя предоставлять некоторую базовую логику внутри интерфейса.

Пример статического метода в интерфейсе:

public interface ICalculator
{
static int Add(int a, int b)
{
return a + b;
}
}

Теперь метод Add() может быть вызван напрямую через интерфейс без реализации в классе:

int result = ICalculator.Add(5, 10);
Console.WriteLine(result); // Вывод: 15

7.6. Интерфейсы как контракт

Интерфейсы служат контрактами для классов. Это означает, что любой класс, реализующий интерфейс, гарантирует, что он содержит реализацию всех методов, определенных в интерфейсе. Это позволяет разработчикам писать код, который работает с интерфейсами, не зная точной реализации класса.

Пример:

public void StartMovement(IMovable movable)
{
movable.Move();
}

В этом примере метод StartMovement может принимать любой объект, реализующий интерфейс IMovable, и гарантировано вызвать метод Move(), не беспокоясь о конкретной реализации класса.

7.7. Пример реализации интерфейса

Пример использования интерфейса для создания контрактов между разными классами:

public interface IDriveable
{
void Drive();
int FuelLevel { get; set; }
}

public class Car : IDriveable
{
public int FuelLevel { get; set; }

public void Drive()
{
if (FuelLevel > 0)
{
Console.WriteLine("Car is driving.");
FuelLevel--;
}
else
{
Console.WriteLine("Car is out of fuel.");
}
}
}

public class ElectricCar : IDriveable
{
public int FuelLevel { get; set; }

public void Drive()
{
if (FuelLevel > 0)
{
Console.WriteLine("Electric car is driving silently.");
FuelLevel--;
}
else
{
Console.WriteLine("Electric car needs recharging.");
}
}
}

class Program
{
static void Main()
{
IDriveable car = new Car { FuelLevel = 3 };
IDriveable electricCar = new ElectricCar { FuelLevel = 2 };

car.Drive(); // Вывод: Car is driving.
electricCar.Drive(); // Вывод: Electric car is driving silently.
}
}

Объяснение:

  • В этом примере два разных класса Car и ElectricCar реализуют интерфейс IDriveable, что позволяет обращаться к ним одинаково через интерфейс.
  • Метод Drive() вызывается для каждого объекта, и каждый класс предоставляет свою реализацию этого метода.

Интерфейсы в C# обеспечивают важный механизм для создания гибких и расширяемых систем, задавая контракты, которые классы должны реализовать. Они позволяют реализовать множественное наследование через множественную реализацию интерфейсов и являются основой для полиморфизма и модульности кода. С введением статических методов и методов с реализацией по умолчанию в C# 8.0, интерфейсы стали еще более мощным инструментом для построения архитектуры приложений.

8. Инкапсуляция

Инкапсуляция — это один из основных принципов объектно-ориентированного программирования (ООП), который подразумевает скрытие внутренней реализации объекта и предоставление доступа к нему только через публичные методы и свойства. Это позволяет защитить данные и функции от некорректного использования и внешних воздействий, а также обеспечивает управление доступом к данным объекта.

8.1. Принцип инкапсуляции в ООП

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

Основные цели инкапсуляции:

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

8.2. Модификаторы доступа и их роль в инкапсуляции

Модификаторы доступа в C# позволяют ограничивать доступ к членам класса (методам, полям, свойствам и т. д.). Использование правильных модификаторов доступа является важной частью инкапсуляции.

Основные модификаторы доступа:

  1. public — член класса доступен из любого кода.
  2. private — член класса доступен только внутри того класса, в котором он определён. Это самый закрытый уровень доступа.
  3. protected — член класса доступен внутри самого класса и его производных классов.
  4. internal — член класса доступен только внутри текущей сборки (assembly).
  5. protected internal — доступен для производных классов и классов внутри текущей сборки.
  6. private protected — доступен только для производных классов внутри текущей сборки.

Пример использования модификаторов:

public class BankAccount
{
private decimal balance; // Закрытое поле (инкапсуляция)

public void Deposit(decimal amount)
{
if (amount > 0)
{
balance += amount; // Изменение закрытого поля через публичный метод
}
}

public decimal GetBalance() // Публичный метод для получения значения
{
return balance;
}
}

8.3. Разграничение доступа к полям через свойства

Вместо предоставления прямого доступа к полям объекта через public поля, инкапсуляция достигается через свойства. Свойства позволяют скрывать внутренние поля и контролировать доступ к ним через геттеры и сеттеры.

Пример с использованием свойств:

public class Person
{
private string name; // Закрытое поле

public string Name // Публичное свойство для доступа к полю
{
get { return name; } // Геттер для чтения значения
set // Сеттер для установки значения
{
if (!string.IsNullOrEmpty(value))
{
name = value;
}
}
}
}

В этом примере:

  • Поле name является приватным, что предотвращает прямой доступ к нему.
  • Свойство Name предоставляет управляемый доступ к полю: можно задавать и получать имя, но с проверкой на некорректные данные в сеттере.

8.4. Примеры использования инкапсуляции для обеспечения безопасности данных

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

Пример 1: Контроль изменения данных

public class Product
{
private decimal price;

public decimal Price
{
get { return price; }
set
{
if (value > 0)
{
price = value; // Только положительные значения могут быть установлены
}
}
}
}

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

Пример 2: Защита от некорректных данных

public class Student
{
private int age;

public int Age
{
get { return age; }
set
{
if (value > 0 && value < 120) // Логическая проверка на допустимый диапазон значений
{
age = value;
}
}
}
}

Здесь инкапсуляция помогает избежать ошибок, ограничив возраст диапазоном значений от 0 до 120.

Пример 3: Изменение состояния через методы

public class Thermostat
{
private double temperature;

public double Temperature
{
get { return temperature; }
}

public void SetTemperature(double temp)
{
if (temp >= -30 && temp <= 50)
{
temperature = temp; // Устанавливаем температуру в безопасном диапазоне
}
else
{
Console.WriteLine("Temperature out of range.");
}
}
}

В данном случае инкапсуляция защищает поле temperature от прямого изменения, обеспечивая контроль над изменениями через метод SetTemperature, который проверяет, находится ли значение в допустимом диапазоне.


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

9. Обобщенные классы (Generics)

Обобщения (Generics) — это важная концепция в C#, которая позволяет создавать классы, методы и структуры, способные работать с данными разных типов без необходимости дублирования кода для каждого конкретного типа. Это повышает гибкость, производительность и безопасность типов в коде.

9.1. Основы обобщений в C#

Обобщения в C# позволяют создавать универсальные классы и методы, которые могут работать с любыми типами данных. Обобщения применяются там, где нужно реализовать общую логику для разных типов данных, избегая их дублирования.

Основные возможности обобщений:

  • Обеспечение типа безопасности: ошибки типа данных обнаруживаются на этапе компиляции, а не во время выполнения.
  • Повторное использование кода: обобщения позволяют писать универсальный код для любых типов данных.
  • Производительность: отсутствие необходимости в упаковке/распаковке данных для работы с ними (например, при использовании коллекций).

9.2. Объявление обобщенных классов и методов

Обобщенные классы

Для объявления обобщенного класса используется синтаксис с параметрами типа. Параметр типа указывается в угловых скобках <T>, где T — это название типа (может быть любым, но часто используется T, U, V для простоты).

Пример обобщенного класса:

public class Box<T>
{
private T item;

public Box(T item)
{
this.item = item;
}

public T GetItem()
{
return item;
}

public void SetItem(T item)
{
this.item = item;
}
}

В этом примере класс Box может работать с любым типом данных: int, string, double, и т.д.

Использование обобщенного класса:

Box<int> intBox = new Box<int>(123);  // Box для целых чисел
Box<string> stringBox = new Box<string>("Hello"); // Box для строк

Console.WriteLine(intBox.GetItem()); // Вывод: 123
Console.WriteLine(stringBox.GetItem()); // Вывод: Hello

Обобщенные методы

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

Пример обобщенного метода:

public class Utilities
{
public static void PrintArray<T>(T[] array)
{
foreach (T item in array)
{
Console.WriteLine(item);
}
}
}

Использование обобщенного метода:

int[] numbers = { 1, 2, 3 };
string[] words = { "apple", "banana", "cherry" };

Utilities.PrintArray(numbers); // Вызов метода для массива целых чисел
Utilities.PrintArray(words); // Вызов метода для массива строк

9.3. Ограничения обобщений (constraints)

Обобщенные классы и методы могут быть ограничены определенными типами с помощью ограничений (constraints). Это помогает уточнить, какие типы можно использовать в качестве параметров типа.

Основные типы ограничений:

  1. where T : struct — ограничивает параметр типа только значимыми типами.
  2. where T : class — ограничивает параметр типа ссылочными типами.
  3. where T : new() — требует, чтобы тип имел публичный конструктор без параметров.
  4. where T : BaseClass — ограничивает тип, который должен быть наследником указанного базового класса.
  5. where T : IInterface — требует, чтобы тип реализовывал определенный интерфейс.

Пример использования ограничений:

public class DataProcessor<T> where T : class, new()
{
public T CreateInstance()
{
return new T(); // Может быть создан только объект с конструктором по умолчанию
}
}

9.4. Преимущества использования обобщенных классов

  1. Безопасность типов:

    • Используя обобщения, можно избежать ошибок времени выполнения, связанных с неверными типами данных, так как ошибки будут выявляться на этапе компиляции.
  2. Универсальность и гибкость:

    • Один обобщенный класс или метод может работать с любым типом данных, что делает код более универсальным.
  3. Повышение производительности:

    • Отсутствие необходимости в упаковке и распаковке значимых типов при использовании коллекций (например, List<T>), что снижает накладные расходы.
  4. Повторное использование кода:

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

9.5. Пример обобщенного класса и метода

Пример обобщенного класса: стек (Stack)

Обобщенный класс, реализующий стек (LIFO — last in, first out):

public class Stack<T>
{
private T[] items;
private int currentIndex = -1;

public Stack(int size)
{
items = new T[size];
}

public void Push(T item)
{
if (currentIndex == items.Length - 1)
throw new InvalidOperationException("Stack is full");

items[++currentIndex] = item;
}

public T Pop()
{
if (currentIndex == -1)
throw new InvalidOperationException("Stack is empty");

return items[currentIndex--];
}
}

Использование:

Stack<int> intStack = new Stack<int>(3);
intStack.Push(1);
intStack.Push(2);
intStack.Push(3);
Console.WriteLine(intStack.Pop()); // Вывод: 3
Console.WriteLine(intStack.Pop()); // Вывод: 2

Stack<string> stringStack = new Stack<string>(2);
stringStack.Push("apple");
stringStack.Push("banana");
Console.WriteLine(stringStack.Pop()); // Вывод: banana

Пример обобщенного метода: нахождение максимального значения

Обобщенный метод для нахождения максимального значения из двух элементов:

public class Utilities
{
public static T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
}

Использование:

int maxInt = Utilities.Max(3, 7);  // Работа с целыми числами
string maxString = Utilities.Max("apple", "banana"); // Работа со строками

Console.WriteLine(maxInt); // Вывод: 7
Console.WriteLine(maxString); // Вывод: banana

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

10. Ключевые концепции работы с памятью

10.1. Управление памятью в C#

В C# управление памятью реализуется автоматически, что делает работу с памятью значительно проще и безопаснее по сравнению с языками, где память нужно выделять и освобождать вручную (например, C++). В C# за распределение и освобождение памяти отвечает сборщик мусора (Garbage Collector), который работает в фоне и автоматически освобождает неиспользуемые объекты, предотвращая утечки памяти.

Основные аспекты управления памятью:

  • Автоматическая сборка мусора — сборщик мусора следит за неиспользуемыми объектами и освобождает память, когда объект больше не нужен.
  • Куча (heap) и стек (stack) — это две области памяти, которые используются для хранения данных разных типов.
  • Ссылочные типы хранятся в куче, а значимые типы — в стеке (если они не являются частью объекта).

10.2. Объекты в куче (heap) и ссылки

В C# память распределяется в двух основных областях:

  • Стек (stack) — используется для хранения значимых типов (например, структур, примитивов) и указателей на объекты ссылочных типов. Стек имеет фиксированный размер и работает по принципу "последним пришел — первым вышел" (LIFO). Доступ к данным в стеке происходит очень быстро.
  • Куча (heap) — используется для хранения ссылочных типов (например, объектов классов). Куча — это область памяти, которая динамически расширяется, но доступ к данным в ней происходит медленнее, чем в стеке.

Работа с объектами в куче:

Когда создается объект класса, он хранится в куче, а в стеке хранится ссылка на этот объект. Например:

Person person = new Person();  // Объект создается в куче, а ссылка хранится в стеке

В этом примере объект Person находится в куче, а переменная person в стеке содержит ссылку на этот объект.

10.3. Сборка мусора (Garbage Collector) и его роль

Сборка мусора (Garbage Collector, GC) — это механизм в .NET, который автоматически освобождает память, занятую объектами, которые больше не используются в программе. Сборщик мусора отслеживает ссылки на объекты в куче и удаляет те объекты, на которые больше нет ссылок, освобождая тем самым память.

Принцип работы сборщика мусора:

  1. Создание объекта: Когда создается объект, память для него выделяется в куче.
  2. Использование объекта: Объект используется в программе до тех пор, пока на него есть ссылки.
  3. Сбор мусора: Когда объект становится недоступным (на него нет ссылок), сборщик мусора может освободить память, занятую этим объектом, чтобы она могла быть использована для других объектов.

Этапы сборки мусора:

  • Generation 0 (Gen 0) — область для недавно созданных объектов. Если объект переживает сборку мусора, он перемещается в Gen 1.
  • Generation 1 (Gen 1) — промежуточная область для объектов, которые пережили одну сборку мусора.
  • Generation 2 (Gen 2) — область для объектов с долгим сроком жизни (например, объекты, которые используются в течение всего времени работы программы).

Пример простого сценария сборки мусора:

void CreateObject()
{
Person person = new Person(); // Объект создается в куче
// После выхода из метода переменная `person` больше недоступна, и объект может быть собран сборщиком мусора
}

Роль сборщика мусора:

  • Автоматическое управление памятью: Разработчики не беспокоятся о ручном освобождении памяти.
  • Предотвращение утечек памяти: Сборщик мусора удаляет неиспользуемые объекты, освобождая память.
  • Оптимизация производительности: Сборка мусора помогает поддерживать свободную память, снижая необходимость расширять область кучи.

10.4. Разница между классами (ссылочные типы) и структурами (значимые типы)

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

Ссылочные типы

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

Пример работы со ссылочным типом:

public class Person
{
public string Name { get; set; }
}

Person person1 = new Person { Name = "Alice" };
Person person2 = person1; // person2 указывает на тот же объект, что и person1

person2.Name = "Bob";
Console.WriteLine(person1.Name); // Вывод: Bob (изменение через person2 отразилось на person1)

Значимые типы

  • Значимые типы хранятся в стеке (если не являются частью ссылочного объекта).
  • Примеры: структуры, примитивные типы (int, float, bool).
  • Переменная значимого типа хранит значение, а не ссылку на объект.
  • При передаче в метод копируется значение, и изменения в методе не влияют на исходные данные.

Пример работы со значимым типом:

public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}

Point point1 = new Point { X = 10, Y = 20 };
Point point2 = point1; // Копируется значение, а не ссылка

point2.X = 30;
Console.WriteLine(point1.X); // Вывод: 10 (point1 не изменился)

Сравнение классов и структур:

Классы (ссылочные типы)Структуры (значимые типы)
Хранятся в кучеХранятся в стеке (если не являются частью объекта)
Передаются по ссылкеПередаются по значению
Не копируются при присваиванииКопируются при присваивании
Может иметь деструкторНе может иметь деструктор
Используются для сложных объектовИспользуются для небольших и простых объектов

Работа с памятью в C# упрощается благодаря автоматическому управлению памятью и сборке мусора. Понимание различий между ссылочными и значимыми типами, а также того, как куча и стек функционируют, помогает писать эффективный код. Сборщик мусора играет важную роль в освобождении неиспользуемых объектов, предотвращая утечки памяти и улучшая производительность приложения.

11. Работа с классами и файлами

11.1. Размещение классов в отдельных файлах

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

Основные преимущества размещения классов в отдельных файлах:

  • Читаемость: Один класс в одном файле упрощает поиск и чтение кода.
  • Модульность: Изменение одного класса не влияет на другие файлы и классы.
  • Удобство навигации: В современных IDE классы легко искать и открывать по их названиям.
  • Упрощение командной работы: Размещение классов в отдельных файлах помогает разделить задачи между разработчиками.

Пример структуры файлов в проекте:

/ProjectFolder
/Models
Person.cs
Car.cs
/Services
PersonService.cs
CarService.cs
Program.cs

Пример файла Person.cs:

namespace Project.Models
{
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
}

Пример файла Car.cs:

namespace Project.Models
{
public class Car
{
public string Model { get; set; }
public int Year { get; set; }
}
}

11.2. Пространства имен (namespaces) и их роль в организации кода

Пространства имен (namespaces) — это механизм для логической организации кода, который помогает избежать конфликтов имен и структурировать код. Пространства имен позволяют разделять классы, интерфейсы и другие типы данных в больших проектах, обеспечивая более чистую и понятную архитектуру.

Основные цели пространств имен:

  • Избежание конфликтов имен: Пространства имен помогают различать классы с одинаковыми именами, если они находятся в разных контекстах (например, в разных библиотеках или модулях).
  • Логическая организация кода: Пространства имен группируют классы по функциональности (например, Models, Services, Controllers).
  • Читаемость: Они помогают структурировать проект, что облегчает понимание его структуры.

Пример использования пространств имен:

namespace Project.Models
{
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
}

Использование пространств имен:

Чтобы использовать класс из другого пространства имен, необходимо импортировать его с помощью ключевого слова using:

using Project.Models;

public class Program
{
public static void Main()
{
Person person = new Person { Name = "Alice", Age = 30 };
Console.WriteLine(person.Name);
}
}

Можно также обращаться к классу напрямую, указывая полное имя:

public class Program
{
public static void Main()
{
Project.Models.Person person = new Project.Models.Person { Name = "Alice", Age = 30 };
Console.WriteLine(person.Name);
}
}

11.3. Структура проектов и файлы классов

Хорошо организованная структура проектов в C# помогает сделать проект более понятным, легко поддерживаемым и масштабируемым. При проектировании структуры обычно создаются отдельные папки для классов, которые выполняют различные функции, такие как модели, сервисы, контроллеры и интерфейсы.

Пример структуры проекта:

/ProjectFolder
/Models
Person.cs
Car.cs
/Services
PersonService.cs
CarService.cs
/Controllers
PersonController.cs
CarController.cs
/Interfaces
IPersonService.cs
ICarService.cs
Program.cs

Описание компонентов:

  1. Models — содержит классы моделей данных. Эти классы описывают структуры объектов (например, Person, Car).
  2. Services — содержит бизнес-логику проекта. Классы сервисов выполняют обработку данных и взаимодействие с базой данных.
  3. Controllers — классы, отвечающие за обработку запросов (в случае ASP.NET) и взаимодействие с сервисами.
  4. Interfaces — интерфейсы для сервисов или других компонентов проекта, которые описывают контракты для их реализации.

Пример структуры файлов:

  1. Person.cs (в папке Models):
namespace Project.Models
{
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
}
  1. PersonService.cs (в папке Services):
using Project.Models;

namespace Project.Services
{
public class PersonService
{
public void PrintPersonInfo(Person person)
{
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
}
}
}
  1. PersonController.cs (в папке Controllers):
using Project.Models;
using Project.Services;

namespace Project.Controllers
{
public class PersonController
{
private readonly PersonService _personService;

public PersonController()
{
_personService = new PersonService();
}

public void ShowPersonInfo()
{
Person person = new Person { Name = "Alice", Age = 30 };
_personService.PrintPersonInfo(person);
}
}
}

Пример использования в Program.cs:

using Project.Controllers;

class Program
{
static void Main()
{
PersonController personController = new PersonController();
personController.ShowPersonInfo();
}
}

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