Основы C#
October 12, 2022

Классы, поля и методы

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

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

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

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

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

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

Разберем на примере машины.
Пусть у нашего условного класса "машина" будет набор данных: "наименование", "номер", "тип машины", "количество колес", "максимальная скорость". И у класса будет определено поведение из методов: "ехать вперед с максимальной скоростью", "ехать назад", "открыть дверь".
Но наша "машина" лишь описывает набор свойств и общее поведение некоторого класс автомобилей, а уже создаваемые по этому описанию объекты автомобилей будут задавать свое собственное состояние.

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

Синтаксис объявления класса выглядит следующим образом:

class ИмяКласса
{
    //тело класса
}

Типы значений и ссылочные типы

В .NET память делится на два основных типа: стек и куча. Стек представляет собой структуру данных, которая растет снизу вверх: каждый новый добавляемый элемент помещается поверх предыдущего, а физически - это зарезервированная для программы, при ее запуске, область памяти. Кучей же (heap'ом) является остальное адресное пространство памяти, доступное процессору.

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

Типы значений - примитивы, можно описать как "неделимые" типы данных, располагающиеся в памяти в зависимости от контекста, обычно на стеке. К типам значений относятся: byte, sbyte, short, ushort, int, uint, long, ulong, float, double, decimal, bool, char. Также к типам значений относятся struct и enum, но о них мы поговорим немного позже:)

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

Все классы относятся к ссылочным типам. Кстати тип string на самом деле является псевдонимом класса String, а, соответственно, тоже является классом. Но выделение памяти при создании строк в .Net происходит немного иначе, нежели для обычных классов, мы рассмотрим это, когда будем подробно говорить о работе со строками.

У типов значений и ссылочных типов есть еще ряд отличий, но перечисленной информации пока достаточно для понимания рассматриваемого материала. А это нам и надо, чтобы все было по полочкам). Так что, пока стоит запомнить, что:

  1. Ссылочные типы хранятся в куче, а на стеке для них выделяется ссылка на данную область памяти.
  2. Типы значений являются примитивами и хранятся в зависимости от контекста на стеке или в куче. (Обычно типы значений хранятся на стеке, но никто не запрещает нам взять и выделить память под тот же int в куче)

Создание объектов класса

При объявлении переменных, если не проинициализировать их, то они будут иметь определенные дефолтные значения. Для типов значений дефолтными значениями являются: целочисленные и типы с плавающей точкой - 0, bool - false, char - '\0'. Ссылочные типы имеют в качестве значения по умолчанию пустую ссылку null (напомню, их переменные на стеке хранят именно ссылку на область в памяти, а так как объект не был создан, то и в памяти объекта еще нет).

int a;              //0
bool cond;          //false
ExampleRobot robot; //null

class ExampleRobot {}

Для создания нового экземпляра класса используется оператор new, который выполняет следующие действия:

  1. Вычисление необходимого количества памяти;
  2. Выделение памяти для объекта;
  3. Инициализация указателя на объект;
  4. Вызов конструктора экземпляра. (про конструкторы, деструкторы и жизненный цикл объектов будет материал далее, все идет своим чередом:)

Синтаксис создания объекта выглядит следующим образом:

ExampleRobot robot = new ExampleRobot();

Поля и методы класса

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

class Robot
{
    public int id;
    public string model;
    public string name = "fuckhead";
}

Как говорилось выше для задания поведения для объектов класса используются методы. Метод - это блок кода, объединяющий набор команд, которые выполняются при его вызове. Методы могут возвращать либо не возвращать значение. Методы, которые ничего не возвращают имеют тип возвращаемого значения void. Для возврата значения используется оператор return.

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

class Robot
{
    public int id;
    public string model;
    public string name = "fuckhead";
    
    public void Dance()
    {
        Console.WriteLine(quot;I'm {name} robot, I will dance");
    }
    
    public int MultiplyWithId(int a, int b)
    {
        int c = a * b * id;
        return c;
    }
}

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

Robot ironMan = new Robot();

ironMan.Dance(); //I'm fuckhead robot, I will dance

int c = ironMan.MultiplyWithId(1, 2); //0
ironMan.id = 5;
c = ironMan.MultiplyWithId(1, 2); //10

Для инициализации полей объекта класса мы можем, задать некоторое дефолтное значение поля, которое будет использоваться при инициализации объекта, можно обратиться к полям напрямую через имя объекта и присвоить какое-то значение. Также можно использовать инициализаторы объекта, позволяющие при создании объекта проинициализировать поля нужными значениями.

Robot ironMan = new Robot{ id=1, model="T1000", name="arnold" }

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

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

class Robot
{
    public int id;
    public static string model = "T1000";
    public static string name = "fuckhead";
    
    public static string GetInfo()
    {
        return quot;Model {model} is {name}";
    }
    
    public void Dance()
    {
        Console.WriteLine(quot;I'm {name} robot with id={id}, " +
            quot;I will dance");
    }
}

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

Robot.name = "Bender";
Console.WriteLine(Robot.GetInfo()); //Model T1000 is Bender

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


Области видимости и спецификаторы доступа

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

Можно выделить следующие контексты:

  • Контекст класса - перменные-поля класса существуют в области видимости до тех пор, пока в этой области находится, содержащий их класс;
  • Контекст метода - переменные, определенные в методе, доступны только в рамках данного метода и являются локальными, они существуют с начала объявления и до конца тела метода. Значения аргументов метода, переданных при вызове метода, считаются локальными переменными метода, а значит они также существуют до конца тела метода.
  • Контекст блока кода - переменные, определенные на уровне блока кода, также являются локальными и доступны только в рамках данного блока и существуют до конца этого блока кода.
class Car
{
    public string model;
    
    public void Drive()
    {
        Console.WriteLine(quot;{model} say tr tr tr");
    }
}

class Person
{
    public static string name = "Sanya";

    public static void DriveOnCar()
    {
        /*Поле model объекта класса Car существует в рамках контекста Car
        *и будет существовать в методе DriveOnCar 
        *пока существует данный объект класса.
        */
        Car car = new Car(){ mdoel="bmw" };
        car.Drive();
    }
    
    public static void Dance()
    {
        {
            /*Какой-то блок кода с локальной перменной a,
            *которая не будет доступна вне данного блока.
            *Т.е. вне этих скобочек в методе Dance мы уже
            *не сможем к ней обратиться.
            *Написал данное чисто для примера, 
            *не смог придумать ничего лучше.
            */
            int a = 1; 
        }
        
        /*От сюда мы не можем обратиться к a, ее здесь уже не существует)
        *И из данного метода мы не можем обратиться к объекту класса Car,
        *который создается в методе DriveOnCar, он существует в контексте 
        *только того метода.
        *
        *Но есть доступ к переменной name, которая находится 
        *в контексте класса Person.
        */
        
        Console.WriteLine(quot;{name} dick suck, and now dance")
    }
}

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

class Robot
{
    public string name = "fuckhed";
    
    public void Dance()
    {
        string name = "NewName";
        Console.WriteLine(quot;I'm {name} robot with id={id}, " +
                quot;I will dance");
    }
    
    public void OldDancer()
    {
        Console.WriteLine(quot;I'm {name} robot with id={id}, " +
                quot;I will dance");
    }
}

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

Существуют следующие спецификаторы:

  • public - элемент доступен из любого места программы;
  • private - доступен только внутри класса;
  • protected - доступен в самом классе и в классах наследниках;
  • internal - доступ возможен из любого кода в той же сборке; (Сборкой можно называть отдельно работающую программу (исполняемый файл exe) либо библиотеку(dll));
  • private protected - доступ к члену класса возможен внутри класса и в объявляющей сборке для классов-наследников.
  • protected internal - доступ к члену класса возможен только из его объявляющей сборки и из классов-наследников в любой сборке.

Если спецификатор не указать вручную, то будет присвоен спецификатор по умолчанию:

  1. Для всех классов модификатором доступа по умолчанию является internal;
  2. Для всех элементов класса модификатором доступа по умолчанию является private.

Теперь должно быть понятно, зачем мы прописывали public и зачем у метода Main указывался модификатор static.


На этом пока все. В следующей статье разберем управляющие конструкции и далее будем углубляться в работу с классами, разбирая все более крутые фишки языка C#. Берегите себя, пока!)