課題とゴールを確認します。だいたい「Pythonならこうする」という話です。
実装だけ見たければ読み飛ばしてください。
インスタンスメソッドでない
わざわざ型名から書いてstaticメソッドの呼び出しを書かないといけないのが冗長で苦痛です。
Pythonなら次のように書けます。すっきり。
この面倒さはJoin()も同様です。頻繁に使う機能なのでつらい。
幸いC#には拡張メソッドがあるので簡単に対応できます。
インデックスを正確に割り振る必要がある
書式に与える引数は、書式内に記述したインデックスにより対応付けされます。このため、Format()で引数を与えるためには、インデックスを決めないといけないし、インデックスに対応する値を正しく与えないといけません。
例えば、下記のように2つ引数をとってFormat()を利用していたとしましょう(※DateTimeは専用の書式があるので普通こんなことしません)。
ここで先頭に引数を増やしたいとします。
インデックスが1つずつずれるので、手で直す必要がありました。この手間によって、ヒューマンエラーが入り込む隙を生み出しています。利用者にインデックス管理を強いるインターフェースは最悪だと思います。
Pythonならインデックスを省略できて、勝手にインクリメントされたインデックスが割り振られます。省略表記のインデックス指定は混在できませんが、かなりマシでしょう。インデックスを省略しても、書式オプションを個別に書くことができます。
さらにPythonは名前付き引数を与えられます。
名前指定ならインデックス管理するより直感的ですし、引数を増やしたからといって既存の引数名を修正する必要もないので扱いやすいですね。引数の順序だって気にする必要がないです。
ところで、C#ではparamsキーワードを用いて可変長引数を受け取ることができますが、これは配列で引数を受け取ることもできます。
Pythonの場合、リストだけでなく、辞書を引数に展開できます。
これはPythonが動的型付け前提だからこそ可能だと思うので、全く同じことをC#でも対応してほしい、というわけではないです。
ただ、Pythonから得られたアイデアとして、下記いずれかの機能は欲しいですね。
- インデックスの省略
- インデックスではなく名前で指定
両方対応してもいいのですが、今回は名前付き引数をDictionaryで与える方法だけを考えます。配列だけでなくDictionaryを引数に持てるようになる、というのは大きな進歩だと思います。
書式が覚えられない
書式の仕様が他言語に見られない独自仕様な上に、多機能すぎて複雑怪奇で覚えられません。別記事に愚痴をまとめました。
独自フォーマット仕様を作るのもデメリットが多いので、何か対策するわけでもないです。諦めましょう。
C# 6の文字列補間
C# 6から導入された強力な言語機能として、文字列補間(string interpolation)があります。
$から始まる文字列リテラルが文字列補間であると解釈され、自動でstring.Format()の呼び出しコードに置き換えられます。つまり、コンパイラが勝手にインデックスとの対応付けを解決してくれます。糖衣構文(シンタックスシュガー)というやつですね。
C# 6が使える環境ならば、もはやstring.Format()を呼び出すコードを書く必要がなくなったし、文字列補間に統一すべきとさえ思います。ただ、Unityを使っている場合、C# 6が使えるようになったのは比較的最近の話なので、採用には慎重になる必要があります。C# 6を使う方法は下記の記事にまとめました。
文字列補間が使えるようになったとしても、string.Format()にDictionaryを渡せるようになると、Dictionaryのインターフェースを通して値を返すオブジェクトを挟む、という別の使い道も用意できるので、文字列補間とはまた別の価値があると考えています。
前置きが長くなりましたが実装します。
下記のことを考慮します。
- メソッドの引数に与えるIDictionaryにはインデクサのみ要求
- 置換の必要がなければ余計なアロケーションなし
- string.Format()でインデックス指定していたのが名前指定に置き換わっただけで、残りの機能は引き継ぐ
与えられた文字列を、キー名からインデックスに変換してstring.Format()に渡すのがシンプルで統一的な挙動にまとまるので、そうします。
最初はStringBuilderの使用を考えていたのですが、内部実装的には編集可能な文字列コンテナというだけのようだったので、冗長だと判断してやめました。
必要に応じて部分文字列リストを作成し、これをstring.Concat()で連結したものをstring.Format()に渡して、あとの処理は任せます。IDictionaryからは必要な値を一度だけ取得してキャッシュすることで、IDictionaryのインデクサが多少冗長でもコストが最小限になるようにしました。
これで下記のように書けます。FormatDictionaryはいちいちDictionary<string, object>
と書きたくないので用意したクラスです。IDictionary<string, object>
を実装してインデクサから値が取れればなんでも使えます。
C# 6ならば、インデクサに対応した初期化子が使えるので、下記のようにも書けます。
formatByNameの第3引数useOptionをfalseにすると、:
後に書かれる書式オプションをstring.Format()に渡さないようになります。無視するのではなく、{key:option}
からkey
を取り出して辞書を引いていたところが、key:option
を取り出して辞書を引くようになります。単に波括弧内を辞書のキーにする挙動になるということですね。
ゲームプログラムにおいて、外部データに保存された文字列を使用してフォーマットすることを考えていて、細かい書式指定やらは必要な時にプログラマがやればいい、という想定でいます。なので、書式指定せずに置換だけ行う機能を選択肢に残しています。