Кроме обычных методов в языке 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
возвращаем значение поля:
get { return name; }
А в блоке set устанавливаем значение переменной name. Параметр value представляет передаваемое значение, которое передается переменной name.
set { name = value; }
В программе мы можем обращаться к этому свойству, как к обычному полю. Если мы ему присваиваем какое-нибудь значение, то срабатывает блок 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: {name} Age: {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 $"{firstName} {lastName}"; } } 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; }
Начиная с версии 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 = $"{value}@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 (добавлен в 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; }