Для чего нужны свойства?
Есть библиотеки, для работами с которыми ваш класс должен содержать свойство, например EntityFramework.
22 июн 2018 в 8:01
6 ответов 6
Сортировка: Сброс на вариант по умолчанию
Смотрите, какие есть преимущества у свойства перед полем.
Если ваше свойство определено так:
public int A
— то непосредственных выгод, конечно, нету. Но выгоды придут позже.
-
Вы можете навесить свою логику на запись и считывание значения. Применений может быть море. Например, вы хотите посчитать, сколько раз считывалось значение:
private int a; private int readcount_a = 0; public int A < get < readcount_a++; return a; >set < a = value; >>
Вы можете сделать триггер на изменение поля:
class Data : INotifyPropertyChanged < public event PropertyChangedEventHandler PropertyChanged; private int a; public int A < get < return a; >set < if (a == value) return; a = value; RaisePropertyChanged(); >> private void RaisePropertyChanged([CallerMemberName] string propertyName = null) < if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); >>
Вы можете залогировать все изменения поля:
public int A < get < return a; >set < Trace.TraceInformation("Changing value of a from to ", a, value); a = value; > >
Вы можете навесить проверку значения на корректность при записи, или ленивую инициализацию при чтении.
class Data < private string a = null; public string A < get < return a ?? (a = LazyComputeInitialA()); >set < if (value == null) throw new ArgumentException(nameof(A) + " cannot be null", nameof(A)); a = value; >> >
В конце-концов, вы можете не расходовать память на значение, если в большинстве случаев оно одинаковое (как это сделано у DependencyProperty ):
class Data < static DictionaryaValues = new Dictionary(); public int A < get < int result; if (aValues.TryGetValue(this, out result)) return result; else return -1; // default value >set < aValues[this] = value; >> >
[Если вы захотите воспользоваться этим кодом в своём проекте, применяйте улучшенный вариант:
static readonly ConditionalWeakTable> aValues = new ConditionalWeakTable>(); public int A < get =>aValues.TryGetValue(this, out var tmp) ? tmp.Value : -1; // default value set => aValues.GetOrCreateValue(this).Value = value; >
public int A
public int A < get < return b + c; >>
(впрочем, такое можно сделать и при наличии сеттера). Например, вы можете предоставлять данные в разных форматах:
public double Radians < get; set; >public double Degrees < get < return Radians * 180.0 / Math.PI; >set < Radians = value * Math.PI / 180.0; >>
interface ISupportsA < int A < get; >> class Data : ISupportsA < public int A < get; set; >>
Но не излишни ли свойства в языке? Кажется, что вместо свойства можно определить просто две функции:
class Data < private int a; public int GetA() < return a; >public int SetA(int a) < this.a = a; >>
Ответ на это таков.
Во-первых, одно свойство вместо двух функций представляет собой логическую группу. В хорошем языке вы говорите то, что думаете. На самом деле вы предоставляете пользователю «переменную» A с дополнительной, часто невидимой снаружи семантикой. Значит, и выглядеть она должна как одна переменная, чтобы пользователи класса думали в тех же терминах, что и вы.
Во-вторых, это читаемость текста. Сравните код со свойствами:
player.car.speed++;
getPlayer().getCar().setSpeed(getPlayer().getCar().getSpeed()+1);
Что легче воспринимается?
Справедливости ради, нужно отметить и недостатки свойств по сравнению с полями.
- Свойства нельзя использовать как out / ref -параметр, поля можно.
- Доступ к полям очень быстр, а вот доступ к свойствам может быть медленным, если код внутри геттера/сеттера медленный. Однако, медленный сеттер или (ещё хуже) геттер считаются порочной практикой, их рекомендуется избегать, чтобы не разрушать ментальную модель «переменная с небольшим довеском».
- Доступ к свойству может выбросить исключение или зависнуть, в то время как более простые поля ведут себя крайне просто. Конечно, правильно написанное свойство не будет зависать, а исключения я бы порекомендовал выбрасывать только в случаях, когда пользователь класса нарушил контракт на доступ к полю.
Ещё одно тонкое отличие свойства от поля состоит в том, что геттер возвращает вам копию значения, в то время как при работе с полем вы получаете доступ непосредственно к переменной. При работе со полями reference-типов (то есть, тип которых есть класс) практической разницы нет, так как работа с объектом по копии ссылки не отличается от работы по оригиналу ссылки. Разница, однако, есть, когда поле является изменяемой структурой (хотя, сами по себе изменяемые структуры — плохая идея). Пример случая, когда это важно, во фрагменте кода ниже.
Часто считают, что можно для начала объявить данные как поле, а потом, при необходимости, «превратить» его в свойство. Это лишь отчасти верно: при этом вы теряете бинарную совместимость. Код, который использовал ваш класс, должен быть перекомпилирован, так как на уровне скомпилированного кода обращение к полю и к свойству — не одно и то же. Кроме того, смысл кода может поменяться, приводя к тонким ошибками. Пример из статьи по ссылке выше:
using System; struct MutableStruct < public int Value < get; set; >public void SetValue(int newValue) < Value = newValue; >> class MutableStructHolder < public MutableStruct Field; public MutableStruct Property < get; set; >> class Test < static void Main(string[] args) < MutableStructHolder holder = new MutableStructHolder(); // Меняет значение holder.Field holder.Field.SetValue(10); // Получает *копию* holder.Property и изменяет её holder.Property.SetValue(10); Console.WriteLine(holder.Field.Value); // 10 Console.WriteLine(holder.Property.Value); // 0 >>
Кстати, согласно Википедии, геттер и сеттер правильно называть акцессор и мутатор соответственно. Вы об этом знали? [Хотя, MSDN пишет просто «методы доступа».]
Свойства
Свойства являются привилегированными компонентами 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 с не допускающей значения NULL. Допустимо задать свойству 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 $""; > > >
В примере выше используется функция интерполяции строк для создания форматированной строки для полного имени.
Можно также использовать члены, воплощающие выражения, которые обеспечивают более краткий способ создания вычисляемого свойства FullName :
public class Person < public string? FirstName < get; set; >public string? LastName < get; set; >public string FullName => $" "; >
Члены, воплощающие выражения, используют синтаксис лямбда-выражений для определения метода, который содержит одно выражение. Здесь это выражение возвращает полное имя объекта 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 = $""; 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 = $""; 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 => $" "; >
Этот способ подходит для любого атрибута, который вы прикрепляете к резервному полю в автоматически реализуемом свойстве.
Реализация 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#. Вы можете предоставлять разные уровни доступа, выполнять проверки, отложенное вычисление или любые другие требования, необходимые в вашем сценарии.
Совместная работа с нами на GitHub
Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.
В чем отличие свойств и полей
Кроме обычных методов в языке C# предусмотрены специальные методы доступа, которые называют свойства . Они обеспечивают простой доступ к полям классов и структур, узнать их значение или выполнить их установку.
Определение свойств
Стандартное описание свойства имеет следующий синтаксис:
[модификаторы] тип_свойства название_свойства < get < действия, выполняемые при получении значения свойства>set < действия, выполняемые при установке значения свойства>>
Вначале определения свойства могут идти различные модификаторы, в частности, модификаторы доступа. Затем указывается тип свойства, после которого идет название свойства. Полное определение свойства содержит два блока: get и set .
В блоке get выполняются действия по получению значения свойства. В этом блоке с помощью оператора return возвращаем некоторое значение.
В блоке set устанавливается значение свойства. В этом блоке с помощью параметра value мы можем получить значение, которое передано свойству.
Блоки get и set еще называются акссесорами или методами доступа (к значению свойства), а также геттером и сеттером.
Person person = new Person(); // Устанавливаем свойство - срабатывает блок Set // значение "Tom" и есть передаваемое в свойство value person.Name = "Tom"; // Получаем значение свойства и присваиваем его переменной - срабатывает блок Get string personName = person.Name; Console.WriteLine(personName); // Tom class Person < private string name = "Undefined"; public string Name < get < return name; // возвращаем значение свойства >set < name = value; // устанавливаем новое значение свойства >> >
Здесь в классе Person определено приватное поле name , которая хранит имя пользователя, и есть общедоступное свойство Name . Хотя они имеют практически одинаковое название за исключением регистра, но это не более чем стиль, названия у них могут быть произвольные и не обязательно должны совпадать.
Через это свойство мы можем управлять доступом к переменной name . В свойстве в блоке get возвращаем значение поля:
А в блоке set устанавливаем значение переменной name. Параметр value представляет передаваемое значение, которое передается переменной name.
В программе мы можем обращаться к этому свойству, как к обычному полю. Если мы ему присваиваем какое-нибудь значение, то срабатывает блок set , а передаваемое значение передается в параметр value :
person.Name = "Tom";
Если мы получаем значение свойства, то срабатывает блок get , который по сути возвращает значение переменной name:
string personName = person.Name;
То есть по сути свойство Name ничего не хранит, оно выступает в роли посредника между внешним кодом и переменной name.
Возможно, может возникнуть вопрос, зачем нужны свойства, если мы можем в данной ситуации обходиться обычными полями класса? Но свойства позволяют вложить дополнительную логику, которая может быть необходима при установке или получении значения. Например, нам надо установить проверку по возрасту:
Person person = new Person(); Console.WriteLine(person.Age); // 1 // изменяем значение свойства person.Age = 37; Console.WriteLine(person.Age); // 37 // пробуем передать недопустимое значение person.Age = -23; // Возраст должен быть в диапазоне от 1 до 120 Console.WriteLine(person.Age); // 37 - возраст не изменился class Person < int age = 1; public int Age < set < if (value < 1 || value >120) Console.WriteLine("Возраст должен быть в диапазоне от 1 до 120"); else age = value; > get < return age; >> >
В данном случае переменная age хранит возраст пользователя. Напрямую мы не можем обратиться к этой переменной — только через свойство Age. Причем в блоке set мы устанавливаем значение, если оно соответствует некоторому разумному диапазону. Поэтому при передаче свойству Age значения, которое не входит в этот диапазон, значение переменной не будет изменяться:
person.Age = -23;Консольный вывод программы:
1 37 Возраст должен быть в диапазоне от 1 до 120 37
Таким образом, свойство позволяет опосредовать и контролировать доступ к данным объекта.
Свойства только для чтения и записи
Блоки set и get не обязательно одновременно должны присутствовать в свойстве. Если свойство определяет только блок get , то такое свойство доступно только для чтения — мы можем получить его значение, но не установить.
И, наоборот, если свойство имеет только блок set , тогда это свойство доступно только для записи — можно только установить значение, но нельзя получить:
Person person = new Person(); // свойство для чтения - можно получить значение Console.WriteLine(person.Name); // Tom // но нельзя установить // person.Name = "Bob"; // ! Ошибка // свойство для записи - можно устновить значение person.Age = 37; // но нелзя получить // Console.WriteLine(person.Age); // ! Ошибка person.Print(); class Person < string name = "Tom"; int age = 1; // свойство только для записи public int Age < set < age = value; >> // свойство только для чтения public string Name < get < return name; >> public void Print()=> Console.WriteLine($"Name: Age: "); >
Здесь свойство Name доступно только для чтения, поскольку оно имеет только блок get :
public string Name < get < return name; >>
Мы можем получить его значение, но НЕ можем установить:
Console.WriteLine(person.Name); // получить можно person.Name = "Bob"; // ! Ошибка - установить нельзя
А свойство Age, наоборот, доступно только для записи, поскольку оно имеет только блок set :
public int Age < set < age = value; >>
Можно установить его значение, но нельзя получить:
person.Age = 37; // установить можно Console.WriteLine(person.Age); // ! Ошибка - получить значение нельзя
Вычисляемые свойства
Свойства необязательно связаны с определенной переменной. Они могут вычисляться на основе различных выражений
Person tom = new("Tom", "Smith"); Console.WriteLine(tom.Name); // Tom Smith class Person < string firstName; string lastName; public string Name < get < return $""; > > public Person(string firstName, string lastName) < this.firstName = firstName; this.lastName = lastName; >>
В данном случае класс Person имеет свойство Name, которое доступно только для чтения и которое возвращает общее значение на основе значений переменных firstName и lastName.
Модификаторы доступа
Мы можем применять модификаторы доступа не только ко всему свойству, но и к отдельным блокам get и set:
Person tom = new("Tom"); // Ошибка - set объявлен с модификатором private //tom.Name = "Bob"; Console.WriteLine(tom.Name); // Tom class Person < string name = ""; public string Name < get < return name; >private set < name = value; >> public Person(string name) => Name = name; >
Теперь закрытый блок set мы сможем использовать только в данном классе — в его методах, свойствах, конструкторе, но никак не в другом классе:
При использовании модификаторов в свойствах следует учитывать ряд ограничений:
- Модификатор для блока set или get можно установить, если свойство имеет оба блока (и set, и get)
- Только один блок set или get может иметь модификатор доступа, но не оба сразу
- Модификатор доступа блока set или get должен быть более ограничивающим, чем модификатор доступа свойства. Например, если свойство имеет модификатор public, то блок set/get может иметь только модификаторы protected internal, internal, protected, private protected и private
Автоматические свойства
Свойства управляют доступом к полям класса. Однако что, если у нас с десяток и более полей, то определять каждое поле и писать для него однотипное свойство было бы утомительно. Поэтому в .NET были добавлены автоматические свойства. Они имеют сокращенное объявление:
class Person < public string Name < get; set; >public int Age < get; set; >public Person(string name, int age) < Name = name; Age = age; >>
На самом деле тут также создаются поля для свойств, только их создает не программист в коде, а компилятор автоматически генерирует при компиляции.
В чем преимущество автосвойств, если по сути они просто обращаются к автоматически создаваемой переменной, почему бы напрямую не обратиться к переменной без автосвойств? Дело в том, что в любой момент времени при необходимости мы можем развернуть автосвойство в обычное свойство, добавить в него какую-то определенную логику.
Стоит учитывать, что нельзя создать автоматическое свойство только для записи, как в случае со стандартными свойствами.
Автосвойствам можно присвоить значения по умолчанию (инициализация автосвойств):
Person tom = new(); Console.WriteLine(tom.Name); // Tom Console.WriteLine(tom.Age); // 37 class Person < public string Name < get; set; >= "Tom"; public int Age < get; set; >= 37; >
И если мы не укажем для объекта Person значения свойств Name и Age, то будут действовать значения по умолчанию.
Автосвойства также могут иметь модификаторы доступа:
class Person < public string Name < private set; get;>public Person(string name) => Name = name; >
Мы можем убрать блок set и сделать автосвойство доступным только для чтения. В этом случае для хранения значения этого свойства для него неявно будет создаваться поле с модификатором readonly, поэтому следует учитывать, что подобные get-свойства можно установить либо из конструктора класса, как в примере выше, либо при инициализации свойства:
class Person < // через инициализацию свойства public string Name < get; >= "Tom"; // через конструктор public Person(string name) => Name = name; >
Блок init
Начиная с версии C# 9.0 сеттеры в свойствах могут определяться с помощью оператора init (от слова «инициализация» — это есть блок init призван инициализировать свойство). Для установки значений свойств с init можно использовать только инициализатор, либо конструктор, либо при объявлении указать для него значение. После инициализации значений подобных свойств их значения изменить нельзя — они доступны только для чтения. В этом плане init-свойства сближаются со свойствами для чтения. Разница состоит в том, что init-свойства мы также можем установить в инициализаторе (свойства для чтения установить в инициализаторе нельзя). Например:
Person person = new(); //person.Name = "Bob"; //! Ошибка - после инициализации изменить значение нельзя Console.WriteLine(person.Name); // Undefined public class Person < public string Name < get; init; >= "Undefined"; >
В данном случае класс Person для свойства Name вместо сеттера использует оператор init . В итоге на строке
Person person = new();
предполагается создание объекта с инициализацией всех его свойств. В данном случае свойство Name получит в качестве значения строку «Undefined». Однако поскольку инициализация свойства уже произошла, то на строке
person.Name = "Bob"; // Ошибка
мы получим ошибку.
Как можно установить подобное свойство? Выше продемонстрирован один из способов — установка значения при определении свойства. Второй способ — через конструктор:
Person person = new("Tom"); Console.WriteLine(person.Name); // Tom public class Person < public Person(string name) =>Name = name; public string Name < get; init; >>
Третий способ — через инициализатор:
Person person = new() < Name = "Bob">; Console.WriteLine(person.Name); // Bob public class Person < public string Name < get; init; >= ""; >
В принцпе есть еще четвертый способ — установка через другое свойство с модификатором init :
var person = new Person() < Name = "Sam" >; Console.WriteLine(person.Name); // Sam Console.WriteLine(person.Email); // Sam@gmail.com public class Person < string name = ""; public string Name < get < return name; >init < name = value; Email = $"@gmail.com"; > > public string Email < get; init; >= ""; >
В данном случае свойство Name управляет полем для чтения name . Благодаря этому перед установкой значения свойства мы можем произвести некоторую предобработку. Кроме того, в выражении init устанавливается другое init-свойство — Email, которое для установки значения использует значение свойства Name — из имени получаем значение для электронного адреса.
Причем если при объявлении свойства указано значение, то в конструкторе мы можем его изменить. Значение, установленное в конструкторе, можно изменить в инициализаторе. Однако дальше процесс инициализации заканчивается. И значение не может быть изменено.
Сокращенная запись свойств
Как и методы, мы можем сокращать определения свойств. Поскольку блоки get и set представляют специальные методы, то как и обычные методы, если они содержат одну инструкцию, то мы их можем сократить с помощью оператора => :
class Person < string name; public string Name < get =>name; set => name = value; > >
Также можно сокращать все свойство в целом:
class Person < string name; // эквивалентно public string Name < get < return name; >> public string Name => name; >
модификатор required
Модификатор required (добавлен в C# 11) указывает, что поле или свойства с этим модификатором обязательно должны быть инициализированы. Например, в следующем примере мы получим ошибку:
Person tom = new Person(); // ошибка - свойства Name и Age не инициализированы public class Person < public required string Name < get; set; >public required int Age < get; set; >>
Здесь свойства Name и Age отмечены как обязательные для инициализации с помощью модификатора required , поэтому необходимо использовать инициализатор для их инициализации:
Person tom = new Person < Name = "Tom", Age = 38 >; // ошибки нет
Причем не важно, устанавливаем эти свойства в конструкторе или инициализируем при определении, все равно надо использовать инициализатор для установки их значений. Например, в следующем примере мы получим ошибку:
Person bob = new Person("Bob"); // ошибка - свойства Name и Age все равно надо установить в инициализаторе public class Person < public Person(string name) < Name = name; >public required string Name < get; set; >public required int Age < get; set; >= 22; >
Свойства (Руководство по программированию в C#)
Свойство — это член, предоставляющий гибкий механизм для чтения, записи или вычисления значения частного поля. Свойства можно использовать, как если бы они являются общедоступными элементами данных, но они являются специальными методами, называемыми методами доступа. Эта функция позволяет легко получать доступ к данным и по-прежнему способствовать обеспечению безопасности и гибкости методов.
Общие сведения о свойствах
- Свойства позволяют классу предоставлять общий способ получения и задания значений, скрывая при этом код реализации или проверки.
- Метод доступа get используется для возврата значения свойства, а метод доступа set — для присвоения нового значения. Метод доступа к свойству init используется для назначения нового значения только во время построения объекта. Эти методы доступа могут иметь различные уровни доступа. Дополнительные сведения см. в разделе Доступность методов доступа.
- Ключевое слово value используется для определения значения, присваиваемого методом доступа set или init .
- Свойства могут быть доступны для чтения и записи (они имеют оба метода доступа — get и set ), только для чтения (они имеют метод доступа get , но не имеют метода доступа set ) или только для записи (они имеют метод доступа set , но не имеют метода доступа get ). Свойства только для записи встречаются редко и чаще всего используются для ограничения доступа к конфиденциальным данным.
- Простые свойства, не требующие пользовательского кода метода доступа, можно реализовать как определения текста выражений или как автоматически реализуемые свойства.
Свойства с резервными полями
Одной из базовых схем реализации свойств является использование закрытого резервного поля для установки и извлечения значения свойства. Метод доступа get возвращает значение закрытого поля, а метод доступа set может выполнять определенные проверки данных до присвоения значению закрытого поля. Оба метода доступа также могут выполнять некоторые преобразования или вычисления данных перед его сохранением или возвратом.
Это показано в следующем примере. В этом примере класс TimePeriod представляет интервал времени. На внутреннем уровне класс сохраняет интервал времени в секундах в закрытом поле с именем _seconds . Свойство чтения и записи с именем Hours позволяет клиенту указывать временной интервал в часах. Методы доступа get и set выполняют необходимое преобразование между часами и секундами. Кроме того, метод доступа set проверяет данные и создает ArgumentOutOfRangeException, если указано недопустимое количество часов.
public class TimePeriod < private double _seconds; public double Hours < get < return _seconds / 3600; >set < if (value < 0 || value >24) throw new ArgumentOutOfRangeException(nameof(value), "The valid range is between 0 and 24."); _seconds = value * 3600; > > >
Чтобы получить и задать значение, можно получить и задать свойства, как показано в следующем примере:
TimePeriod t = new TimePeriod(); // The property assignment causes the 'set' accessor to be called. t.Hours = 24; // Retrieving the property causes the 'get' accessor to be called. Console.WriteLine($"Time in hours: "); // The example displays the following output: // Time in hours: 24
Определения текста выражений
Как правило, методы доступа к свойствам состоят из однострочных операторов, которые просто назначают или возвращают результат выражения. Эти свойства можно реализовать как члены, воплощающие выражение. Определения текста выражений состоят из символа => , за которым идет выражение, назначаемое свойству или извлекаемое из него.
Свойства, доступные только для чтения, могут реализовать get метод доступа в качестве элемента, на основе выражения. В этом случае не используется ни ключевое слово метода доступа get , ни ключевое слово return . В следующем примере показана реализация свойства только для чтения Name в виде члена, воплощающего выражение.
public class Person < private string _firstName; private string _lastName; public Person(string first, string last) < _firstName = first; _lastName = last; >public string Name => $" "; >
И get метод set доступа могут быть реализованы как члены с выражением. В этом случае необходимо указывать ключевые слова get и set . В следующем примере показано использование определений текста выражений для обоих методов доступа. Ключевое слово return не используется с методом get доступа.
public class SaleItem < string _name; decimal _cost; public SaleItem(string name, decimal cost) < _name = name; _cost = cost; >public string Name < get =>_name; set => _name = value; > public decimal Price < get =>_cost; set => _cost = value; > >
Автоматически реализуемые свойства
В некоторых случаях свойства get и set методы доступа просто назначают значение или извлекают значение из резервного поля, не включая дополнительную логику. С помощью автоматически реализуемых свойств можно упростить код, в то время как компилятор C# будет прозрачно предоставлять вам резервное поле.
Если у свойства есть методы доступа get и set (или get и init ), они оба должны быть автоматически реализованы. Автоматически реализуемое свойство определяется с помощью ключевых слов get и set без указания какой-либо реализации. Следующий пример аналогичен предыдущему, за исключением того, что Name и Price являются автоматически реализуемыми свойствами. В этом примере также удаляется параметризованный конструктор, что позволяет инициализировать объекты SaleItem , вызывая конструктор без параметров и инициализатор объекта.
public class SaleItem < public string Name < get; set; >public decimal Price < get; set; >>
Автоматически реализованные свойства могут объявлять различные специальные возможности для get методов доступа и set доступа. Обычно вы объявляете общедоступный get метод доступа и частный set метод доступа. Дополнительные сведения см. в статье об ограничении специальных возможностей доступа.
Обязательные свойства
Начиная с C# 11, можно добавить required член, чтобы принудительно инициализировать любое свойство или поле клиентского кода:
public class SaleItem < public required string Name < get; set; >public required decimal Price < get; set; >>
Чтобы создать SaleItem объект, необходимо задать как свойства, так Name и Price свойства с помощью инициализаторов объектов, как показано в следующем коде:
var item = new SaleItem < Name = "Shoes", Price = 19.95m >; Console.WriteLine($": sells for ");
См. также
- Использование свойств
- Свойства интерфейса
- Сравнение свойств и индексаторов
- Ограничение доступности методов доступа
- Автоматически реализуемые свойства
Спецификация языка C#
Дополнительные сведения см. в разделе Свойства в Спецификации языка C#. Спецификация языка является предписывающим источником информации о синтаксисе и использовании языка C#.
См. также
- Индексаторы
- Ключевое слово get
- Ключевое слово set
Совместная работа с нами на GitHub
Источник этого содержимого можно найти на GitHub, где также можно создавать и просматривать проблемы и запросы на вытягивание. Дополнительные сведения см. в нашем руководстве для участников.