我给一段并发临界区加了 lock 锁、自以为线程安全了,可线上还是不停冒出数据被并发改乱的脏数据,我反复确认 lock 语句写得没错临界区也包对了,最后才发现问题出在我锁的那个对象上——每个线程锁的根本不是同一个对象、这把锁形同虚设的深度复盘

我有一段会被多个线程并发调用的代码,里面要读改一份共享状态(往共享集合加元素、更新共享计数),我知道需要互斥就规规矩矩用 lock 把临界区包了起来、自以为万无一失。可线上偏偏时不时出现脏数据:共享集合元素丢失、计数对不上,典型的并发写竞争没被挡住。我反复检查 lock 语法没错、临界区范围也把读改都包进去了,又怀疑别处有没加锁的路径也没发现。直到盯着 lock 后面括号里的锁对象看才如遭雷击:我锁的对象要么是方法里 lock(new object()) 每次新建的、要么是一个每个实例各有一份的实例字段而多线程操作的是不同实例,无论哪种结果都一样——每个线程锁的都是一个不同的对象,而 lock 的互斥只在多个线程争用同一个锁对象时才成立,大家锁各自不同的对象就等于各在各的门上挂锁谁也挡不住谁全都畅通进了临界区,这把锁从来没真正锁住过任何东西。根因是 lock(obj){...} 的语义是进入代码块前获取 obj 这个对象上的互斥锁、同一时刻只有一个线程能持有同一个 obj 的锁、其他想锁这个 obj 的线程必须等待,所以 lock 能否互斥完全取决于需要互斥的线程锁的是不是同一个对象引用,锁不同对象彼此毫无关系互不阻塞;锁本身不保护代码也不保护数据,它提供的只是对同一个锁对象的排他争用。正解是用 private readonly object _lock=new object() 作锁:readonly 防被重新赋值成新对象、private 防外部锁它引发意外互斥死锁、专用(别用 this/typeof/字符串字面量)避免和别处共用、且锁对象作用域要和被保护状态匹配(静态状态用静态锁实例状态用实例锁),访问同一份共享状态的每一处都必须锁这同一个对象;能用 Interlocked 原子操作、ConcurrentDictionary 等并发安全类型的就别手写锁。这篇复盘从故障现场讲到 lock 互斥的前提是锁同一对象、几种锁对象写法对错对照、怎么诊断,再到私有专用锁与全路径加锁的完整正解与骨架,以及分布式锁 key 不一致/幂等键算不一样/缓存 key 规则不统一/消息主题对不上等同类坑,和协调与共同支点、协调的关键不在用了机制而在大家指向同一个支点的认知。

我给一段并发临界区加了 lock 锁、自以为线程安全了,可线上还是不停冒出数据被并发改乱的脏数据,我反复确认 lock 语句写得没错、临界区也包对了,最后才发现问题出在我锁的那个对象上——每个线程锁的根本不是同一个对象、这把锁形同虚设

这是一次让我把 C# 里"lock 锁对象"这件事,从"加了 lock 就线程安全了",重新理解成"只有大家锁的是同一个对象,lock 才真的互斥"的事故。我给一段并发临界区加了 lock 锁,自以为线程安全了。可线上还是不停冒出"数据被并发改乱"的脏数据。我反复确认 lock 语句写得没错、临界区也包对了,最后才发现问题出在我锁的那个对象上——每个线程锁的根本不是同一个对象,这把锁形同虚设。这篇就把这次"加了 lock 却没起到互斥作用"的事故,从头到尾复盘一遍。

故障现场:lock 加了,数据照样被并发改乱

我有一段代码,会被多个线程并发调用,里面要读改一份共享状态(比如往一个共享集合里加元素、或更新一个共享计数)。我知道这需要互斥,于是规规矩矩地用 lock 把临界区包了起来,自以为万无一失。可线上偏偏时不时出现脏数据:共享集合元素丢失、计数对不上,典型的并发写竞争没被挡住的症状。

我一开始百思不得其解:我明明 lock 了啊。我反复检查 lock 的语法没错、临界区的范围也确实把读改操作都包进去了。我又怀疑是不是别处有没加锁的访问路径,排查也没发现。直到我盯着那个 lock 后面括号里的锁对象看,才如遭雷击——我锁的对象,要么是在方法里 lock (new object()) 每次都新建的一个对象,要么是一个"每个实例各有一份"的实例字段、而多个线程操作的是不同的实例。无论哪种,结果都一样:每个线程进来,锁的都是一个不同的对象;而 lock 的互斥,只在"多个线程争用同一个锁对象"时才成立——大家锁的是各自不同的对象,就等于各自在各自的门上挂锁,谁也挡不住谁,全都畅通无阻地进了临界区。这把锁,从来就没真正锁住过任何东西。

// 错误写法1: 每次进方法都 new 一个对象当锁 —— 每个调用锁的都是不同对象
public void AddItem(int x)
{
    lock (new object())          // ★ 每次都是全新对象, 没有任何两个线程锁到一起
    {
        _sharedList.Add(x);      // 临界区毫无保护, 并发写竞争照旧
    }
}

// 错误写法2: 锁一个"每个实例各一份"的字段, 但多线程操作不同实例
public class Counter
{
    private readonly object _lock = new object();   // 每个 Counter 实例各一个
    private static int _sharedTotal;                // 但要保护的是【静态】共享状态!
    public void Inc()
    {
        lock (_lock)             // 不同实例锁不同的 _lock, 锁不住静态的 _sharedTotal
        {
            _sharedTotal++;      // 仍然并发竞争
        }
    }
}

// lock 的本质: lock(obj) { ... } ≈ 对 obj 这个对象争用一把互斥锁。
//   只有【锁同一个 obj】的线程之间才互斥; 锁不同 obj 的, 互不影响。
// 锁错对象(每次 new / 实例锁配静态状态) = 大家锁的不是同一个 → 形同虚设

问题被钉死在这个认知错位上:我以为"写了 lock,这段代码就被保护了",但 lock 保护的不是"这段代码",而是"对某一个特定锁对象的争用"——它的互斥效果,完全取决于"所有需要互斥的线程,是不是都锁的同一个对象"。我锁了一个每次都新建的、或每个实例各有一份的对象,等于每个线程都在锁一把"只有自己拿着钥匙"的锁,彼此之间根本不存在争用,自然谁也挡不住谁。lock 语句本身写得再对、临界区包得再准,只要锁错了对象,这把锁就是个摆设。我以为我给临界区上了一把公共的大门锁,其实我给每个进门的人各发了一把自己的小锁、各锁各的门。我以为大家在抢同一把锁,其实每个人都轻松拿到了一把谁也不抢的锁。

第一件事:想明白 lock 互斥的前提是"锁同一个对象"

把这次事故彻底想清楚,关键是理解lock (obj) { ... } 的语义是:进入代码块前,获取 obj 这个对象上的互斥锁,退出时释放;同一时刻,只有一个线程能持有 同一个 obj 的锁,其他想锁这个 obj 的线程必须等待。所以 lock 能否互斥,完全取决于"需要互斥的那些线程,锁的是不是同一个对象引用"。锁不同的对象,彼此毫无关系、互不阻塞。

这就引出了用锁对象的几条铁律:第一,锁对象必须是"所有要互斥的线程都能访问到的、同一个、稳定的引用"——绝不能每次 new;第二,锁的对象要和"它保护的共享状态"在生命周期/作用域上匹配——保护静态共享状态就得用静态锁对象,保护实例状态用实例锁对象;第三,锁对象应当是私有的、专用的(private readonly object _lock = new object();),不要 lock(this)lock(typeof(T)) 或锁字符串字面量——因为这些对象外部也能拿到、也可能被别处锁,容易引发意外的互斥或死锁。关键认知是:锁本身不保护代码、也不保护数据;它提供的只是"对同一个锁对象的排他争用";要让一段对共享状态的访问真正互斥,必须让所有访问这份状态的线程,都去争用同一个、且只用于保护这份状态的锁对象。

// 正解: 用一个私有、只读、专用、且与被保护状态作用域匹配的锁对象

// 场景A: 保护实例状态 → 用【实例】锁对象(每个实例各保护自己的状态)
public class SafeList
{
    private readonly List _items = new();
    private readonly object _lock = new object();   // 实例字段, readonly 不会被换掉
    public void Add(int x)
    {
        lock (_lock)            // 同一实例的所有调用, 锁同一个 _lock → 真互斥
        {
            _items.Add(x);
        }
    }
}

// 场景B: 保护静态共享状态 → 必须用【静态】锁对象
public class GlobalCounter
{
    private static int _total;
    private static readonly object _lock = new object();   // 静态, 全局唯一
    public static void Inc()
    {
        lock (_lock)            // 所有线程锁同一个静态 _lock → 真互斥
        {
            _total++;
        }
    }
}

// 要点: 锁对象 readonly(防被重新赋值变成新对象)、private(防外部锁它)、
//   专用(别用 this/typeof/字符串)、作用域匹配被保护的状态(实例 or 静态)

想通这一层,我才明白自己错在哪:我把注意力全放在了"有没有写 lock、临界区包得对不对"上,却忽略了"lock 的那个对象,是不是所有要互斥的线程共用的同一个"这个真正决定成败的前提。lock 是一种"靠争用同一个对象来协调"的机制,而我让每个线程去争用各自不同的对象,这个协调就从根上失效了。根治之道,是让锁对象成为"大家共同认定的、唯一的那个协调点":私有、只读、专用、作用域和被保护的状态对齐。加锁的关键不在于"写了 lock",而在于"大家锁的是不是同一把锁"。

第二件事:正解——私有只读专用锁对象,作用域与被保护状态对齐

找到根因,正解就清晰了:用一个 private readonly object _lock = new object(); 作锁——readonly 防止它被重新赋值成新对象,private 防止外部锁它引发意外互斥/死锁,专用(别用 this/typeof/字符串字面量)避免和别处共用同一对象;并让锁对象的作用域和它保护的共享状态匹配:保护静态状态用静态锁、保护实例状态用实例锁;所有访问同一份共享状态的代码,都必须锁这同一个对象。

// 错误: 每次 new / 锁可变字段 / lock(this) —— 锁不到一起或被外部干扰
lock (new object()) { ... }              // ✗ 每次不同对象, 没互斥
lock (this) { ... }                       // ✗ 外部可能也 lock(这个实例) → 意外/死锁
lock ("LOCK") { ... }                     // ✗ 字符串驻留, 全进程共享同一个 → 乱

// 正解: 私有、只读、专用、作用域匹配
public class Account
{
    private decimal _balance;
    private readonly object _lock = new object();   // 实例锁: 保护本实例的 _balance

    public void Withdraw(decimal amount)
    {
        lock (_lock)                       // 同一账户的所有操作锁同一个 _lock
        {
            if (_balance >= amount) _balance -= amount;
        }
    }
    public void Deposit(decimal amount)
    {
        lock (_lock)                       // ★ 关键: 和 Withdraw 锁【同一个】对象
        {
            _balance += amount;            // 读改 _balance 的每处都锁 _lock 才互斥
        }
    }
}

// 更省心: 能用并发安全类型/原子操作就别手写锁
private int _counter;
Interlocked.Increment(ref _counter);                 // 原子自增, 无需 lock
private readonly ConcurrentDictionary _map = new();  // 并发安全集合

这套做法的精髓,是把锁对象做成"一个稳定的、唯一的、大家共同争用的协调点",并确保"每一处访问同一份共享状态的代码,都锁这同一个对象"。readonly 保证这个协调点不会中途被换掉、private 保证它不被外人占用、作用域匹配保证它的"覆盖范围"正好罩住要保护的状态。更进一步,能用 Interlocked 原子操作、ConcurrentDictionary 等并发安全类型解决的,就别手写锁——把"用对锁"这件容易错的事,交给经过验证的工具。不是"写了 lock"就安全,而是要让所有竞争者真正争用同一把锁。

【用 lock 做互斥, 我现在认死的几条】

1. lock(obj) 只在"多个线程锁同一个 obj"时才互斥; 锁不同对象 = 没锁

2. 锁对象绝不能每次 new; 必须是稳定、唯一、大家共用的同一引用

3. 用 private readonly object _lock = new object(); 作专用锁

4. 别 lock(this) / lock(typeof) / lock(字符串) —— 外部能拿到, 易意外/死锁

5. 锁对象作用域要和被保护状态匹配: 静态状态用静态锁, 实例状态用实例锁

6. 访问同一份共享状态的【每一处】代码, 都要锁【同一个】对象

7. 能用 Interlocked / Concurrent 集合等并发安全工具的, 优先用, 少手写锁

第三件事:其他"以为协调了、其实各自为政"的同类坑

顺着"协调机制要靠'大家认定同一个协调点'才生效、各用各的就形同虚设"这条线,我把同类的坑都排查了一遍:

第一个,分布式锁的 key 不一致。多个节点想互斥,却用了不同的锁 key(或锁在各自本地),等于没有全局互斥,和"每次 new 锁对象"如出一辙。

第二个,幂等用的去重键算得不一样。两条本该被识别为"同一操作"的请求,各自算出不同的幂等键,去重就失效了——大家没用同一个"身份依据"。

第三个,缓存 key 拼接规则不统一。读和写用了不同规则拼 key,写进去的缓存读时永远命中不到,等于没缓存。

第四个,事件订阅和发布用的主题/通道名对不上。发布方发到一个 topic、订阅方听另一个,消息永远收不到——双方没约定同一个通道。

第四件事:几种锁对象写法——对错对照表

我把常见的锁对象写法摆在一起对比,核心看"会不会真的互斥、有没有副作用":

锁对象写法 互斥效果 问题
lock (new object()) 每次新对象, 没有两个线程锁到一起
实例锁 + 静态共享状态 不同实例锁不同对象, 锁不住静态状态
lock (this) 有但危险 外部也能 lock 这个实例, 易意外互斥/死锁
lock (typeof(T)) 有但危险 Type 全局共享, 跨无关代码互相阻塞
lock ("字符串字面量") 有但危险 字符串驻留, 全进程共用同一个, 乱锁
private readonly object _lock 有, 正确 私有专用, 作用域匹配即安全

看清这张表,写法就有定论了:正确的锁对象只有一种——私有(private)、只读(readonly)、专用(不兼作他用)、作用域和被保护状态匹配的对象;new object() 每次新建是"没锁",实例锁配静态状态是"锁错范围",this/typeof/字符串则是"锁了但会被外部干扰"。我这次踩坑,正是栽在"每次 new / 实例锁配静态状态"这种"看着锁了、实则没互斥"的写法上。

第五件事:我曾经对 lock 想当然的几个误区

这次事故也把我对 lock 的一堆"想当然"照了个底朝天:

我以为 实际上
写了 lock 这段代码就线程安全了 lock 保护的是对同一锁对象的争用, 不是代码本身
lock 后面锁什么对象无所谓 必须是所有竞争线程共用的同一个稳定对象
每个方法里 new 一个对象锁一下就行 每次新对象, 没有任何互斥效果
实例锁能保护静态共享状态 不同实例锁不同对象, 锁不住静态状态
lock(this) 简单又好用 外部可能也锁它, 易意外互斥甚至死锁

这些误区的根子是同一个:我把"lock 这个动作"当成了能直接给代码或数据上保护的"魔法咒语",而没理解它本质是一种"靠争用同一个对象来排队"的协调机制——它的全部效力,都建立在"大家争用的是同一个对象"这个前提上。一旦每个线程锁的是各自不同的对象,这个"排队"就不存在了,lock 写得再标准也只是个空动作。把"我用了协调机制"当成"协调一定生效",而忽略了协调生效的前提是"大家协调在同一个点上",是这类并发问题的共同根源。

第六件事:写 lock、排查"加了锁还是有并发问题"时,我现在的自检习惯

现在每当我写 lock、或排查"明明加了锁却还是出并发脏数据",我都会先按这张图问自己:

这张图的精髓,是"lock 只在大家锁同一个对象时才互斥;先确认锁对象是不是稳定唯一共用的,再确认每处访问都锁了它"设计就用 private readonly 专用锁对象、作用域和被保护状态对齐、访问同一状态的每处都锁同一对象、排查就盯住 lock 括号里的对象看是不是每次 new 或锁错范围这套习惯,让我从"写了 lock 就放心"变成了"先确认大家锁的是不是同一把锁"——核心始终是:lock(obj){...} 的语义是进入代码块前获取 obj 这个对象上的互斥锁、退出时释放,同一时刻只有一个线程能持有同一个 obj 的锁、其他想锁这个 obj 的线程必须等待;所以 lock 能否互斥完全取决于需要互斥的那些线程锁的是不是同一个对象引用,锁不同的对象彼此毫无关系互不阻塞;由此有几条铁律:锁对象必须是所有要互斥的线程都能访问到的同一个稳定引用(绝不能每次 new),锁对象要和它保护的共享状态在生命周期作用域上匹配(保护静态共享状态就用静态锁对象、保护实例状态用实例锁对象),锁对象应当是私有专用的 private readonly object _lock=new object() 而不要 lock(this)/lock(typeof(T))/锁字符串字面量(因为这些对象外部也能拿到也可能被别处锁、容易引发意外互斥或死锁),且访问同一份共享状态的每一处代码都必须锁这同一个对象;锁本身不保护代码也不保护数据,它提供的只是对同一个锁对象的排他争用,要让对共享状态的访问真正互斥必须让所有访问这份状态的线程都去争用同一个且只用于保护这份状态的锁对象;能用 Interlocked 原子操作、ConcurrentDictionary 等并发安全类型解决的就别手写锁。

我立下的几条规矩

这场"加了 lock 却没互斥"的事故,换来了我写并发代码时,刻进骨子里的几条铁律:

  1. lock(obj) 只在多个线程锁同一个 obj 时才互斥;锁不同对象等于没锁。
  2. 锁对象绝不能每次 new;必须是稳定、唯一、大家共用的同一引用。
  3. 用 private readonly object _lock = new object() 作专用锁。
  4. 别 lock(this)/lock(typeof)/lock(字符串)——外部能拿到,易意外互斥/死锁。
  5. 锁对象作用域要和被保护状态匹配:静态状态用静态锁,实例状态用实例锁。
  6. 访问同一份共享状态的每一处,都要锁同一个对象,别漏路径。
  7. 能用 Interlocked / Concurrent 集合等并发安全工具的,优先用,少手写锁。

附:我现在写线程安全类的"私有专用锁 + 全路径加锁"骨架

这是我现在写线程安全类固定套的骨架——把这次踩坑的教训(私有只读专用锁、作用域匹配、每处访问都锁同一对象、优先用并发安全工具)固化成一套结构,让"锁形同虚设"那种坑再不会埋进代码:

// 写法一: 手写锁 —— 私有 readonly 专用锁, 所有访问同一状态处都锁它
public class ThreadSafeBuffer
{
    private readonly List _items = new();
    // 私有(外部锁不到)、readonly(不会被换成新对象)、专用(只护 _items)
    private readonly object _lock = new object();

    public void Add(T item)
    {
        lock (_lock) { _items.Add(item); }          // 锁 _lock
    }
    public bool TryTake(out T item)
    {
        lock (_lock)                                 // ★ 和 Add 锁同一个 _lock
        {
            if (_items.Count == 0) { item = default!; return false; }
            item = _items[0]; _items.RemoveAt(0); return true;
        }
    }
    public int Count
    {
        get { lock (_lock) { return _items.Count; } } // 读也要锁同一个 _lock
    }
    // 要点: 凡是碰 _items 的每一处, 无一例外都 lock(_lock); 漏一处就破功
}

// 写法二(更优先): 能用并发安全类型就别手写锁, 把"用对锁"交给标准库
public class Counters
{
    private int _total;
    private readonly ConcurrentDictionary _byKey = new();

    public void Inc()            => Interlocked.Increment(ref _total);   // 原子
    public int Total            => Volatile.Read(ref _total);
    public void Bump(string k)  => _byKey.AddOrUpdate(k, 1, (_, v) => v + 1);
}

// 自检: 压力测试 N 个线程并发调用, 断言最终状态与预期一致
//   long n = 100_000;
//   Parallel.For(0, n, _ => counters.Inc());
//   Debug.Assert(counters.Total == n, "并发计数不一致 → 锁没生效!");

这套骨架把我这次的教训钉死在了结构里:手写锁时,锁对象一律 private readonly object _lock(私有防外部锁、只读防被换、专用只护一份状态)、且凡是碰这份共享状态的每一处(增、删、改、读)无一例外锁同一个 _lock;能用 Interlocked / ConcurrentDictionary 等并发安全类型的就优先用、把"用对锁"交给标准库;最后用多线程压力测试 + 断言最终状态验证锁真的生效。这样,所有竞争线程都汇聚到同一把锁上排队,而不再是当初那个"各锁各的、谁也挡不住谁"的形同虚设。把"协调的关键在于大家指向同一个支点"这个道理,沉淀成写并发类的固定骨架,这是我对这次"锁了却照样改乱的脏数据"最实在的交代——毕竟,一把谁也不抢的锁,锁得再勤,也只是自欺欺人。

写在最后

回头看,这场由"锁错对象"引发的"lock 形同虚设"事故,真正教给我的,远不止"用 private readonly 锁对象"这一个技巧。它让我对"任何'协调多方'的机制——锁、约定、信号、key、通道——其生效的根本前提,都是'多方协调在同一个点上';协调机制本身只是提供了'协调的能力',而能力要变成'实际的协调',必须靠所有参与方真正地、一致地指向同一个协调点;一旦各方'各用各的',哪怕每一方都规规矩矩用了这个机制,协调也根本不存在",有了一次刻骨的体会。我栽跟头,是因为我把"使用了协调机制(写了 lock)"当成了"协调已经生效(线程被互斥了)",却忽略了协调生效的真正前提是"所有竞争方争用的是同一个协调点(同一个锁对象)"——我以为 lock 是一句给代码上保护的咒语,念了就灵;我没理解它的本质是"让想进同一扇门的人,去抢同一把挂在门上的锁"——而我却给每个进门的人,各发了一把只有他自己拿着的锁;于是每个人都轻松地"锁上了自己的锁、开了自己的门",彼此之间根本没有发生过任何争抢,这扇本该只容一人通过的门,实际上对所有人同时敞开着这让我领悟到一个关于"协调与共同的支点"的深刻认知:一切让多方相互协调、相互排斥、相互识别的机制(互斥锁、分布式锁、幂等键、缓存键、消息主题、约定的暗号),其效力都不来自"机制本身被使用了",而来自"所有参与方都指向了同一个共同的支点";这个共同支点(同一个对象、同一个 key、同一个通道)才是协调真正发生的地方——它像一个所有人必须汇聚的"交汇点",只有大家都来到这同一个点上,排队、互斥、识别、传递才可能发生;而最隐蔽的失败,恰恰是"每个人都用了正确的机制、却各自指向了不同的点":表面上看人人合规、机制齐备,实质上由于缺了"同一个支点"这个前提,协调从未真正建立——它失败得悄无声息,因为没有任何一处报错,只有结果在不断出错这给了我一种看待"一切'用某种机制协调多方'之事"时的清醒:每当我用一种机制去协调多方(加锁、去重、缓存、收发消息)时,要追问"所有参与方,是不是真的指向了同一个协调点?那个对象/key/通道,是不是稳定唯一、大家共用的同一个,还是各自不同的"——确保协调机制的所有参与方汇聚到同一个共同支点上,而不是各用各的、貌合神离;"协调的关键不在'用了机制'、而在'大家指向同一个支点'",是用对 lock、也是用对一切多方协调机制的关键认清 lock 只在锁同一对象时互斥、锁错对象等于没锁、要用私有专用且作用域匹配的锁对象——这,是我用一次"加了锁却照样并发脏数据"的事故,换来的、关于 C#、也关于如何让协调汇聚于同一支点的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次写 lock 时,先盯一眼括号里的对象、问一句"所有竞争的线程,锁的真的是同一个它吗?",那我对着那些"明明锁了却照样改乱"的脏数据排查的大半天,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我给一批缓存数据统一设了一小时过期、平时缓存命中率高得很数据库压力很小,可每隔整整一小时数据库就会突然被海量请求瞬间打垮、CPU 飙满响应超时,排查很久才惊觉那批缓存是同一时刻批量写进去的、于是又在同一时刻集体失效把流量齐刷刷地全砸向了数据库的深度复盘

2026-6-3 8:36:14

技术教程

我在 TypeScript 里定义了一个配置对象、method 字段明明写死的就是字符串 GET,可把它传给一个只接受 GET 或 POST 这种联合字面量类型的函数时编译器死活报错说 string 不能赋值,我盯着那个明明白白的 GET 看了半天最后才搞懂编译器早就把我写死的字面量悄悄拓宽成了普通的 string 的深度复盘

2026-6-3 8:48:27

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索