0Pricing
C# Academy · Урок

Неизменяемость с init и with

Используйте установщики только для инициализации, чтобы создавать неизменяемые объекты, и выражения with для получения копий с неразрушающими изменениями.

«Неизменяемость с init и with» — бесплатный урок C# Academy на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения C# Academy, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс C# Academy содержит 4 уроков всего.

Что такое неизменяемость

Неизменяемый объект нельзя изменить после создания. Неизменяемость предотвращает случайные изменения, по умолчанию делает объекты потокобезопасными и упрощает понимание кода. C# предоставляет сеттеры init и выражения with, которые упрощают создание неизменяемых объектов.

Сеттеры только на этапе инициализации

Аксессор init позволяет задать свойство во время инициализации объекта (в конструкторе или инициализаторе объекта), но не после неё. Это неизменяемый аналог set.

public class Point
{
    public double X { get; init; }
    public double Y { get; init; }
}

var p = new Point { X = 3.0, Y = 4.0 }; // OK — init phase
p.X = 5.0; // COMPILE ERROR — cannot set after init

// All of these are valid init-phase assignments:
var p2 = new Point(X: 1.0, Y: 2.0);
var p3 = new Point { X = 0, Y = 0 };

init в позиционных свойствах записей

Позиционные свойства записей по умолчанию доступны только для init — именно это делает записи неизменяемыми. Компилятор создаёт get; init; для каждого позиционного параметра.

// This record:
public record Person(string Name, int Age);

// Is equivalent to:
public record Person
{
    public string Name { get; init; }
    public int Age    { get; init; }
    public Person(string Name, int Age) { this.Name = Name; this.Age = Age; }
    // + Deconstruct, Equals, GetHashCode, ToString
}

var alice = new Person("Alice", 30);
alice.Name = "Bob"; // COMPILE ERROR

Выражения with: неизменяющее исходное значение изменение

Выражение with создаёт копию записи с изменёнными указанными свойствами. Исходная запись не меняется — это настоящая неизменяемость.

public record Person(string Name, int Age, string Email);

var alice = new Person("Alice", 30, "alice@example.com");

// Create a modified copy — alice is unchanged
var olderAlice = alice with { Age = 31 };
var renamed    = alice with { Name = "Alicia", Email = "alicia@example.com" };

Console.WriteLine(alice.Name);      // Alice (unchanged)
Console.WriteLine(olderAlice.Age);  // 31
Console.WriteLine(renamed.Name);    // Alicia

with для типов, не являющихся записями

C# 10 позволяет использовать with для любой структуры или класса с семантикой конструктора копирования, но естественнее всего этот подход работает с записями. Для классов его необходимо реализовать вручную.

// struct with with-expression:
public struct Temperature
{
    public double Celsius { get; init; }
    public double Fahrenheit => Celsius * 9 / 5 + 32;
}

var t1 = new Temperature { Celsius = 20 };
var t2 = t1 with { Celsius = 25 }; // copy with change
Console.WriteLine(t1.Celsius); // 20 — unchanged
Console.WriteLine(t2.Celsius); // 25

Цепочки выражений with

Объединяйте несколько выражений with в цепочку, чтобы создавать сложные преобразования: на каждом шаге из предыдущего неизменяемого значения создаётся новое.

public record Order(int Id, string Status, decimal Total, DateTime UpdatedAt);

var order = new Order(42, "Pending", 99.99m, DateTime.UtcNow);

// Apply a promotion discount then mark as confirmed
var finalOrder = order
    with { Total    = order.Total * 0.9m }    // 10% off
    with { Status   = "Confirmed" }
    with { UpdatedAt = DateTime.UtcNow };

Console.WriteLine(order.Status);     // Pending (original unchanged)
Console.WriteLine(finalOrder.Status); // Confirmed

Вычисляемые свойства в неизменяемых типах

Производные свойства неизменяемых типов естественным образом являются чистыми: они вычисляются на основе фиксированных значений свойств и всегда возвращают один и тот же результат для одинакового входного значения.

public record Money(decimal Amount, string Currency)
{
    public Money Add(Money other)
    {
        if (Currency != other.Currency)
            throw new InvalidOperationException("Currency mismatch");
        return this with { Amount = Amount + other.Amount };
    }

    public Money Multiply(decimal factor) =>
        this with { Amount = Amount * factor };

    public override string ToString() =>
        $"{Amount:F2} {Currency}";
}

var price = new Money(10.00m, "USD");
var tax   = price.Multiply(0.08m);
var total = price.Add(tax);
Console.WriteLine(total); // 10.80 USD

Неизменяемые коллекции

Сочетайте записи с ImmutableList<T> и другими типами из System.Collections.Immutable, чтобы создавать полностью неизменяемые структуры данных.

using System.Collections.Immutable;

public record ShoppingCart(
    string UserId,
    ImmutableList<CartItem> Items)
{
    public ShoppingCart AddItem(CartItem item) =>
        this with { Items = Items.Add(item) };

    public ShoppingCart RemoveItem(int productId) =>
        this with { Items = Items.RemoveAll(i => i.ProductId == productId) };

    public decimal Total => Items.Sum(i => i.Price * i.Quantity);
}

Потокобезопасность благодаря неизменяемости

Неизменяемые объекты по своей природе потокобезопасны: при совместном использовании между потоками синхронизация не нужна, поскольку их состояние не может измениться после создания.

// Immutable config record shared across all threads safely
public record AppConfig(
    string ConnectionString,
    int MaxRetries,
    TimeSpan Timeout);

// Register as singleton — safe because record is immutable
builder.Services.AddSingleton(
    new AppConfig(
        ConnectionString: config["DB"]!,
        MaxRetries: 3,
        Timeout: TimeSpan.FromSeconds(30)));

// Any thread can read this simultaneously without locks

Практический пример: функциональное сохранение событий

Неизменяемые записи естественно сочетаются с сохранением событий: каждое доменное событие неизменяемо, а состояние получают свёрткой событий — без изменений и неожиданных эффектов.

public record OrderState(
    int Id,
    string Status = "Draft",
    decimal Total = 0m);

public static OrderState Apply(OrderState state, object evt) => evt switch
{
    OrderPlaced   e => state with { Status = "Pending", Total = e.Total },
    OrderShipped  _ => state with { Status = "Shipped" },
    OrderCancelled _ => state with { Status = "Cancelled" },
    _ => state
};

// Fold events to get current state:
var state = events.Aggregate(
    new OrderState(id),
    Apply);

Быстрая проверка

Что выражение 'with' делает с исходной записью?

Повторение: неизменяемость с init и with

Основные выводы:

  • Аксессор init: свойство можно задать только во время инициализации, но не после неё
  • Позиционные свойства записей по умолчанию доступны только для init
  • Выражение with: создаёт изменённую копию, а исходная запись не меняется
  • Объединяйте выражения with в цепочки для многошаговых преобразований
  • Неизменяемые типы потокобезопасны без синхронизации
  • Сочетайте их с ImmutableList<T> для полностью неизменяемых графов объектов

Часто задаваемые вопросы

Урок «Неизменяемость с init и with» бесплатный?

Да — полный текст урока «Неизменяемость с init и with» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс C# Academy, подпишись на CoddyKit PRO. Курс C# Academy содержит 4 уроков всего.

Чему я научусь в уроке «Неизменяемость с init и with»?

Используйте установщики только для инициализации, чтобы создавать неизменяемые объекты, и выражения with для получения копий с неразрушающими изменениями. Ты практикуешь C# Academy с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать C# Academy?

Предыдущий опыт не требуется. C# Academy на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.

Сколько времени занимает урок «Неизменяемость с init и with»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке C# Academy?

Да. Каждый урок C# Academy включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.

Все уроки этого курса

  1. Типы записей: основы и синтаксис
  2. Неизменяемость с init и with
  3. Равенство значений и деконструкция
  4. Записи в предметно-ориентированном проектировании
← Назад к C# Academy