Классы и объекты C#: свойства

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

Введение

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

internal class Building
{
    public double width;
    public double length;
    public double height;

    public double GetVolume() => width * length * height;

    public Building(): this(10)
    {
        
    }

    public Building(double width): this(width, 20)
    { 
        
    }

    public Building(double width, double length): this(width, length, 30) 
    {

    }

    public Building(double width, double length, double height)
    {
        this.width = width;
        this.length = length;
        this.height = height;
    }
}

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

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

Всего в C# существует четыре ключевых слова для указания уровня доступа к классам и его членам: public, private, protected и internal. Из этих ключевых слов определяются шесть уровней доступа:

  1. public — доступ к типу или члену класса возможен из любого другого кода в том числе из извне.
  2. private — доступ к типу или члену возможен только из кода в том же объекте class или struct.
  3. protected — доступ к типу или члену возможен только из кода в том же объекте class либо в class, производном от этого class.
  4. internal — доступ к типу или члену возможен из любого кода в той же сборке, но не из другой сборки.
  5. protected internal — доступ к типу или члену возможен из любого кода в той сборке, где он был объявлен, или из производного class в другой сборке.
  6. private protected — доступ к типу или члену возможен только из его объявляющей сборки из кода в том же class либо в типе, производном от этого class. Этот уровень доступа можно использовать, начиная с версии C# 7.2.

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

public double width; 
public double length; 
public double height;

а можем не указывать, тогда к члену класса по умолчанию будет применен модификатор private.

Классы (class) и структуры (struct), которые объявляются без модификатора, по умолчанию имеют доступ internal. Также,  все классы и структуры, определенные в пространствах имен и не являющиеся вложенными в другие классы, могут иметь только два модификатора доступа — public или internal. Примеры использования различных модификаторов доступа рассмотрим на примере нашего класса, но, прежде, чем мы это сделаем, стоит разобраться с тем, что такое свойство.

Свойства класса

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

Почему не стоит давать прямой доступ к полям класса? Это можно, опять же продемонстрировать на таком простом примере как наш Building: сейчас мы открыли доступ к полям width, length и height извне. При этом, все три поля имеют тип double. Таким образом, зная,что из себя представляет этот тип данных, мы можем без проблем сделать вот так:

Building building = new Building();
building.height = -100.5;

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

В общем случае, свойство определяется в классе следующим образом:

[модификатор_доступа] тип имя_свойства 
{
  get {возвращаемое_значение;}
  set {устанавливаемое_значение;}
}

Здесь модификатор_доступа — это один из шести модификаторов доступа, рассмотренных выше. В большинстве ситуаций используется модификатор public. Далее идёт  тип_данных и имя свойства.  В фигурных скобках содержатся блоки get — для чтения поля,которому соответствует свойство и set — для записи поля.  Например, создадим свойство, с помощью которого мы будем определять высоту здания (представлена только часть кода, демонстрирующая объявление свойства):

class Building
{
    double height;

    public double Height
    {
        get { return height; }
        set
        {
            if (value <= 0)
                throw new Exception("Высота здания не может быть менее или равна 0 метров");
            height = value;
        }
    }
}

На что здесь стоит обратить внимание:

  1. поле height у нас теперь без модификатора доступа (по умолчанию оно стало private)
  2. объявлено публичное свойство Height, причем в блоке set (запись поля) реализована дополнительная логика — проверка того, что задается положительное значение высоты здания. Если будет задана отрицательная или нулевая высота, то будет сгенерировано исключение. Об исключениях мы будем говорить далее в одной из частей учебника.
В нашем примере поле height и свойство Height различаются только написанием первой буквы. В C# принято следующее правило: поля класса пишутся с маленькой буквы, а названия свойств и методов — с большой. В принципе, никто не запрещает назвать поля и свойства по-разному — код будет работать, но считается хорошим тоном именовать поля и свойства одинаковыми словами (желательно, существительными)

Теперь, если попробовать запустить приложение и выполнить вот такое присваивание:

Building building = new Building();
building.Height = -100.5;

то программа выдаст ошибку:

System.Exception HResult=0x80131500 Сообщение = Высота здания не может быть менее или равна 0 метров Источник = FirstClass

Так же, мы можем объявлять свойства доступные только для чтения (у таких свойств будет отсутствовать блок set) и только для записи (у таких свойств отсутствует блок get). Второй вариант свойств (только для записи) встречается крайне редко, а вот свойства только для чтения — довольно часто. Например, мы можем создать свойство «Объем» следующим образом:

class Building
{
    //метод
    private double GetVolume()
    {
        return width * length * height;
    }

    //Свойства
    public double Volume { get { return GetVolume(); } }
}

здесь мы объявили свойство Volume доступное только для чтения. При этом, методу GetVolume() мы установили модификатор private, а свойству — public.

Теперь, познакомившись с тем, что такое свойства в C# перепишем допишем наш класс и создадим необходимые свойства для задания параметров:

internal class Building
{
    public double width;
    public double length;
    double height;


    public double Height 
    {
        get => height;
        set 
        {
            if (value <= 0)
                throw new Exception("Высота здания не может быть равна нулю или быть отрицательной");
            height = value;
        }   
    }

    public double Width
    {
        get => width;
        set
        {
            if (value <= 0)
                throw new Exception("Ширина здания не может быть равна нулю или быть отрицательной");
            width = value;
        }
    }

    public double Length
    {
        get => length;
        set
        {
            if (value <= 0)
                throw new Exception("Высота здания не может быть равна нулю или быть отрицательной");
            length = value;
        }
    }

    public double Volume 
    { 
        get => GetVolume(); 
    }

    private double GetVolume() => width * length * height;

    public Building(): this(10)
    {
        
    }

    public Building(double width): this(width, 20)
    { 
        
    }

    public Building(double width, double length): this(width, length, 30) 
    {

    }

    public Building(double width, double length, double height)
    {
        this.width = width;
        this.length = length;
        this.height = height;
    }
}

Чтобы обратиться к свойству мы должны написать имя объекта, нажать на клавиатуре точку и выбрать интересующее нас свойство:

Сокращенная форма записи свойств в C# (автоматические свойства)

Если в блоках get и set свойства не реализуется никакая дополнительная логика, то допускается сокращенная записи свойства. Например, добавим в наш объект свойство Name (название здания) и применим сокращенную форму записи:

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

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

Выражения для свойств в C#

Если реализация свойства представляет собой одиночное выражение, в качестве метода получения или задания можно использовать выражения для свойств. Например, у нас в классе у свойств Width, Length, Height и Volume в блоке get используется одиночное выражение, более, того, в логическую операцию в блоке set мы также можем «свернуть», используя тернарную операцию, поэтому мы можем упростить код нашего класса, используя выражения C#:

class Building
{
    private double width;
    private double length;
    private double height;

    public Building()
    {
        width = 10;
        length = 10;
        height = 2;
    }

    //метод
    private double GetVolume()
    {
        return width * length * height;
    }

    //Свойства
    public double Height
    {
        get => height;
        set => height = value > 0 ? value : throw new Exception("Высота здания не может быть менее 0 метров");
    }

    public double Width
    {
        get => width;
        set => width = value > 0 ? value : throw new Exception("Ширина здания не может быть менее 0 метров");
    }

    public double Length
    {
        get => length;
        set => length = value > 0 ? value : throw new Exception("Длина здания не может быть менее 0 метров");
    }

    public double Volume => GetVolume();
}

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

Значение свойства по умолчанию в C#

Иногда бывает необходимо инициализировать значение свойства значением, отличным, от значения по умолчанию для типа данных. Например, наше новое свойство Name имеет тип данных string (строка) и по умолчанию будет инициализировано как null, так как строки относятся к ссылочным типам данных. Если мы хотим, чтобы сразу при создании объекта наше свойство получило некоторое значение по умолчанию, то мы можем сделать это следующим образом:

class Building
{
    public string Name { get; set; } = "Неизвестное здание";
}

То есть сразу после закрывающей скобки } для свойства ставится равно и пишется значение свойства по умолчанию.

Блок init

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

internal class Building
{
    public double width;
    public double length;
    double height;

    public string Id { get; init; }
    //здесь прочие свойства, методы и конструкторы класса
 }

Чтобы назначить этому свойству значение мы можем воспользоваться инициализатором:

Building building = new Building { Id = "aaaa-bbbb-3333"};

или конструктором, например, так:

public Building(double width, double length, double height)
{
    Id = "indentificator";
    this.width = width;
    this.length = length;
    this.height = height;
}

Если мы попытаемся изменить значение свойства после создания и инициализации объекта, например, так:

Building building = new Building { Id = "aaaa-bbbb-3333"};
building.Id = "new ident";

то в Visual Studio мы сразу увидим ошибку:

Модификатор required

Модификатор required доступен начиная с C# 11. Применение этого модификатора к свойству или полю класса означает, что оно должно быть инициализировано инициализатором объекта. Например, сделаем свойство Name обязательным:

public required string Name { get; set; }

Теперь мы обязаны определить значение свойства в инициализаторе объекта. Если мы этого не сделаем, то получим ошибку:

Следующий код скомпилируется без ошибок:

using FirstClass;

Building building = new Building { Id = "aaaa-bbbb-3333", Name = "Моё новое здание"};

Итого

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

Подписаться
Уведомить о
guest
0 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии