第50章:Flyweight ②:.NET定番(stringの共有 / ArrayPool)🧵📦
ねらい 🎯✨
- Flyweight(フライウェイト)を「共有して軽くする」って感覚でつかむ🪶🙂
- .NETの“現実のFlyweightっぽさ”として string と ArrayPool
を読んで体感する👀📚 - 「どこが共有されて」「どこが共有しちゃダメか」を見抜けるようになる🔍⚠️
到達目標 ✅🌸
- Flyweightのキモ(共有できるのは“だいたい不変”)を1文で言える🗣️✨
stringで「共有されやすいもの/されないもの」の違いを説明できる🧵🙂ArrayPool<T>.Sharedの基本形(Rent→使う→Return)を迷わず書ける📦🔁- “共有の副作用”の代表例(可変・クリア漏れ・サイズ勘違い)を避けられる🧯😵💫
手順 🧭🧩
1) Flyweightを一言で言う🪶
-
「同じものは共有して使い回し、メモリとGCの負担を下げる」 だよ🙂✨
-
ただし、共有できるのはだいたい 不変(immutable) なものが中心⚠️
- 共有してるのに中身を変えたら、全員に影響しちゃうからね😱💥
2) string の“共有っぽさ”を読む🧵👀
A. 文字列リテラルは「メタデータからロードされる」📌
- C#の
"hello"みたいな 文字列リテラル は、IL的にはldstr命令で読み込まれるよ📦 ldstrは「アセンブリのメタデータにある文字列リテラル」を扱う命令として説明されてるよ📚(Microsoft Learn)
ここで言いたいことはシンプルで、同じ文字列が大量に出る世界では「共有(または共有に近い挙動)」が効いてくる、って感覚を持つこと🙂🪶
B. 「共有していい文字列」と「共有が危ない文字列」⚖️
-
共有して嬉しい例:
- 商品カテゴリ名、ステータス名、固定のキー(
"Paid","Shipped"みたいな)🏷️✨
- 商品カテゴリ名、ステータス名、固定のキー(
-
共有が危ない(または意味が薄い)例:
- ユーザー入力、ログ本文、1回しか出ない長文🧾💦
- 共有の管理コストの方が高いことが多い😵
3) ArrayPool<T> は“バッファのFlyweight”📦🪶

A. 何が嬉しい?(超ざっくり)🙂
byte[]とかchar[]を処理のたびにnewすると、大量確保→GC がしんどくなる😵💫🗑️- そこで 配列をプール(使い回し) して、割り当てを減らすのが
ArrayPool<T>📦✨
B. .NET公式でも「ArrayPool.Shared を使って割り当て回避」って文脈で出てくる📚
- たとえば
ReadOnlySequence<T>を連続バッファとして扱いたいとき、ToArray()はヒープに新規配列を作るのでホットパスでは避けたい →ArrayPool<T>.Sharedでコピーして割り当て回避、みたいな説明があるよ🧠✨(Microsoft Learn)
C. ASP.NET Coreの例でも “プールした配列を借りて返す” が普通に出てくる🌐📦
- ミドルウェア例で
ArrayPool<byte>.Shared.Rent(...)→ 使う →Return(...)の形が出てくるよ🔁(Microsoft Learn)
4) ArrayPoolの“基本の型”を覚える(これだけでOK)🧠✅
合言葉:Rent → 使う(実長で!)→ finally で Return 🔁✨
-
Rent(minLength)- 返ってくる配列は minLength以上(ピッタリとは限らない)📏🙂
-
“実際に使った長さ”は 自分で管理 する(ここ超重要!)🧷⚠️
-
Return(array, clearArray: true/false)- 秘密データ(トークン、個人情報など)が入るなら
clearArray: trueが安心🧼🔐
- 秘密データ(トークン、個人情報など)が入るなら
落とし穴 ⚠️😵💫
-
共有対象が可変で事故る(Flyweight最大の地雷)💣
- 共有した配列・共有した参照の中身を変えたら、別の処理が壊れる😱
-
ArrayPool<T>で 「借りた配列は自分のもの」だと思い込む- 返し忘れ(Return漏れ)→ プールが効かない → 逆に遅い😵
- 2回返す(ダブルReturn)→ だいたい地獄👹
-
配列サイズの勘違い
Rent(1024)したら 2048 が来ることもある → 末尾はゴミが残ってるかも🗑️- “実長”を無視して全部処理するとバグる😵💫
-
クリア漏れ
- 秘密が入ったバッファを
clearArray:falseで返すと、別の処理に残骸が混ざる可能性😨🔐
- 秘密が入ったバッファを
演習 🏃♀️💨🧪
演習1:string の“共有感”をテストで確認🧵🔍
- MSTestで 参照が同じか を見てみよう🙂✨
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace DesignPatterns.Tests;
[TestClass]
public class FlyweightStringTests
{
[TestMethod]
public void String_Sharing_Basics()
{
// 同じリテラル
string a = "hello";
string b = "hello";
// 参照が同じになりやすい(=共有っぽい挙動が起きることが多い)
Assert.IsTrue(object.ReferenceEquals(a, b));
// new string は別インスタンスになりやすい
string c = new string(new[] { 'h', 'e', 'l', 'l', 'o' });
Assert.IsFalse(object.ReferenceEquals(a, c));
// Internで共有側に寄せる(※使いどころは慎重に!)
string d = string.Intern(c);
Assert.IsTrue(object.ReferenceEquals(a, d));
}
}
✅ポイント
ReferenceEqualsが true/false になる理由を、自分の言葉でメモしてね📝🙂
演習2:ArrayPool<byte> で“使い回しバッファ”を体験📦🔁
- ストリームを最後まで読む処理を、
ArrayPool<byte>で書いてみよう🌊✨ - 目的は「毎回
new byte[]しない」ことだよ🙂
using System.Buffers;
public static class PooledIo
{
public static async Task<byte[]> ReadAllBytesPooledAsync(
Stream stream,
CancellationToken ct = default)
{
const int BufferSize = 16 * 1024;
var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(BufferSize);
try
{
using var ms = new MemoryStream();
while (true)
{
int read = await stream.ReadAsync(buffer.AsMemory(0, BufferSize), ct);
if (read == 0) break;
ms.Write(buffer, 0, read);
}
// 最終結果は「必要な分だけ」新規配列にする(戻り値が扱いやすい)
return ms.ToArray();
}
finally
{
// 重要:必ず返す! 秘密データが混ざる可能性があるなら clearArray:true
pool.Return(buffer, clearArray: true);
}
}
}
テスト例(軽くでOK)🧪🙂
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Text;
namespace DesignPatterns.Tests;
[TestClass]
public class FlyweightArrayPoolTests
{
[TestMethod]
public async Task ReadAllBytesPooledAsync_ReadsAll()
{
string text = new string('a', 100_000);
byte[] src = Encoding.UTF8.GetBytes(text);
using var stream = new MemoryStream(src);
byte[] result = await PooledIo.ReadAllBytesPooledAsync(stream);
Assert.AreEqual(src.Length, result.Length);
CollectionAssert.AreEqual(src, result);
}
}
✅ポイント
finallyでReturnしてる?(ここ命!)🔁⚠️readの長さだけWriteしてる?(実長管理!)📏✅
チェック ✅📌
- Flyweightを「共有して軽くする」って説明できた?🪶🙂
- 共有していい条件を言えた?(だいたい“不変”!)🧊✅
ArrayPool<T>の基本形を暗記せずに書けた?(Rent→try/finally→Return)📦🔁Rentの配列が “ピッタリサイズじゃない” 前提で扱えてる?📏⚠️clearArray:trueを使う場面(秘密データ)を言えた?🧼🔐