享元模式核心是分离内蕴状态(可共享、只读,如字符、字体、字号)与外蕴状态(不可共享、上下文相关,如位置、选中态);c#实现关键在于明确哪些状态由客户端传入、哪些缓存在享元内部,键建议用字符串格式"{char}_{fontsize}_{fontname}",享元类必须为私有构造的class,且返回接口而非具体类型。

享元模式的核心在于分离内蕴状态和外蕴状态
内蕴状态(intrinsic state)是可共享的、不变的,比如字符的字体、字号、颜色;外蕴状态(extrinsic state)是不可共享的、随上下文变化的,比如字符在文本中的位置、行高、是否被选中。C# 中实现享元的关键不是“怎么写一个 Flyweight 类”,而是「哪些状态必须由客户端传入,哪些可以缓存在享元内部」。
常见错误是把本该由客户端管理的位置、索引、上下文 ID 等塞进享元对象,导致缓存失效或线程不安全。正确做法是:享元类只保留 char、FontFamily、FontSize 这类只读属性,其余全靠 Render(int x, int y, bool isSelected) 这类方法接收外蕴参数。
用 Dictionary 做享元工厂最稳妥
字符串拼接作为键比用结构体或自定义类更轻量、更易调试,也天然支持多线程读取(Dictionary 的读操作是线程安全的)。键格式建议统一为 "{Char}_{FontSize}_{FontName}",避免因空格、大小写、单位(pt/em)混用导致重复创建。
- 不要用
ConcurrentDictionary—— 初始化阶段加锁一次就够了,后续全是读,没必要为写优化 - 键里不要包含颜色值(如 RGB 元组),容易因浮点精度或 Alpha 通道引发不命中;颜色应归入外蕴状态,由渲染逻辑处理
- 工厂的
GetFlyweight(char c, float size, string family)方法应返回接口ICharFlyweight,而非具体类,便于后期替换实现(比如加入 GPU 字形缓存)
注意 .NET 中值类型与引用类型的共享陷阱
如果享元内部持有 Font 或 Brush 这类 GDI+ 对象,它们本身不是线程安全的,且不能跨 Graphics 上下文共享。此时享元不能直接缓存 Font 实例,而应缓存构造参数(fontFamilyName, emSize, style),在每次 Draw() 时按需新建 —— 虽然略增开销,但避免了 GDI 句柄泄漏和跨线程访问异常。
另一个典型问题是误用 struct 实现享元:结构体拷贝会破坏共享语义,且无法实现接口多态。所有享元类必须是 class,且构造函数私有。
示例键生成逻辑:
var key = $"{c}_{size:F1}_{family?.Replace(" ", "_") ?? "Default"}";
享元不是万能的,别在小规模场景强行套用
当对象总数
真正适合享元的场景很明确:富文本编辑器里成千上万个字符、游戏地图中海量相同地形砖块、日志系统中重复的错误码枚举实例。验证是否值得用,最简单的方法是用 dotMemory 对比前后堆内存中同类对象实例数 —— 如果 CharRenderer 实例从 50,000 降到 200,才说明模式生效。
最容易被忽略的一点:享元工厂本身要控制生命周期。若长期运行的服务中反复创建/丢弃工厂,缓存就形同虚设。它通常应是静态单例,或注册为 Singleton 服务,且不依赖任何请求作用域对象。










