Основы C#

Свойства get и set в C#

Обновлено: · 6 мин чтения

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

Свойства get и set в C#
Содержание
  1. Свойство и поле: в чём разница
  2. Полная форма свойства с get и set
  3. Автоматические свойства
  4. Разный доступ у get и set
  5. Init: значение только при создании
  6. Вычисляемые свойства
  7. Валидация значений в set
  8. Required: обязательные свойства
  9. Свойства в интерфейсах и перечисление примеров применения
  10. Типичные ошибки
  11. Частые вопросы

Свойство и поле: в чём разница

Поле - это переменная внутри класса, которая хранит данные напрямую. Если сделать поле открытым, любой код сможет записать в него что угодно, и класс не сможет этого ни проверить, ни запретить. Свойство решает проблему: снаружи оно используется как поле, а внутри имеет код доступа. Класс сам решает, что вернуть и какие значения принять.

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. Заметное замедление возникает только если внутри тела выполняются тяжёлые операции.