Свойства

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

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

Синтаксис свойства

Синтаксис свойств является естественным расширением полей. Поле определяет место хранения:

public class Person
{
    public string? FirstName;

    // Omitted for brevity.
}

Определение свойства содержит объявления для методов доступа get и set, которые получают и устанавливают значение этого свойства:

public class Person
{
    public string? FirstName { get; set; }

    // Omitted for brevity.
}

Синтаксис, показанный выше, является синтаксисом автосвойств. Компилятор создает место хранения для поля, поддерживающего свойство. Компилятор также реализует тело методов доступа get и set.

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

public class Person
{
    public string FirstName { get; set; } = string.Empty;

    // Omitted for brevity.
}

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

Вы можете определить хранилище самостоятельно, как показано ниже:

public class Person
{
    public string? FirstName
    {
        get { return _firstName; }
        set { _firstName = value; }
    }
    private string? _firstName;

    // Omitted for brevity.
}

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

public class Person
{
    public string? FirstName
    {
        get => _firstName;
        set => _firstName = value;
    }
    private string? _firstName;

    // Omitted for brevity.
}

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

В примере выше определяется свойство для чтения и записи. Обратите внимание на ключевое слово value в методе доступа set. Метод доступа set всегда имеет один параметр с именем value. Метод доступа get должен возвращать значение, которое можно преобразовать в свойство (string в этом примере).

Это основные сведения о синтаксисе. Существует множество различных вариантов, поддерживающих различные идиомы дизайна. Рассмотрим их, а также соответствующие параметры синтаксиса.

Проверка

Приведенные выше примеры демонстрируют один из простейших вариантов определения свойств: свойство для чтения и записи без проверки. Путем написания нужного кода в методах доступа get и set можно реализовать много разных сценариев.

Можно написать код в методе доступа set, чтобы гарантировать, что значения, представленные свойством, всегда будут допустимыми. Например, предположим, что одно правило для Person класса заключается в том, что имя не может быть пустым или пробелом. Это можно реализовать следующим образом:

public class Person
{
    public string? FirstName
    {
        get => _firstName;
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("First name must not be blank");
            _firstName = value;
        }
    }
    private string? _firstName;

    // Omitted for brevity.
}

Предыдущий пример можно упростить, воспользовавшись выражением throw в рамках проверки метода задания свойства:

public class Person
{
    public string? FirstName
    {
        get => _firstName;
        set => _firstName = (!string.IsNullOrWhiteSpace(value)) ? value : throw new ArgumentException("First name must not be blank");
    }
    private string? _firstName;

    // Omitted for brevity.
}

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

hero.FirstName = "";

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

Этот синтаксис можно расширить для любых компонентов в вашем сценарии. Можно проверить отношения между разными свойствами или соответствие любым внешним условиям. Любые допустимые операторы C# являются допустимыми в методе доступа свойства.

Управление доступом

До этого момента все примеры определения свойств определяли свойства для чтения и записи с помощью открытых методов доступа. Это не единственные операции доступа для свойств. Можно создать свойства, доступные только для чтения, или назначить другой уровень доступа для методов set и get. Предположим, ваш класс Person должен допускать изменение значения свойства FirstName только из других методов этого класса. Вы можете предоставить методу доступа set уровень доступа private, а не public:

public class Person
{
    public string? FirstName { get; private set; }

    // Omitted for brevity.
}

Теперь к свойству FirstName можно получать доступ из любого кода, но назначить его можно только из другого кода в классе Person.

Вы можете добавить любой ограничивающий модификатор доступа для методов доступа set или get. Модификатор доступа, установленный для отдельного метода доступа, должен задавать более строгие ограничения, чем модификатор доступа для определения свойства. Приведенный выше пример допустим, так как свойство FirstName является открытым (public) а метод доступа set — закрытым (private). Невозможно объявить private свойство с методом public доступа. Свойство также можно объявить как protected, internal, protected internal или даже private.

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

Только для чтения

Можно также ограничить изменения свойством, чтобы его можно было задать только в конструкторе. Внести соответствующие изменения в класс Person можно следующим образом:

public class Person
{
    public Person(string firstName) => FirstName = firstName;

    public string FirstName { get; }

    // Omitted for brevity.
}

Только для инициализации

В предыдущем примере требуется, чтобы вызывающие пользователи использовали конструктор, включающий FirstName параметр. Вызывающие не могут использовать инициализаторы объектов для назначения значения свойству. Для поддержки инициализаторов можно сделать set метод доступа методом init доступа, как показано в следующем коде:

public class Person
{
    public Person() { }
    public Person(string firstName) => FirstName = firstName;

    public string? FirstName { get; init; }

    // Omitted for brevity.
}

В предыдущем примере вызывающий объект позволяет создавать конструктор Person по умолчанию, даже если этот код не задает FirstName свойство. Начиная с C# 11, для задания этого свойства можно требовать вызывающих абонентов:

public class Person
{
    public Person() { }

    [SetsRequiredMembers]
    public Person(string firstName) => FirstName = firstName;

    public required string FirstName { get; init; }

    // Omitted for brevity.
}

Предыдущий код делает два дополнения к классу Person . Во-первых FirstName , объявление свойства включает required модификатор. Это означает, что любой код, создающий новое Person свойство, должен задать это свойство. Во-вторых, конструктор, принимаюющий firstName параметр, имеет System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute атрибут. Этот атрибут сообщает компилятору, что этот конструктор задает всеrequired элементы.

Внимание

Не путайте required с ненулевой. Допустимо задать required для свойства null значение или default. Если тип не допускает значение NULL, например string в этих примерах, компилятор выдает предупреждение.

Вызывающие элементы должны использовать конструктор с SetsRequiredMembers или задать FirstName свойство с помощью инициализатора объектов, как показано в следующем коде:

var person = new VersionNinePoint2.Person("John");
person = new VersionNinePoint2.Person{ FirstName = "John"};
// Error CS9035: Required member `Person.FirstName` must be set:
//person = new VersionNinePoint2.Person();

Вычисляемые свойства

Свойству не нужно просто возвращать значение поля-члена. Можно создать свойства, возвращающие вычисляемое значение. Расширим объект Person так, чтобы он возвращал полное имя, вычисляемое путем объединения имени и фамилии:

public class Person
{
    public string? FirstName { get; set; }

    public string? LastName { get; set; }

    public string FullName { get { return $"{FirstName} {LastName}"; } }
}

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

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

public class Person
{
    public string? FirstName { get; set; }

    public string? LastName { get; set; }

    public string FullName => $"{FirstName} {LastName}";
}

Члены, воплощающие выражения, используют синтаксис лямбда-выражений для определения метода, который содержит одно выражение. Здесь это выражение возвращает полное имя объекта person.

Свойства с вычислением в кэше

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

public class Person
{
    public string? FirstName { get; set; }

    public string? LastName { get; set; }

    private string? _fullName;
    public string FullName
    {
        get
        {
            if (_fullName is null)
                _fullName = $"{FirstName} {LastName}";
            return _fullName;
        }
    }
}

Обратите внимание, что приведенный выше код содержит ошибку. Если код изменяет значение свойств FirstName или LastName, ранее вычисленное поле fullName является недопустимым. Вам потребуется изменить методы доступа set свойств FirstName и LastName, чтобы вычислить поле fullName еще раз:

public class Person
{
    private string? _firstName;
    public string? FirstName
    {
        get => _firstName;
        set
        {
            _firstName = value;
            _fullName = null;
        }
    }

    private string? _lastName;
    public string? LastName
    {
        get => _lastName;
        set
        {
            _lastName = value;
            _fullName = null;
        }
    }

    private string? _fullName;
    public string FullName
    {
        get
        {
            if (_fullName is null)
                _fullName = $"{FirstName} {LastName}";
            return _fullName;
        }
    }
}

Эта окончательная версия вычисляет свойство FullName только при необходимости. Если ранее вычисленная версия является допустимой, используется она. Если другое изменение состояния делает ранее вычисленную версию недействительной, она будет пересчитана. Разработчики, использующие этот класс, не должны знать подробности реализации. Ни одно из этих внутренних изменений не влияет на использование объекта person. Это главная причина для использования свойств для предоставления доступа к членам данных объекта.

Присоединение атрибутов к автоматически реализуемым свойствам

Атрибуты поля можно присоединить к созданному компилятору поле резервного копирования в автоматически реализованных свойствах. Например, рассмотрим изменение класса Person, который добавляет уникальное целочисленное свойство Id. Свойство записывается Id с помощью автоматического реализованного свойства, но проект не вызывает сохранение Id свойства. NonSerializedAttribute можно прикреплять только к полям, а не свойствам. Можно прикрепить NonSerializedAttribute к резервному полю для свойства Id с помощью описателя field: в атрибуте, как показано в следующем примере:

public class Person
{
    public string? FirstName { get; set; }

    public string? LastName { get; set; }

    [field:NonSerialized]
    public int Id { get; set; }

    public string FullName => $"{FirstName} {LastName}";
}

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

Реализация INotifyPropertyChanged

Последним сценарием, где необходимо написать код в методе доступа к свойству, является поддержка интерфейса INotifyPropertyChanged, используемого для уведомления клиентов привязки данных об изменении значения. При изменении значения свойства объект вызывает событие INotifyPropertyChanged.PropertyChanged, указывающее на изменение. Библиотеки привязки данных, в свою очередь, изменяют отображаемые элементы на основе этого изменения. В следующем примере кода показано, как можно реализовать свойства INotifyPropertyChanged для FirstName этого класса person.

public class Person : INotifyPropertyChanged
{
    public string? FirstName
    {
        get => _firstName;
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("First name must not be blank");
            if (value != _firstName)
            {
                _firstName = value;
                PropertyChanged?.Invoke(this,
                    new PropertyChangedEventArgs(nameof(FirstName)));
            }
        }
    }
    private string? _firstName;

    public event PropertyChangedEventHandler? PropertyChanged;
}

Оператор ?. называется оператором объединения со значением NULL. Он проверяет наличие пустой ссылки перед вычислением правой стороны оператора. В конечном итоге, если нет подписчиков на событие PropertyChanged, код для вызова события не выполняется. В этом случае без такой проверки будет создано исключение NullReferenceException. Дополнительные сведения см. в разделе events. В этом примере также используется новый оператор nameof для преобразования символа имени свойства в текстовое представление. Использование nameof может уменьшить ошибки, в которых вы неправильно ввели имя свойства.

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

Подведем итоги

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