C# Academy · 课时

使用 init 与 with 实现不可变性

使用仅限 init 的设置器创建不可变对象,并使用 with 表达式生成以非破坏方式修改的副本。

第 2 / 4 课12 个步骤

使用 init 与 with 实现不可变性 是 CoddyKit 上的免费 C# Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 4 节课。

什么是不可变性

不可变对象在创建后不能被更改。不可变性可以防止意外修改,使对象默认具备线程安全性,并简化代码推理。C# 提供 init 设置器和 with 表达式,让实现不可变性更加便捷。

仅限 init 的设置器

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> 结合,构建完全不可变的对象图
免费开始

用 AI 导师学习 C# — 免费

在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。

课程
93
课程
346

常见问题解答

「使用 init 与 with 实现不可变性」课时是免费的吗?

是的 — 「使用 init 与 with 实现不可变性」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 4 节课。

「使用 init 与 with 实现不可变性」这节课中我会学到什么?

使用仅限 init 的设置器创建不可变对象,并使用 with 表达式生成以非破坏方式修改的副本。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 C# Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「使用 init 与 with 实现不可变性」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 C# Academy 课中编写并运行代码吗?

能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 记录类型:基础与语法
  2. 使用 init 与 with 实现不可变性
  3. 值相等性与解构
  4. 领域驱动设计中的记录
← 返回 C# Academy