C# является полноценным объектно-ориентированным языком. Это значит, что программу на C# можно представить в виде взаимосвязанных взаимодействующих между собой объектов.
Описанием объекта является класс, а объект представляет экземпляр этого класса. Можно еще провести следующую аналогию. У нас у всех есть некоторое представление о человеке, у которого есть имя, возраст, какие-то другие характеристики. То есть некоторый шаблон — этот шаблон можно назвать классом. Конкретное воплощение этого шаблона может отличаться, например, одни люди имеют одно имя, другие — другое имя. И реально существующий человек (фактически экземпляр данного класса) будет представлять объект этого класса.
В принципе ранее уже использовались классы. Например, тип string, который представляет строку, фактически является классом. Или, например, класс Console, у которого метод WriteLine()
выводит на консоль некоторую информацию. Теперь же посмотрим, как мы можем определять свои собственные классы.
По сути класс представляет новый тип, который определяется пользователем. Класс определяется с помощью ключевого слова сlass:
class название_класса
{
// содержимое класса
}
После слова class идет имя класса и далее в фигурных скобках идет собственно содержимое класса. Например, определим в файле Program.cs класс Person, который будет представлять человека:
class Person
{
}
Однако такой класс не особо показателен, поэтому добавим в него некоторую функциональность.
Поля и методы класса
Класс может хранить некоторые данные. Для хранения данных в классе применяются поля. По сути поля класса — это переменные, определенные на уровне класса.
Кроме того, класс может определять некоторое поведение или выполняемые действия. Для определения поведения в классе применяются методы.
Итак, добавим в класс Person поля и методы:
class Person
{
public string name = "Undefined"; // имя
public int age; // возраст
public void Print()
{
Console.WriteLine($"Имя: {name} Возраст: {age}");
}
}
В данном случае в классе Person определено поле name
, которое хранит имя, и поле age
, которое хранит возраст человека. В отличие от переменных, определенных в методах, поля класса могут иметь модификаторы, которые указываются перед полем. Так, в данном случае, чтобы все поля были доступны вне класса Person поля определены с модификатором public.
При определении полей мы можем присвоить им некоторые значения, как в примере выше в случае переменной name
. Если поля класса не инициализированы, то они получают значения по умолчанию. Для переменных числовых типов это число 0.
Также в классе Person определен метод Print()
. Методы класса имеют доступ к его поля, и в данном случае обращаемся к полям класса name и age для вывода их значения на консоль. И чтобы этот метод был виден вне класса, он также определен с модификатором public.
Создание объекта класса
После определения класса мы можем создавать его объекты. Для создания объекта применяются конструкторы. По сути конструкторы представляют специальные методы, которые называются так же как и класс, и которые вызываются при создании нового объекта класса и выполняют инициализацию объекта. Общий синтаксис вызова конструктора:
new конструктор_класса(параметры_конструктора);
Сначала идет оператор new, который выделяет память для объекта, а после него идет вызов конструктора.
Конструктор по умолчанию
Если в классе не определено ни одного конструктора (как в случае с нашим классом Person), то для этого класса автоматически создается пустой конструктор по умолчанию, который не принимает никаких параметров.
Теперь создадим объект класса Person:
Person tom = new Person(); // создание объекта класса Person
// определение класса Person
class Person
{
public string name = "Undefined";
public int age;
public void Print()
{
Console.WriteLine($"Имя: {name} Возраст: {age}");
}
}
Для создания объекта Person используется выражение new Person()
. В итоге после выполнения данного выражения в памяти будет выделен участок, где будут храниться все данные объекта Person. А переменная tom
получит ссылку на созданный объект, и через эту переменную мы можем использовать данный объект и обращаться к его функциональности.
Обращение к функциональности класса
Для обращения к функциональности класса — полям, методам (а также другим элементам класса) применяется точечная нотация точки — после объекта класса ставится точка, а затем элемент класса:
объект.поле_класса
объект.метод_класса(параметры_метода)
Например, обратимся к полям и методам объекта Person:
Person tom = new Person(); // создание объекта класса Person
// Получаем значение полей в переменные
string personName = tom.name;
int personAge = tom.age;
Console.WriteLine($"Имя: {personName} Возраст {personAge}"); // Имя: Undefined Возраст: 0
// устанавливаем новые значения полей
tom.name = "Tom";
tom.age = 37;
// обращаемся к методу Print
tom.Print(); // Имя: Tom Возраст: 37
class Person
{
public string name = "Undefined";
public int age;
public void Print()
{
Console.WriteLine($"Имя: {name} Возраст: {age}");
}
}
Консольный вывод данной программы:
Имя: Undefined Возраст: 0 Имя: Tom Возраст: 37
Константы классы
Кроме полей класс может определять для хранения данных константы. В отличие от полей из значение устанавливается один раз непосредственно при их объявлении и впоследствии не может быть изменено. Кроме того, константы хранят некоторые данные, которые относятся не к одному объекту, а ко всему классу в целом. И для обращения к константам применяется не имя объекта, а имя класса:
Person tom = new Person();
tom.name = "Tom";
tom.age = 37;
tom.Print(); // Person: Tom - 37
Console.WriteLine(Person.type); // Person
// Person.type = "User"; // !Ошибка: изменить константу нельзя
class Person
{
public const string type = "Person";
public string name = "Undefined";
public int age;
public void Print() => Console.WriteLine($"{type}: {name} - {age}");
}
Здесь в классе Person определена константа type
, которая хранит название класса:
public
const
string
type = "Person";
Название класса не зависит от объекта. Мы можем создать много объектов Person, но название класса от этого не должно измениться — оно относится ко всем объектам Person и не должно меняться. Поэтому название типа можно сохранить в виде константы.
Стоит отметить, что константе сразу при ее определении необходимо присвоить значение.
Подобно обычным полям мы можем обращаться к константам класса внутри этого класса. Например, в методе Print значение константы выводится на консоль.
Однако если мы хотим обратиться к константе вне ее класса, то для обращения необходимо использовались имя класса:
Console.WriteLine(Person.type); // Person
Таким образом, если необходимо хранить данные, которые относятся ко всему классу в целом
Структуры
Наряду с классами структуры представляют еще один способ создания собственных типов данных в C#. Более того многие примитивные типы, например, int, double и т.д., по сути являются структурами.
Определение структуры
Для определения структуры применяется ключевое слово struct:
struct имя_структуры
{
// элементы структуры
}
После слова struct идет название структуры и далее в фигурных скобках размещаются элементы структуры — поля, методы и т.д.
Например, определим структуру, которая будет называться Person и которая будет представляет человека:
struct Person
{
}
Как и классы, структуры могут хранить состояние в виде полей (переменных) и определять поведение в виде методов. Например, добавим в структуру Person пару полей и метод:
struct Person
{
public string name;
public int age;
public void Print()
{
Console.WriteLine($"Имя: {name} Возраст: {age}");
}
}
В данном случае определены две переменные — name и age для хранения соответственно имени и возраста человека и метод Print для вывода информации о человеке на консоль.
И как и в случае с классами, для обращения к функциональности структуры — полям, методам и другим компонентам структуры применяется точечная нотация — после объекта структуры ставится точка, а затем указывается компонент структуры:
объект.поле_структуры
объект.метод_структуры(параметры_метода)
Создание объекта структуры
Инициализация с помощью конструктора
Для использования структуры ее необходмо инициализировать. Для инициализации создания объектов структуры, как и в случае с классами, применяется вызов конструктура с оператором new. Даже если в коде стуктуры не определено ни одного конструктора, тем не менее имеет как минимум один конструктор — конструктор по умолчанию, который генерируется компилятором. Этот конструктор не принимает параметров и создает объект структуры со значениями по умолчанию.
new
название_структуры();
Например, создадим объект структуры Person с помощью конструктора по умолчанию:
Person tom = new Person(); // вызов конструктора
// или так
// Person tom = new();
tom.name = "Tom"; // изменяем значение по умолчанию в поле name
tom.Print(); // Имя: Tom Возраст: 0
struct Person
{
public string name;
public int age;
public void Print()
{
Console.WriteLine($"Имя: {name} Возраст: {age}");
}
}
В данном случае создается объект tom. Для его создания вызывается конструктор по умолчанию, который устанавливает значения по умолчанию для его полей. Для числовых данных это значение 0, поэтому поле age
будет иметь значение 0. Для строк это значение null
, которое указывает на отсутствие значения. Но далее, если поля доступны (а в данном случае поскольку они имеют модификатор public
они доступны), мы можем измениь их значения. Так, здесь полю name
присваивается строка «Tom». Соответственно при выполнении метода Print()
мы получим следующий консольный вывод:
Имя: Tom Возраст: 0
Непосредственная иницилизация полей
Если все поля структуры доступны (как в случае с полями структуры Person, который имеют модификатор public), то структуру можно инициализировать без вызова конструктора. В этом случае необходимо присвоить значения всем полям структуры перед получением значений полей и обращением к методам структуры. Например:
Person tom; // не вызываем конструктор
// инициализация полей
tom.name = "Sam";
tom.age = 37;
tom.Print(); // Имя: Sam Возраст: 37
struct Person
{
public string name;
public int age;
public void Print()
{
Console.WriteLine($"Имя: {name} Возраст: {age}");
}
}
Инициализация полей по умолчанию
Стоит отметить, что начиная с версии C# 10, мы можем напрямую инициализировать поля структуры при их определении (до C# 10 это делать было нельзя):
Person tom = new Person();
tom.Print(); // Имя:Tom Возраст: 1
struct Person
{
// инициализация полей значениями по умолчанию - доступна с C#10
public string name = "Tom";
public int age = 1;
public void Print() =>Console.WriteLine($"Имя: {name} Возраст: {age}");
}
Однако даже в этом случае, несмотря на значения по умолчанию, необходимо вызывать конструктор, если мы хотим использоват эти значения.
Конструкторы структуры
Как и класс, структура может определять конструкторы. Однако, если в структуре определяется конструктор, то в нем обязательно надо инициализировать все поля структуры.
Например, добавим в структуру Person конструктор:
Person tom = new();
Person bob = new("Bob");
Person sam = new("Sam", 25);
tom.Print(); // !!!! Имя: Возраст: 0
bob.Print(); // Имя: Bob Возраст: 1
sam.Print(); // Имя: Sam Возраст: 25
struct Person
{
public string name;
public int age;
public Person(string name = "Tom", int age = 1)
{
this.name = name;
this.age = age;
}
public void Print() => Console.WriteLine($"Имя: {name} Возраст: {age}");
}
В данном случае в структуре Person определен конструктор с двумя параметрами, для которых предоставлены значения по умолчания. Однако обратите внимание на создание первого объекта структуры:
Person tom = new(); // по прежнему используется конструктор без параметров по умолчанию
tom.Print(); // !!!! Имя: Возраст: 0
Здесь по-прежнему применяется конструктор по умолчанию, тогда как при инициализации остальных двух переменных структуры применяется явно определенный конструктор.
Однако начиная с версии C# 10 мы можем определить свой конструктор без параметров:
Person tom = new();
tom.Print(); // Имя: Tom Возраст: 37
struct Person
{
public string name;
public int age;
public Person()
{
name = "Tom";
age = 37;
}
public void Print() => Console.WriteLine($"Имя: {name} Возраст: {age}");
}
Опять же при определении конструктора без параметров необходимо инициализировать все поля структуры.
В случае если нам необходимо вызывать конструкторы с различным количеством параметров, то мы можем, как и в случае с классами, вызывать их по цепочке:
Person tom = new();
Person bob = new("Bob");
Person sam = new("Sam", 25);
tom.Print(); // Имя: Tom Возраст: 1
bob.Print(); // Имя: Bob Возраст: 1
sam.Print(); // Имя: Sam Возраст: 25
struct Person
{
public string name;
public int age;
public Person() : this("Tom")
{ }
public Person(string name) : this(name, 1)
{ }
public Person(string name, int age)
{
this.name = name;
this.age = age;
}
public void Print() => Console.WriteLine($"Имя: {name} Возраст: {age}");
}
Конструкторы по прежнему должны инициализировать значения всех полей, однако поскольку при вызове любого конструктора цепочка все равно закончится на последнем конструкторе, который выполняет инициализацию, то инициализацию полей в других конструкторах можно не делать. Консольный вывод программы:
Имя: Tom Возраст: 1 Имя: Bob Возраст: 1 Имя: Sam Возраст: 25
Инициализатор структуры
Также, как и для класса, можно использовать инициализатор для создания структуры:
Person tom = new Person { name = "Tom", age = 22 };
tom.Print(); // Имя: Tom Возраст: 22
struct Person
{
public string name;
public int age;
public void Print() => Console.WriteLine($"Имя: {name} Возраст: {age}");
}
При использовании инициализатора сначала вызывается конструктор без параметров: если мы явным образом не определили конструктор без параметров, то вызывается конструктор по умолчанию. А затем его полям присваиваются соответствующие значения.
Копирование структуры с помощью with
Если нам необходимо скопировать в один объект структуры значения из другого с небольшими изменениями, то мы можем использовать оператор with:
Person tom = new Person { name = "Tom", age = 22 };
Person bob = tom with { name = "Bob" };
bob.Print(); // Имя: Bob Возраст: 22
В данном случае объект bob получает все значения объекта tom, а затем после оператора with в фигурных скобках указывается поля со значениями, которые мы хотим изменить.
В чем отличие между структурой и классом?
Структуры синтаксически очень похожи на классы, но существует принципиальное отличие, которое заключается в том, что класс – является ссылочным типом (reference type), а структуры – значимым типом (value type). Следовательно, классы всегда создаются в так называемой “куче” (heap), а структуры создаются в стеке (stack).
Но это справедливо в очень простых случаях, главное отличие структур и классов: структуры, указываемые в списке параметров метода, передаются по значению (то есть копируются), объекты классов — по ссылке. Именно это является главным различием в их поведении, а не то, где они хранятся. Примечание: структуру тоже можно передать по ссылке, используя модификаторы out и ref.
Чем больше вы будете использовать структуры вместо маленьких классов, тем менее затратным по ресурсам будет использование памяти.
Два правила структур
Первое правило структуры: Всегда все переменные должны быть инициализированы.
В классах Вы можете инициализировать значение полей непосредственно при их объявлении. В структурах такого сделать нельзя, и поэтому данный код вызовет ошибку при компиляции. Поэтому:
Второе правило структуры: Нельзя инициализировать переменные в том месте, где они объявляются.
Сравнение классов и структур в сжатом виде:
Вопрос | Структура | Класс |
Какого же типа экземпляр объекта? | Значимый (value) тип | Ссылочный (reference) тип |
Где “живут” экземпляры этих объектов? | Экземпляры структуры называют значениями и “живут” они в стеке (stack). | Экземпляры классов называют объектами и “живут” они в куче (heap). |
Можно ли создать конструктор по умолчанию? | Нет | Да |
Если создается свой конструктор, будет ли компилятор генерировать конструктор по умолчанию? | Да | Нет |
Если в своём конструкторе не будут инициализированы некоторые поля, будут ли они автоматически инициализированы компилятором? | Нет | Да |
Разрешается ли инициализировать переменные там, где их объявляют? | Нет | ДА |