享元模式适用于gc压力来自大量相似小对象时,核心是分离可共享的内部状态与需客户端传入的外部状态;典型信号为循环中反复创建仅字段差异微小的对象,且对象无事件、无资源释放依赖、不持有上下文。

享元模式不是“写了就能省内存”,而是当你发现 GC 压力来自上万份几乎一样的小对象(比如字符、图标、单元格样式)时,才值得动手——否则就是给代码加复杂度。
什么时候该用 IFlyweight 而不是直接 new
典型信号是:你在循环里反复创建对象,每个只差一两个字段(如 x、y、isHighlighted),而其余字段(如 char、fontFamily、fontSize)完全一致;同时这些对象不订阅事件、不调用 Dispose()、也不依赖 this 持有业务上下文。
反例包括:
- 对象内部有
public event EventHandler Clicked—— 享元被多处复用,事件会混乱 - 构造函数里直接 new 一个
Bitmap或Font并赋给字段 —— GDI+ 对象不能跨线程/上下文共享 - 把
RenderContext存成字段再在Draw()里读取 —— 外在状态必须每次传入,否则破坏共享安全性
FlyweightFactory.GetFlyweight() 的 key 怎么设计才靠谱
key 必须唯一、稳定、无歧义地标识一组内在状态。别用 ToString()、JSON 序列化或匿名类型(new { Font = "Arial", Size = 12 }),它们要么太重,要么每次都是新类型,查不到缓存。
推荐做法:
- 用
string拼接,格式统一为"{Char}_{FontSize:F1}_{FontFamily}",注意替换空格和特殊字符 - 若需更高性能且字段不多,可用
ValueTuple<char int string></char>,但要确保GetHashCode()和Equals()正确实现 - 避免把颜色(如
Color.FromArgb(255, 100, 100, 100))塞进 key —— 浮点精度、Alpha 通道、命名差异都会导致缓存失效
ConcreteFlyweight 为什么必须是不可变的
因为多个线程或渲染帧可能同时调用同一个实例的 Operation() 方法。一旦某个地方改了它的字段(比如 public int X { get; set; }),其他调用方立刻拿到脏数据——这不是 bug,是设计崩溃。
强制手段:
- 所有字段声明为
readonly,构造函数一次性注入全部内在状态 - 不提供任何 public setter,连
protected都慎用 - 极少数需延迟加载(如首次访问才解码图片),可用
Lazy<bitmap></bitmap>,但初始化逻辑仍要线程安全
外在状态传参超过 4–5 个怎么办
说明你没合理分层。不要硬塞一堆参数进 Render(int x, int y, bool isHighlighted, float zoom, Color overrideColor, bool isRTL) —— 可读性差,也难维护。
正确做法:
- 封装成轻量
struct RenderContext,只含数据,不含方法或引用类型(避免 GC 压力) - 禁止在
RenderContext里塞业务逻辑(如CalculateLayout())、服务引用(如IUserService)或大对象(如整个DocumentModel) - 如果
RenderContext自身开始变重或频繁修改,说明外在状态边界已模糊,应回头重新审视哪些该进享元、哪些真该由客户端管
最常被忽略的一点:享元类必须是 class,不能是 struct —— 结构体拷贝会破坏共享语义,且无法实现接口多态;工厂返回的也必须是接口(如 ICharacter),而非具体类型,否则后期替换实现(比如加 GPU 缓存)就卡死了。










