首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 設計模式深度解析:享元模式 記憶體優化的關鍵模式

     編輯:關於C#

前言

享元模式 是結構型設計模式中專門解決「大量細粒度物件記憶體壓力」的模式。其核心思想:**區分內部狀態 (可共享) 與外部狀態 (不可共享),透過共享內部狀態大幅減少物件數量**。適用於文字編輯器字元、圖形編輯器圖元、遊戲粒子系統、連線池、執行緒池等大量相似物件場景。

核心概念:內部狀態 vs 外部狀態

特性內部狀態外部狀態
定義物件固有、不隨環境改變的資料隨環境、情境變化的資料
共享性可跨物件共享不可共享,由客戶端管理
儲存位置享元物件內部客戶端傳入方法參數
範例 (文字編輯器)字元碼、字型、字體大小位置 (x,y)、顏色、旋轉角度
範例 (圖形編輯器)圖形類型、幾何定義座標、縮放、顏色、圖層

單純享元模式結構


// 享元介面
public interface IFlyweight
{
    void Operation(int extrinsicState); // 外部狀態由參數傳入
}

// 具體享元:儲存內部狀態
public class ConcreteFlyweight : IFlyweight
{
    private readonly string _intrinsicState; // 內部狀態 (唯讀、共享)

    public ConcreteFlyweight(string intrinsicState)
    {
        _intrinsicState = intrinsicState;
    }

    public void Operation(int extrinsicState)
    {
        Console.WriteLine($"ConcreteFlyweight: 內部狀態={_intrinsicState}, 外部狀態={extrinsicState}");
    }
}

// 享元工廠:管理享元池,確保共享
public class FlyweightFactory
{
    private readonly Dictionary _flyweights = new();

    public IFlyweight GetFlyweight(string key)
    {
        if (!_flyweights.TryGetValue(key, out var flyweight))
        {
            flyweight = new ConcreteFlyweight(key);
            _flyweights[key] = flyweight;
            Console.WriteLine($"[Factory] 建立新享元: {key}");
        }
        else
        {
            Console.WriteLine($"[Factory] 重用享元: {key}");
        }
        return flyweight;
    }

    public int Count => _flyweights.Count;
}

// 客戶端使用
var factory = new FlyweightFactory();
IFlyweight fx = factory.GetFlyweight("A");
fx.Operation(1); // 外部狀態由客戶端提供
IFlyweight fy = factory.GetFlyweight("B");
fy.Operation(2);
IFlyweight fz = factory.GetFlyweight("A"); // 重用 "A"
fz.Operation(3);
Console.WriteLine($"總享元數: {factory.Count}"); // 輸出 2

實戰案例 1:文字編輯器字元共享

問題

一份 10 萬字的文件,若每個字元都是獨立物件,將建立 10 萬個物件。實際上只有約 100 個不同字元 (ASCII/常用中文),其餘差異僅在「位置」與「樣式」。

享元實作


// 內部狀態:字元碼、字型家族
public record CharacterIntrinsic(char Char, string FontFamily, float FontSize);

// 外部狀態:位置、顏色、樣式
public record CharacterExtrinsic(int X, int Y, ConsoleColor Color, bool Bold, bool Italic);

// 享元介面
public interface ICharacterFlyweight
{
    void Render(CharacterExtrinsic extrinsic);
}

// 具體享元
public class CharacterFlyweight : ICharacterFlyweight
{
    private readonly CharacterIntrinsic _intrinsic;

    public CharacterFlyweight(CharacterIntrinsic intrinsic)
    {
        _intrinsic = intrinsic;
    }

    public void Render(CharacterExtrinsic extrinsic)
    {
        // 實際渲染邏輯:使用 _intrinsic.Char, _intrinsic.FontFamily 等
        // 結合 extrinsic.X, extrinsic.Y, extrinsic.Color 繪製
        Console.WriteLine($"渲染字元: '{_intrinsic.Char}' at ({extrinsic.X},{extrinsic.Y}) " +
                         $"Font={_intrinsic.FontFamily} Size={_intrinsic.FontSize} " +
                         $"Color={extrinsic.Color} Bold={extrinsic.Bold}");
    }
}

// 享元工廠
public class CharacterFactory
{
    private readonly Dictionary _pool = new();

    public ICharacterFlyweight GetCharacter(char c, string fontFamily = "Consolas", float fontSize = 12f)
    {
        var key = new CharacterIntrinsic(c, fontFamily, fontSize);
        return _pool.GetValueOrDefault(key) ?? (_pool[key] = new CharacterFlyweight(key));
    }

    public int PoolSize => _pool.Count;
}

// 文件模型:只儲存外部狀態序列
public class Document
{
    private readonly List<(ICharacterFlyweight Flyweight, CharacterExtrinsic Extrinsic)> _chars = new();
    private readonly CharacterFactory _factory = new();

    public void AddChar(char c, int x, int y, ConsoleColor color = ConsoleColor.White)
    {
        var flyweight = _factory.GetCharacter(c);
        var extrinsic = new CharacterExtrinsic(x, y, color, false, false);
        _chars.Add((flyweight, extrinsic));
    }

    public void Render()
    {
        foreach (var (fw, ext) in _chars)
            fw.Render(ext);
    }

    public void Stats()
    {
        Console.WriteLine($"文件字元數: {_chars.Count}, 享元池大小: {_factory.PoolSize}");
        Console.WriteLine($"記憶體節省: ~{(_chars.Count - _factory.PoolSize) * 100 / _chars.Count}%");
    }
}

// 使用
var doc = new Document();
string text = "Hello Flyweight Pattern! ";
for (int i = 0; i < text.Length; i++)
    doc.AddChar(text[i], i * 10, 0);

doc.Render();
doc.Stats();
// 輸出: 文件字元數: 25, 享元池大小: 18, 記憶體節省: ~28%

實戰案例 2:咖啡店飲品系統 (原文範例改良)

咖啡攤:無外部狀態 (單純享元)


// 飲品內部狀態:名稱、配方、基礎價格
public record CoffeeIntrinsic(string Name, string Recipe, decimal BasePrice);

// 享元介面
public interface ICoffeeFlyweight
{
    void Serve(int orderNumber); // 外部狀態: 訂單號/桌號
}

// 具體享元
public class CoffeeFlyweight : ICoffeeFlyweight
{
    private readonly CoffeeIntrinsic _intrinsic;

    public CoffeeFlyweight(CoffeeIntrinsic intrinsic) => _intrinsic = intrinsic;

    public void Serve(int orderNumber)
    {
        Console.WriteLine($"[訂單 #{orderNumber}] 提供 {_intrinsic.Name} - 配方: {_intrinsic.Recipe} - 價格: ${_intrinsic.BasePrice}");
    }
}

// 工廠
public class CoffeeFactory
{
    private readonly Dictionary _coffees = new();

    public ICoffeeFlyweight GetCoffee(string name)
    {
        return _coffees.GetValueOrDefault(name) ?? (_coffees[name] = new CoffeeFlyweight(
            name switch
            {
                "Black Coffee" => new CoffeeIntrinsic("Black Coffee", "Espresso + Hot Water", 50m),
                "Cappuccino" => new CoffeeIntrinsic("Cappuccino", "Espresso + Steamed Milk + Foam", 80m),
                "Latte" => new CoffeeIntrinsic("Latte", "Espresso + Steamed Milk", 75m),
                "Espresso" => new CoffeeIntrinsic("Espresso", "Pure Espresso Shot", 60m),
                _ => throw new ArgumentException($"未知飲品: {name}")
            }));
    }

    public int VarietyCount => _coffees.Count;
}

// 客戶端
var factory = new CoffeeFactory();
int orderNo = 0;
string[] orders = { "Black Coffee", "Cappuccino", "Espresso", "Cappuccino", "Latte", "Black Coffee", "Espresso" };

foreach (string coffeeName in orders)
{
    factory.GetCoffee(coffeeName).Serve(++orderNo);
}
Console.WriteLine($"總訂單: {orderNo}, 飲品種類: {factory.VarietyCount}");
// 輸出: 總訂單: 7, 飲品種類: 4 (只建立 4 個飲品物件)

咖啡廳:有外部狀態 (桌號)


// 外部狀態:桌號、加糖、加冰、特殊要求
public record CoffeeExtrinsic(int TableNumber, bool Sugar, bool Ice, string SpecialRequest);

// 享元介面調整
public interface ICoffeeFlyweight
{
    void Serve(CoffeeExtrinsic extrinsic);
}

// 具體享元調整
public class CoffeeFlyweight : ICoffeeFlyweight
{
    private readonly CoffeeIntrinsic _intrinsic;

    public CoffeeFlyweight(CoffeeIntrinsic intrinsic) => _intrinsic = intrinsic;

    public void Serve(CoffeeExtrinsic extrinsic)
    {
        Console.WriteLine($"[桌號 {extrinsic.TableNumber}] {_intrinsic.Name} " +
                         $"(糖: {extrinsic.Sugar}, 冰: {extrinsic.Ice}) " +
                         $"{(string.IsNullOrEmpty(extrinsic.SpecialRequest) ? "" : $"備註: {extrinsic.SpecialRequest}")}");
    }
}

// 使用
var factory = new CoffeeFactory();
var table1 = new CoffeeExtrinsic(1, true, false, "少冰");
var table2 = new CoffeeExtrinsic(2, false, true, "");

factory.GetCoffee("Latte").Serve(table1);
factory.GetCoffee("Latte").Serve(table2);
// 同一個 Latte 享元服務不同桌號,外部狀態由客戶端傳入

複合享元模式

將多個單純享元組合成不可共享的複合物件,複合物件本身不共享,但內部組件共享:


// 複合享元:一行文字 (由多個字元享元組成)
public class TextLineFlyweight : IFlyweight
{
    private readonly List _characters;

    public TextLineFlyweight(IEnumerable characters)
    {
        _characters = characters.ToList();
    }

    public void Operation(int extrinsicState) // extrinsicState 可為行號、縮排等
    {
        int x = extrinsicState * 20; // 簡化:行號轉 x 座標
        foreach (var ch in _characters)
        {
            ch.Render(new CharacterExtrinsic(x, 0, ConsoleColor.White, false, false));
            x += 10; // 字元寬度
        }
    }
}

適用性判斷清單

當 **所有** 以下條件成立時,考慮使用享元模式:

  • ✅ 系統有大量相似物件 (通常 > 10,000)
  • ✅ 物件佔用大量記憶體
  • ✅ 物件狀態大部分可外部化 (內部狀態小、固定)
  • ✅ 可將物件按內部狀態分組,每組僅需一個共享實例
  • ✅ 軟體不依賴物件身分識別 (即物件可互換)
  • ✅ 願意接受額外複雜度 (工廠管理、外部狀態傳遞)

優缺點分析

優點缺點
大幅減少記憶體使用 (常達 50-90%)增加系統複雜度 (需區分內/外部狀態)
減少 GC 壓力 (物件數量少)執行期稍慢 (需查找工廠、傳遞外部狀態)
符合單一職責 (狀態分離)邏輯分散 (客戶端需管理外部狀態)
天然支援不可變物件設計除錯困難 (共享物件難以追蹤特定實例)

.NET 內建享元應用

  • 字串駐留:string.Intern() 將相同字串存入全域表,相同內容共享同一實例
  • 枚舉值:Enum 成員為單例,天然享元
  • Tuple/Record 結構相等:相同值的 ValueTuple/Record 可能被編譯器優化共享
  • WPF Freezable:凍結後可跨執行緒共享的物件 (筆刷、幾何圖形等)

現代替代方案

方案適用場景優點
Object Pool (物件池)頻繁建立/銷毀的昂貴物件 (資料庫連線、執行緒、大型緩衝區)重用實例、可控制池大小、支援非同步
Value Type (struct)小型、不可變、大量資料 (座標、顏色、矩陣)零 GC、堆疊配置、記憶體緊湊
Array/Span/Memory大量同質資料處理連續記憶體、SIMD 友善、零複製切片
String Interning大量重複字串內建、透明、跨 AppDomain

總結

享元模式是記憶體受限環境、大量細粒度物件系統的利器。其精髓在於 **「狀態分離」與「共享管理」**。現代 .NET 開發中,若面臨大量物件記憶體壓力,優先考慮:

  1. 能否改用 struct / record struct (值型別)
  2. 能否使用 ArrayPool / MemoryPool / 物件池
  3. 是否真正需要物件身分識別 (若不需要,享元模式極佳)
  4. 內部狀態是否足夠小且固定 (若是,享元模式效果顯著)

理解享元模式不僅能解決記憶體問題,更能訓練「狀態分離」的架構思維,這在雲端原生、高併發系統設計中極具價值。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved