コンテンツにスキップ

クラスと関数の使い分け

扱うこと: PowerShell 5.1 でクラスを使ってよいところ・使ってはいけないところ(試した結果と決まり)、目的ごとのクラス設計(作ったクラスの置き場所と役目)とそのつなぎ方。扱わないこと: スレッド・プロセスの一覧そのもの(プロセスとスレッド)。先に読むページ: プロセスとスレッド。

クラスと関数の使い分け

PowerShell 5.1 のクラスのメソッドは、別のランスペースへ渡したインスタンスから呼んでも、作った側(定義したランスペース)の関数を呼ぶ(試し c)。作った側のランスペースが同時に実行中のときにどう動くかは試していない。次の 3 つを Windows で試し、その結果をもとに使い分けを決める。

試したこと(環境: Windows 11、PowerShell 5.1.26100.9549、main 79065aa、2026-09-26。個人のパスは書かない)

  • a. クラスのメソッドから、同じランスペースのスクリプトの関数・$script: 変数が見えるか。ランスペースの無い .NET のスレッドからクラスのメソッドを呼べるか
  • b. 関数の呼び出し・インスタンスのメソッドの呼び出し・静的メソッドの呼び出し・$script: 変数の読み・クラスのプロパティの読み・hashtable の読みの、1 回あたりの速さ(下の「速さ」)
  • c. CreateRunspace で作ったランスペースの中でクラスを dot-source して使えるか。別のランスペースへ渡したインスタンスのメソッドは、どちらのランスペースの関数を呼ぶか
  • d.(#147 で追加)Pester の BeforeAll で lib.ps1 を dot-source したとき、テストファイルのトップレベルで定義したクラスのメソッドから、lib.ps1 の関数と $script: 変数が見えるか
flowchart TD
    A["別のランスペースへ<br>インスタンスを渡す"] --> B{"そのインスタンスの<br>メソッドを呼ぶ"}
    B --> C["作った側(定義した)<br>ランスペースの関数が呼ばれる"]
    C --> D["受け取った側の関数・$script: 変数は見えない"]

決まり

決まり 理由(試した結果)
クラスは、使うランスペースの中で定義のファイルを dot-source して作り、そのランスペースの中だけで使う CreateRunspace で作ったランスペースの中で dot-source したクラスは使えた(c)
クラスのインスタンスを別のスレッド(別のランスペース)へ渡さない。別のスレッドには素の値(例 Workspace の Dir の文字列)を渡し、そこで作り直す 渡したオブジェクトのメソッドは、作った側のランスペースの関数を呼んだ。作った側が実行中のときに別スレッドから呼んだときの動きは試していない(c)。また、同じ名前のクラスでも dot-source したランスペースごとに別の型になるとみられ、型付きの引数や -is が合わないおそれがある(試していない)
スレッドをまたいで使うもの(受け渡しの口・取り込みの結果・キャッシュ)は、今までどおり .NET のコレクション(ConcurrentQueue・ConcurrentDictionary・[hashtable]::Synchronized など)と hashtable・関数で作る。クラスはそれを持って読み書きしてよい 同上
クラスのメソッドから使ってよい関数・変数は、スクリプトのトップのスコープ(または global)で dot-source したファイルの関数と $script: 変数だけにする(関数の定義がクラスより前でも後でもよい) 見えた(a)。試したのはスクリプトのトップで dot-source した場合だけ。PS のクラスのメソッドは、呼んだ側ではなく定義したスコープから関数を引くので、関数やスクリプトブロックの中(Pester の BeforeAll・It を含む)で定義した関数・変数、Mock で差し替えた関数はメソッドから見えないことがある(tests/tebunko/indexer/extract_office.Tests.ps1 の先頭のコメントはこの場合に当たる)。関数の中で dot-source した場合・テストでの差し替えは試していない
クラスのメソッドの本体で、$script: 変数(${stateDone} などの定数)を直接読まない。 必要なら、その値を(司令などの普通の関数から)引数かコンストラクタで渡す。関数の呼び出し(引数の既定値が $script: 変数を読むものを含む)はそのまま使ってよい Pester の BeforeAll で . lib.ps1 した場合、テストファイルのトップレベルのクラスのメソッドから関数は呼べたが、$script: 変数の読みは空になった($script:stateDone と書いても、BeforeAll の外の $stateDone とは値が食い違う。d)。関数の呼び出しなら、その関数自身のスコープ(呼んだ側ではなく定義側)で $script: 変数を読むため問題ない(newStatusRow の既定値 ${stateNew} は読めた)。本番(indexer.ps1 がトップレベルで indexer_lib.ps1 を dot-source する経路)では問題にならないと見られるが、テストと本番で動きが食い違う原因になるため、クラスの本体では読まない決まりにする
テストで差し替えたいもの(ファイル・COM・時刻などの I/O)は、メソッドの中で関数を呼んで Mock で差し替える形にせず、コンストラクタの引数かプロパティで受け取る(スクリプトブロックや部品のオブジェクト) 上の行のとおり、メソッドの中の関数は Mock で差し替えられるとは限らない
ランスペースの無い .NET のスレッド([System.Threading.Thread] に渡したデリゲート・System.Threading.Timer のコールバックなど)から、クラスのメソッドやスクリプトブロックを呼ばない try/catch でも捕まらずにプロセスごと終わった(a。クラスとデリゲートのどちらが原因かは切り分けていない。今のコードにこの使い方は無い)
引数なしで作れるクラスは、既定のコンストラクタを明示する tests/meta/classes.Tests.ps1(既にある決まり。ここから参照する)
メソッドの中のローカル変数の名前は、プロパティの名前と大文字・小文字だけの違いにしない($Channel プロパティがあれば $channel をローカル変数に使わない) PS のクラスは、プロパティと同名(大文字・小文字を区別しない)のローカル変数への代入を、$this. を付けないプロパティへの代入と見なし、パースエラーになる(Cannot assign property, use '$this.Xxx'.。#147 で見つかった)
メソッドのパラメータに既定値($x = 60 など)を書いても、呼ぶ側が省略するとエラーになる(関数と違い、既定値は使われない) [T]::new().M(1) が Cannot find an overload になった(#147 で確かめた)。呼ぶ側で必ず全部の引数を渡す
スレッドやプールの寿命を持つクラスは Open(または最初の使用)でスレッドを作り、Close で止めて片づける。Close は何度呼んでもよい 今の決まりのまま

今のクラスと使う場所

クラス 作って使うスレッド
SearchService・BackgroundQueue・IndexingSession 画面のスレッド
WorkerPool プールを持つ側のスレッドで作り、そのスレッドから使う: 画面のスレッド(BackgroundQueue の中。shared/core/worker_pool.ps1)・検索の司令のスレッド(newContentIndexWorkerPool。tebunko/search/content_index_search.ps1)・インデクサの司令のスレッド(getFilesGramValues。tebunko/index/gram_index_store.ps1)
Workspace(tebunko/core/workspace.ps1) lib.ps1 を読む各ランスペース(画面・インデクサの司令・取り込み・検索のスレッド)で、それぞれ作る。別のスレッドへは Dir の文字列を渡し、そこで作り直す(workspace.ps1 の先頭のコメント・core/paths.ps1・indexer/indexer_run.ps1・search/search_run.ps1)。新しい決まり「作ったランスペースで使い、渡さない」の先例として書く

ZipSizeLimitException(System.Exception を継承。shared/office/office_reader.ps1)は、上の表の「作って使うスレッド」の対象外(値を持たせて投げる・捕まえるだけの例外であり、インスタンスを保って使い回さない)。readZipEntry が ZIP の部品・1 ファイルの合計のサイズの上限を超えたときに投げ、部品名(PartName)・大きさ(MeasuredBytes)・部品ごとか合計か(LimitKind)を持つ。.Message は画面・取り込み一覧向けの簡潔な文言のままとし、この 3 つのプロパティは呼び出し元(extract_office.ps1・indexer_run.ps1)が writeZipSizeLimitLog(indexer/indexer_state.ps1)でインデックス作成のログにだけ書く(細工した Office ファイルから身を守ること)。

refactor で作るクラス(IndexCatalog など)は下の「目的ごとのクラス」へ。

画面層のクラス(shared/ui/types.ps1 の NotifyBase・ConfirmFact、tebunko/ui/ の search/result_types.ps1・search/target_types.ps1・index/index_types.ps1 の WPF に渡す型)は、上の表と決まりの外(画面のスレッドだけで使う)。

速さ(1 回あたりの時間。10 万回 × 3 回の中央値、空ループ 0.64μs を含む)

操作 時間
関数の呼び出し 40.5μs(ばらつきがあり、初回が大きい)
インスタンスのメソッドの呼び出し 4.33μs
静的メソッドの呼び出し 3.56μs
$script: 変数の読み 0.63μs
クラスのプロパティの読み 0.86μs
hashtable の読み 1.06μs
  • 読み取り方: 変数・プロパティ・hashtable の読みは、どれも空ループ(0.64μs)と区別できないほど小さく、差は無いものとみる。比べるのは関数とメソッドの呼び出しの差だけで、メソッドは関数の約 10 分の 1。クラスにしても呼び出しは遅くならない。 ただしメソッドの中から関数を呼べば関数の時間がそのまま足される。取り込みの 1 ファイルごとに通る経路(IngestDispatcher・OfficeWatchdog)では、書き直しで関数の呼び出しの段を増やさない。1 ファイル 1 回増えると 1,000 ファイルで約 40ms で、1 ファイルの取り込み(Office を使えば数百 ms 以上)に比べれば小さいが、段を重ねると積み上がるため。
  • 性能を落としていないかの判断は、この表ではなく取り込みの計測(性能とリソースの計測のしかた の「計測の口」)で、同じ日に main と比べて行う。

別スレッドが読むコードに、画面のクラスを含めない

別スレッド(検索・背景の仕事・インデックス作成の取り込み)は、getPartLoad(scripts/tebunko/core/parts.ps1)が返す部品(lib・indexerLib)だけを読み込む。この部品には、画面(ui\ 配下)のクラス(SearchTarget・IndexNode など、画面の部品を持つもの)を入れない。

  • 単一 .ps1 版では、部品の文字列を関数(importTebunkoPart)として別のランスペースに登録し、そこで構文解析する。画面のクラスが混ざると、画面のスレッドのクラスと別の型として定義され、-is の比較や型の指定が食い違う。
  • 同じ名前のクラスを別のランスペースで使うときの食い違いは、tests/tebunko/core/parts.Tests.ps1(同じランスペースの使い回し・別のランスペースの取り違え・2 スレッドが 1 つの State を共有する場合)が確かめる。
  • 部品に入れるファイルの範囲は、tests/meta/structure.Tests.ps1 と tools/new_single_script.ps1 の検査が、読み込み口(lib.ps1・indexer_lib.ps1・indexer_main.ps1)からたどれる範囲と一致するかで確かめる。

目的ごとのクラス

目的ごとに分けたクラスの設計。クラスを足す・変えるときは、この節を実装に合わせて直す。StatusLedger・PendingPublish・IndexingReporter は実装ずみ(#147)。OfficeWatchdog・IndexCatalog・WorkspaceMover も実装ずみ。IngestPlanner は名前の保存を IndexCatalog に任せる形に直した。IngestDispatcher も実装ずみ。

classDiagram
    class IndexCatalog
    class WorkspaceMover
    class StatusLedger
    class PendingPublish
    class IndexingReporter
    class IngestPlanner
    class OfficeWatchdog
    class IngestDispatcher
    IndexCatalog ..> StatusLedger : 取り込み一覧を通して書く
    note for IndexCatalog "使う側(画面・司令)の入口が\nほかの7つとつなぐ。互いは呼ばない"
クラス 目的(1 つに絞る。凝集) 作って使うスレッド 層 置くファイル 今ある場所 依存してよい相手
IndexCatalog インデックスの追加・改名・削除・名前の割り当て。保存先(targetFolders・取り込み一覧・元のフォルダの記録・searchExcludes・store\gram\)のうち、改名・削除が書き換えるものの手順と順番、途中で失敗したときの扱い(止める・残す)だけを持つ(一覧 targetFolders と元のフォルダの記録の書き直しは、画面が SaveTargets・WriteSourceFile で呼ぶ)。保存先の形式は知らない。Workspace と設定ファイルの場所を受け取って作る(場所を暗黙に使わない) 画面のスレッド。別のスレッドの仕事の中でも、ワークスペースの場所(文字列)から newIndexCatalog で作り直して使う。インデクサが名前を保存するときは、司令のスレッドで別に作る 状態層 tebunko/index/index_catalog.ps1(lib.ps1) 実装ずみ。RenameIndex・RenameIndexWithSource(改名のあと元のフォルダの記録を書き直す。記録だけ失敗しても改名は成功で、理由を返す)・RemoveIndex・RemoveIndexes(まとめて削除。1 つ失敗しても続ける)・AssignAndSaveNames・StoreAssignedNames(インデクサ用)・SaveTargets(targetFolders の保存)・WriteSourceFile(元のフォルダの記録)・AssertReachable(ワークスペースに届くかの確認)。作り方は newIndexCatalog(取り込み一覧を書く StatusLedger も作って渡す)。呼ぶ側は ui/index/index_store.ps1 の applyIndexEdit・deleteIndexes、ui/index/index_list.ps1 の loadTargets、indexer/indexer_run.ps1。改名・削除の手順は、index_store.ps1 の renameIndex・removeIndex・removeIndexes だったもの 保存先ごとの読み書きの部品(既存の関数でよい。設定は invokeSettingsLocked・saveAssignedIndexNames・writeTargetFolders、元のフォルダの記録・2-gram の索引は、その読み書きの関数)、searchExcludes の読み書きの部品(core/search_excludes.ps1 の readSearchExcludes・writeSearchExcludes・removeSearchExcludesUnder。WorkspaceMover と同じものを通す)、取り込み一覧は作る側から渡された StatusLedger、index_name.ps1(判断層)、Workspace
WorkspaceMover ワークスペースの切り替え(移す・searchExcludes の付け替え・保存・失敗したら戻す) 確かめは画面のスレッド、ChangeWorkspace は裏のスレッド 状態層 tebunko/core/workspace_mover.ps1(lib.ps1) 実装ずみ。ListEntries(tebunko のファイル・フォルダ)・Conflicts(移し先にある同じ名前)・ChangeWorkspace("use"・"move"・"reset" のどれかで切り替え、@{ Ok; Stage; Message; Targets; Count } を返す)・MoveEntries・RemoveEntries・UseTargets。呼ぶ側は ui/settings/settings.ps1 の startWorkspaceMove(applyWorkspace が確かめたあとに呼ぶ)。core/workspace.ps1 の getWorkspaceEntries・getWorkspaceMoveConflicts・moveWorkspace・moveWorkspaceEntry・copyDirectoryTree・removeWorkspaceEntries・useWorkspaceTargets だったもの 設定の読み書きの部品(invokeSettingsLocked)、searchExcludes の読み書きの部品(core/search_excludes.ps1 の moveSearchExcludes。IndexCatalog と同じ searchExcludes の部品を通す)、ファイルの移動の部品(fs.ps1)、Workspace
StatusLedger 取り込み一覧・今回の失敗と消えたファイルの記録。改名・削除の書き換えもラップする。列と状態の定義(core/paths.ps1)は判断層・index_store.ps1 なども読むため動かさない インデクサの司令のスレッド。画面が取り込み一覧を読むときは画面のスレッドで別に作る 状態層 tebunko/indexer/indexer_state.ps1(lib.ps1) 実装ずみ。ReadStatus・WriteStatus・AddRow・ReadRelPaths・RenameIndexName・RemoveIndexName・Failures(プロパティ)・DroppedRows(プロパティ)。getIndexNameMap(core/status_file.ps1)・getIndexStats(index_store.ps1)は読むだけの部品として外に残す 取り込み一覧・状態ファイルの読み書きの部品(core/status_file.ps1 の関数と、indexer_state.ps1 の addStatusRow)、コンストラクタで受け取った Workspace(場所を暗黙に使わない)
PendingPublish フォルダごとの取り込み中の数(Busy)とまだ渡していない数(Pending)を数え、どちらも 0 になったフォルダを本文インデックスに書き出してよいと決める 司令のスレッド 状態層 tebunko/indexer/pending_publish.ps1(indexer_lib.ps1) 実装ずみ。Add・MarkFolder(書き出し待ちの記録)・Restore・MarkPublished(外し終えていない削除 Unpublished)・AddPending・Dispatch・Skip・Complete(数える)・TakeFlushable・TakeAll(取り出す) ファイルの相対パスからフォルダを決める getBookDir(indexer_plan.ps1)。書き出し(publishIndexFolders)は TakeFlushable / TakeAll が返した結果を見て司令(invokeIndexerBody の中の flushPending)が呼ぶ
IndexingReporter 進み具合の書き込みと、画面の確認を待つこと。受け渡しの口(hashtable のまま)を持って書く 司令のスレッド 状態層 tebunko/indexer/indexing_reporter.ps1(indexer_lib.ps1) 実装ずみ。Progress・WaitForApproval(waitForIndexingApproval だった処理)。段階(${indexingPhase*})はクラスの本体で読まず、呼び出し元(司令)から引数で受け取る。ログ(writeIndexerLog・$script:indexerLog)は部品のまま、IndexingReporter には入れない 受け渡しの口(hashtable)、ログの書き込みの部品
IngestPlanner 対象フォルダ・名前・クロール・前回失敗・強制終了の回数から、取り込む順番を決める。取り込み直すかの判断は indexer_decide.ps1(判断層の関数)のまま呼ぶ 司令のスレッド 状態層 tebunko/indexer/indexer_plan.ps1(indexer_lib.ps1) 実装ずみ。AssignNames・SaveNames(インデックス名の割り当てと保存)・SelectOnly・Crawl・KeepForCancel・ApplyFailed・ApplyInterrupted・FolderByName。計画の種類・状態の値(${planKind*}・${stateFailed})はクラスの本体で読まず、コンストラクタの kinds(4 つのキーが無ければ例外)で受け取る。名前の保存は、司令が渡すスクリプトブロック(SaveNames の save。中身は IndexCatalog の StoreAssignedNames)で行い、設定ファイルの場所はこのクラスが持たない。メソッドは AssignNames → SaveNames → SelectOnly → Crawl → KeepForCancel か ApplyFailed → ApplyInterrupted → FolderByName の順に呼ぶ。進み具合は、司令が渡すスクリプトブロック(Crawl の onFolder)で出す(IndexingReporter を直接呼ばない) indexer_decide.ps1(判断層)、クロールの部品。前回の失敗・強制終了の回数は、司令が StatusLedger から取り出したデータで受け取る
OfficeWatchdog Office の制限時間を見張り、止まったら止める(取り込み中に利用者が窓を開いた PowerPoint は、止めるかの決め方 getWatchdogVerdict で止めずに残し、ワーカーを諦めたと司令に伝える)。見張りのスレッド(Start が [PowerShell]::Create() で作る別のランスペース)へは、今のまま [hashtable]::Synchronized(State)を渡す(クラスにしない)。Office が使えなくなったこと($script:officeUnavailable)はツールの判断なので、クラスには入れず今の場所に残す 読み込んだランスペースごとに 1 つ($script:officeWatchdog)。見張りのスレッドを始める(Start)のは、取り込みのワーカーと、スレッドを分けないときの指揮役だけ 状態層(shared/) shared/office/office_app.ps1(indexer_lib.ps1 から今と同じく読む)。実装ずみ。State(Deadline・Pids・TimedOut・Stop・Users・Abandon・WindowProbe・Cancel・Cancelled。Cancel は受け渡しの口を渡す入れ物で、Stop が立つと制限時間を待たずに自分のアプリを止めて Cancelled を立てる)・Start・Stop・UpdateWatchedPids・IsRunning。Start は Stop するまで(スレッドの参照がある間)は何もせず、Stop のあとは再び Start できる(何度呼んでも安全)。インスタンスは、office_app.ps1 を読み込むときにランスペースごとに $script:officeWatchdog として 1 つ作る(どのワーカーも State を読み書きするため)。どのツールからも使う Office の部品なので shared/ に置き、ツールを知らない 実装ずみ。ingestWorkerScript は Start / Stop を呼ぶだけ。$script:officeUnavailable は tebunko/indexer/extract_office.ps1・indexer_run.ps1 shared/office/ の部品(office_process.ps1 など)だけ。ツールのものに依存しない
IngestDispatcher ワーカー・先読み・取り込み中の管理(性能に効く経路)。取り込みのスレッドとは今までどおり hashtable と .NET のコレクションで受け渡す 司令のスレッド 状態層 tebunko/indexer/ingest_dispatcher.ps1(indexer_lib.ps1) 実装ずみ。Start・BeginPass・DispatchNext・Submit・SetInlineResult・Receive・TakeAbandonedWorker・StopIfRequested・InflightCount・GetInflightCount・Stop 取り込みのスレッド(ワーカーごとに CreateRunspace で作る STA のランスペース。Start・Submit が作る)と BlockingCollection、取り込みのスレッドのスクリプト。WorkerPool には寄せない(今の形を変えない)。終わった取り込みの結果は hashtable で司令に返す

クラスのつなぎ方

  • 目的ごとのクラス(この表の 8 つ)どうしは、互いを直接呼ばない。 8 つを作ってつなぐのは、使う側の入口だけにする(インデクサでは司令役の invokeIndexerBody、画面では呼ぶ側の applyIndexEdit など、取り込みのスレッドでは ingestWorkerScript)。8 つの間はデータ(hashtable・PSCustomObject)で受け渡す。8 つは、ほかの 7 つを自分で作らない・探さない。
  • この 8 つの間の例外は 1 つだけにする: IndexCatalog は取り込み一覧を StatusLedger を通して書く。その StatusLedger は、IndexCatalog を作る側(画面・司令)が作ってコンストラクタで渡す(依存の向きは IndexCatalog → StatusLedger の一方向)。
  • 8 つのほかのクラスは次のように扱う。値だけを持つクラス(Workspace)は、受け取って使ってよい。基盤のクラス(WorkerPool など。今の BackgroundQueue が中で WorkerPool を作るように)は、受け取って使うか、持ち主として作って片づけてよい。
  • searchExcludes を書き換えるのは IndexCatalog(改名・削除のとき)と WorkspaceMover(切り替えのとき)の 2 つ。どちらも同じ searchExcludes の読み書きの部品(core/search_excludes.ps1 の readSearchExcludes・writeSearchExcludes・removeSearchExcludesUnder)を通す(インデックスの改名・削除で searchExcludes の記録が残った不具合(#125)は、書き換える保存先の一覧が散っていて漏れたことが原因。2 か所で別の書き方をしない)。
  • 画面は IndexCatalog・WorkspaceMover を呼ぶだけにし、「呼ぶ → 結果を出す」にする(applyIndexEdit などの手順を画面に残さない)。
  • IndexCatalog は保存先の形式を直接知らない。 保存先ごとの読み書きは小さな部品(既存の関数でよい)に任せ、IndexCatalog は書き換える手順と順番、途中で失敗したときの扱いだけを持つ。
  • 取り込みの 1 ファイルごとに司令を通るのは、IngestDispatcher が返した結果(hashtable)を StatusLedger・PendingPublish・IndexingReporter のメソッドに渡すところだけにする。増えるのはメソッドの呼び出しで、関数の呼び出しの段は増やさない(上の「速さ」のとおり)。

置いたファイルを変えたときは、この表と ソースの分け方 の表・図を同じ PR で直す。

refactor で守ること

  • スレッドをまたいでクラスのインスタンスを渡さない(上の「クラスと関数の使い分け」の決まり)。
  • 計測の口を変えない。 変えるなら先に計測を直し、main で測り直す。口の一覧は 性能とリソースの計測のしかた の「計測の口」の表を正とする(StatusLedger の列と状態、IndexingReporter の段階の値は、この口に含まれる。名前と値は変えない)。
  • 設定ファイルの読む → 変える → 書くは、core/settings.ps1 の invokeSettingsLocked の中で行う。IndexCatalog・WorkspaceMover の書き込みもそうする(名前の割り当ての保存は saveAssignedIndexNames を使う)。
  • searchExcludes の削除・付け替え・保存でのフォルダの下かどうかの比べ方は 1 か所(core/search_excludes.ps1 の testFolderOrUnder)だけにする。 大文字・小文字を区別しない序数の比べ(OrdinalIgnoreCase)で、folder の末尾の \ を外してから、同じか、<フォルダ>\ で始まるかを見る。既知の制限: \\?\ 付きと無し、/ 区切り、..、8.3 形式の短い名前は同じ場所として扱わない(書き方をそろえるのは呼び出す側。このツールが書いたパスどうしを比べる)。searchExcludes の付け替え・削除と、検索対象のツリーが使う。
  • クラスのメソッド名は、組み込みの型やコレクションと同じ名前(Remove・Rename・Move・Change・Entries など)にしない。画面のスレッドの検査(tests/meta/ui_thread_io.Tests.ps1)が名前でメソッドの呼び出しを見つけるため、同じ名前の別のメソッド(List.Remove など)まで「ファイルに触る」と見なして誤って落ちる。
  • 性能は、同じ PC・同じデータ・同じ引数で、同じ日に main → refactor のブランチ → main と続けて測った数字どうしで比べる。
  • 取り込みの 1 ファイルごとの経路で、関数の呼び出しの段を増やさない。