C#JuniorTechnical

Объясните разницу между const и readonly в C#.

const встраивается как литерал при компиляции (только примитивы/строки), readonly вычисляется в runtime и может хранить любой тип. Ключевая ловушка const — бинарная несовместимость: при изменении значения потребители видят старое до перекомпиляции.

const vs readonly в C#

Оба ключевых слова создают «неизменяемые» поля, но с принципиально разной семантикой:

  • const — константа времени компиляции. Значение должно быть известно при компиляции, встраивается (inlined) в вызывающий код как литерал. Допустимые типы: числа, строки, bool, enum, null.
  • readonly — поле, которое можно присвоить только в объявлении или в конструкторе. Вычисляется в runtime. Допустимые типы: любые, включая сложные объекты и структуры.
public class Config
{
    // const: значение 3.14159 встраивается в каждый вызывающий файл
    public const double Pi = 3.14159265358979;

    // const string — интернируется, встраивается как литерал
    public const string AppName = "Talento";

    // readonly: вычисляется однажды в runtime, не встраивается
    public static readonly DateTime LaunchDate = new DateTime(2024, 1, 15);

    // readonly с вычислением из внешних данных
    public readonly string ConnectionString;

    public Config(IConfiguration configuration)
    {
        // Присвоение возможно только здесь
        ConnectionString = configuration["Db:ConnectionString"]
            ?? throw new InvalidOperationException("DB connection string not set");
    }
}

// Ошибка: const не может быть присвоен в конструкторе
// public const string Value;
// Value = "x"; // CS0133

// Ошибка: const не может хранить объект
// public const List<int> Items = new(); // CS0134

// Правильно для коллекций:
public static readonly IReadOnlyList<string> AllowedRoles =
    new[] { "Admin", "Recruiter", "Candidate" };

Критическая проблема версионирования с const

Если сборка A определяет public const int MaxRetries = 3, а сборка B её использует, то при перекомпиляции A со значением 5 — сборка B всё равно использует старое значение 3, пока B не будет перекомпилирована. Это классическая бинарная несовместимость. С static readonly такой проблемы нет — значение читается в runtime.

// Библиотека v1
public const int MaxRetries = 3;
// После деплоя v2 с MaxRetries = 5 — старые потребители всё равно видят 3!

// Безопаснее:
public static readonly int MaxRetries = 5; // читается в runtime

readonly struct и in-параметры

// readonly struct — все поля неизменяемы, нет defensive copy
readonly struct Point
{
    public double X { get; }
    public double Y { get; }
    public Point(double x, double y) { X = x; Y = y; }
    public double Distance => Math.Sqrt(X * X + Y * Y);
}

// 'in' параметр + readonly struct = нет копии при передаче
void PrintDistance(in Point p) => Console.WriteLine(p.Distance);

Подводные камни

  • Версионирование const. Публичные const в библиотеках — ловушка: изменение значения требует перекомпиляции всех потребителей. Для публичных API предпочитайте static readonly.
  • const и enums — то же самое. const значения enum также встраиваются — изменение enum-значения ломает ABI.
  • readonly не делает объект иммутабельным. readonly List<int> Items = new();ссылка не изменится, но сам список можно модифицировать. Для иммутабельности используйте IReadOnlyList<T> или ImmutableArray<T>.
  • Struct без readonly — defensive copy. Если struct-поле не помечено readonly, каждый вызов метода на нём создаёт копию (defensive copy). Добавьте readonly к struct или к полю.
  • Потокобезопасность readonly. readonly гарантирует видимость значения после конструктора, но не атомарность изменений вложенных объектов. Для многопоточного доступа нужен lock или Interlocked.

Common mistakes

  • Путать константы времени компиляции и readonly-поля с похожим механизмом из другой версии или платформы.
  • Игнорировать runtime-границы C#: lifecycle, DI scope, SQL translation, UI thread или platform API.
  • Не обсуждать null/empty/error cases и поведение под нагрузкой.

What the interviewer is testing

  • Кандидат объясняет константы времени компиляции и readonly-поля на конкретном примере, а не только определением.
  • Указывает последствия для производительности, тестируемости и поддержки.
  • Различает документированное поведение текущего стека и устаревшие практики.

Sources

Related topics