Свойства get и set в C#
Свойство в C# - это член класса, который выглядит как поле, но работает через методы чтения get и записи set. Оно позволяет проверять значения, вычислять результат на лету и ограничивать доступ. Ниже разобраны обычные свойства, автосвойства, init, вычисляемые свойства, валидация и модификатор required.

Содержание
- Свойство и поле: в чём разница
- Полная форма свойства с get и set
- Автоматические свойства
- Разный доступ у get и set
- Init: значение только при создании
- Вычисляемые свойства
- Валидация значений в set
- Required: обязательные свойства
- Свойства в интерфейсах и перечисление примеров применения
- Типичные ошибки
- Частые вопросы
Свойство и поле: в чём разница
Поле - это переменная внутри класса, которая хранит данные напрямую. Если сделать поле открытым, любой код сможет записать в него что угодно, и класс не сможет этого ни проверить, ни запретить. Свойство решает проблему: снаружи оно используется как поле, а внутри имеет код доступа. Класс сам решает, что вернуть и какие значения принять.
public class Person
{
public string Name = ""; // открытое поле: контроля нет
}
var p = new Person();
p.Name = ""; // никто не помешает записать пустое имя
Кроме контроля значений, у свойств есть и другие преимущества. Их можно объявлять в интерфейсах, они поддерживаются привязкой данных в WPF, ASP.NET и других фреймворках, а позже в них можно добавить логику без изменения кода, который их использует. Связь с закрытыми данными описана в статье про инкапсуляцию.
Полная форма свойства с get и set
Классическая запись использует закрытое поле для хранения значения, которое называют резервным (backing field). Блок get возвращает значение, блок set принимает его через неявный параметр value.
public class Person
{
private string _name = "";
public string Name
{
get { return _name; }
set { _name = value; }
}
}
var p = new Person();
p.Name = "Анна";
Console.WriteLine(p.Name); // Анна
Если логика простая, тело можно записать короче через выражение со стрелкой: get => _name; set => _name = value;. Для свойства, которое только возвращает значение, достаточно одной стрелки после имени.
public class Box
{
private int _size;
public int Size
{
get => _size;
set => _size = value;
}
}
Автоматические свойства
Когда никакой логики в get и set нет, используют автосвойство: компилятор сам создаёт скрытое резервное поле. Запись занимает одну строку, а позже её можно превратить в полное свойство, не меняя вызывающий код.
public class Product
{
public string Title { get; set; } = "Без названия"; // значение по умолчанию
public decimal Price { get; set; }
public int Quantity { get; set; } = 1;
}
var item = new Product { Title = "Ручка", Price = 25.5m };
Console.WriteLine($"{item.Title}: {item.Price} x {item.Quantity}");
Начальное значение задают сразу после объявления. Если оно не указано, свойство получает значение по умолчанию для своего типа: ноль для чисел, false для bool и null для ссылочных типов. При включённых nullable-типах компилятор предупредит о строке без значения, поэтому ей присваивают "" или используют required.
Разный доступ у get и set
Часто нужно, чтобы значение читали все, а менять могли только внутри класса. Для этого у set указывают более строгий модификатор, например private. Как работают уровни доступа, разобрано в статье про модификаторы доступа.
public class Order
{
public int Id { get; } // только чтение: задаётся в конструкторе
public string Status { get; private set; } = "Новый";
public Order(int id) => Id = id;
public void Complete() => Status = "Выполнен";
}
var order = new Order(15);
order.Complete();
Console.WriteLine($"{order.Id}: {order.Status}"); // 15: Выполнен
// order.Status = "Отменён"; // ошибка: set закрыт
Свойство без set называют свойством только для чтения: значение присваивается при объявлении или в конструкторе. Свойство, у которого set закрыт модификатором private, можно менять внутри класса в любых методах, но не снаружи.
Init: значение только при создании
Начиная с C# 9 вместо set можно использовать init. Такое свойство разрешено присваивать только в инициализаторе объекта и в конструкторе, после чего оно становится неизменяемым. Это удобно для моделей данных, которые не должны меняться после создания.
public class Point
{
public int X { get; init; }
public int Y { get; init; }
}
var p = new Point { X = 3, Y = 4 };
Console.WriteLine($"({p.X}, {p.Y})");
// p.X = 10; // ошибка компиляции: init-свойство нельзя менять после создания
var moved = new Point { X = p.X + 1, Y = p.Y };
В отличие от свойства только для чтения без set, init позволяет использовать инициализатор объекта, то есть записывать значения в фигурных скобках при вызове new. Для создания изменённых копий неизменяемых объектов удобны record и выражение with, но это уже отдельная тема.
Вычисляемые свойства
Свойство не обязано хранить значение: оно может вычислять его при каждом обращении. Так делают, когда результат зависит от других данных объекта, и хранить его отдельно не нужно, чтобы он не рассинхронизировался.
public class Rectangle
{
public double Width { get; set; }
public double Height { get; set; }
public double Area => Width * Height;
public double Perimeter => 2 * (Width + Height);
public bool IsSquare => Width == Height;
}
var r = new Rectangle { Width = 4, Height = 5 };
Console.WriteLine(r.Area); // 20
r.Width = 10;
Console.WriteLine(r.Area); // 50, пересчитано автоматически
Не стоит помещать в get тяжёлые операции: обращаться к свойству привыкли как к быстрой операции, и скрытый запрос к базе данных или чтение файла станет неприятной неожиданностью. Для затратных действий лучше написать метод. Подробнее про устройство методов написано в статье про методы в C#.
Валидация значений в set
Одно из главных применений set - проверка входных данных. Если значение недопустимо, можно выбросить исключение или привести его к допустимому. Тогда класс всегда остаётся в корректном состоянии.
public class Employee
{
private string _name = "";
private int _age;
private decimal _salary;
public string Name
{
get => _name;
set
{
if (string.IsNullOrWhiteSpace(value))
throw new ArgumentException("Имя не может быть пустым");
_name = value.Trim();
}
}
public int Age
{
get => _age;
set
{
if (value < 16 || value > 100)
throw new ArgumentOutOfRangeException(nameof(value), "Возраст вне допустимого диапазона");
_age = value;
}
}
public decimal Salary
{
get => _salary;
set => _salary = Math.Max(0, value); // отрицательное приводим к нулю
}
}
var e = new Employee { Name = " Пётр ", Age = 30, Salary = -500 };
Console.WriteLine($"{e.Name}, {e.Age}, {e.Salary}"); // Пётр, 30, 0
Исключения при неверных значениях можно перехватить, как описано в статье про try-catch. Для простых проверок, когда ошибка не критична, удобнее привести значение к допустимому или проигнорировать присваивание.
В C# 14 появилось ключевое слово field, которое позволяет обратиться к скрытому резервному полю прямо в блоке set, не объявляя отдельное поле вручную. Однако на старых версиях языка такой код не соберётся, поэтому в проектах на .NET 8 используют классическое резервное поле.
Required: обязательные свойства
Модификатор required, добавленный в C# 11, требует, чтобы значение свойства было задано при создании объекта. Если вы забудете присвоить его в инициализаторе, компилятор выдаст ошибку. Это удобнее, чем полагаться на конструктор с длинным списком параметров.
public class User
{
public required string Login { get; init; }
public required string Email { get; init; }
public string? Phone { get; init; } // необязательное
}
var ok = new User { Login = "alex", Email = "alex@example.com" };
// var bad = new User { Login = "alex" }; // ошибка: не задано обязательное Email
Сочетание required и init часто используют для моделей, передаваемых между слоями приложения. Необязательные свойства при включённых nullable-типах помечают знаком вопроса, как Phone в примере выше.
Свойства в интерфейсах и перечисление примеров применения
В интерфейсе можно объявить свойство без реализации, и каждый класс обязан его реализовать. Это способ задать контракт: какие данные должен предоставлять объект.
public interface IShape
{
string Name { get; }
double Area { get; }
}
public class Circle : IShape
{
public double Radius { get; init; }
public string Name => "Круг";
public double Area => Math.PI * Radius * Radius;
}
IShape shape = new Circle { Radius = 2 };
Console.WriteLine($"{shape.Name}: {shape.Area:F2}"); // Круг: 12,57
Свойства встречаются повсюду: Length у строки и массива, Count у списка, Now у DateTime. Для обращения к ним не нужны скобки, как у методов, что делает код компактнее. Свойство может быть и статическим, если оно относится ко всему классу, а не к объекту: об этом рассказано в материале про static.
Типичные ошибки
- Рекурсия в set: запись Name = value внутри свойства Name вызывает сама себя до переполнения стека. Нужно присваивать резервному полю.
- Тяжёлые вычисления в get, из-за которых каждое обращение замедляет программу.
- Открытые поля вместо свойств в публичных классах.
- Побочные эффекты в get: чтение значения не должно менять состояние объекта.
- Забытая инициализация строковых свойств при включённых nullable, из-за чего возникают предупреждения.
- Попытка присвоить init-свойство после создания объекта.
Частые вопросы
Чем свойство отличается от поля?
Поле хранит значение напрямую, а свойство работает через методы доступа get и set, что позволяет проверять данные, вычислять значения и ограничивать доступ. Для пользователя класса запись выглядит одинаково.
Когда использовать автосвойство, а когда полное?
Автосвойство подходит, когда дополнительной логики нет. Если нужна проверка значения, преобразование, уведомление об изменении или ленивое вычисление, пишите полное свойство с резервным полем.
Чем init отличается от private set?
Init позволяет задать значение при создании объекта через инициализатор, а потом запрещает менять его всем, включая методы самого класса. Private set разрешает изменять значение внутри класса в любой момент, но не позволяет задавать его в инициализаторе снаружи.
Можно ли сделать свойство только для записи?
Да, достаточно объявить только блок set, но на практике такие свойства почти не встречаются и считаются плохим стилем. Для передачи данных внутрь лучше подойдёт метод.
Что означает запись public int X { get; }?
Это автосвойство только для чтения. Значение можно задать в месте объявления или в конструкторе, после этого оно остаётся неизменным. Меняться оно не может ни снаружи, ни из обычных методов класса.
Замедляют ли свойства программу по сравнению с полями?
Для простых автосвойств разницы практически нет: компилятор JIT встраивает вызовы простых get и set. Заметное замедление возникает только если внутри тела выполняются тяжёлые операции.





