この記事のまとめ
- 型安全なプログラムを意識することで、データの渡し間違いによる論理バグを、未然に防ぐことができます。
- 型安全なプログラムの実現には、
record
型またはrecord struct
型の活用を推奨します。 - 特に引数の数が多いメソッドでは、タイプセーフな設計が効果を発揮します。
組み込みの型を使うことの弊害
大規模な開発をしていると、よりタイプセーフな形でプログラムを開発したくなることがあります。 例えば以下のようなケースを考えてみてください。
int id = 1; int stage = 4; string message = GetMessage(id); Console.WriteLine($"stage:{stage} {message}"); static string GetMessage(int messageId) => messageId switch { 1 => "Hello", 2 => "Goodbye", _ => "I don't know" };
GetMessage
メソッドは messageId
を指定してメッセージを取得する機能を持ちます。
引数には int
型の値を渡せます。
上記のプログラムは全く問題なく動作しますが、引数に任意の int
型の値を渡せるため、論理バグを引き起こしやすい構造になっています。
例えば以下のようなプログラムを考えてみます。
int id = 1; int stage = 4; string message = GetMessage(stage); // 誤ってstageを渡してしまった
上記のようコーディングしても、プログラム上は int
型の値を渡しており、コンパイルも実行もできてしまいます。
ただ、実行結果に論理的な誤りがある状態であり、正しいプログラムではありません。
このような問題を、プログラムの作り方で改善するひとつの方法が、タイプセーフ(型安全)という考え方です。
タイプセーフにする方法
先ほどの例で問題を引き起こす根本的な原因は、 GetMessage
メソッドの引数に、汎用的な型である int
型を使っていることにあります。
ここにメッセージIDを表す専用の型を導入することで、誤った値を指定するリスクを格段に減らせます。
C#の場合、 record
型または record struct
型を使うとうまく解決できます。
MessageId id = new MessageId(1); int stage = 4; string message = GetMessage(id); // MessageId以外は渡せない Console.WriteLine($"stage:{stage} {message}"); static string GetMessage(MessageId messageId) => messageId.Value switch { 1 => "Hello", 2 => "Goodbye", _ => "I don't know" }; record struct MessageId(int Value);
この例ではメッセージIDを表す専用の型を record struct
で作成しています。
GetMessage
メソッドの引数にその専用型を使用し、メッセージID以外が指定されないように型のレベルで強制しています。
タイプセーフであることのメリット
タイプセーフであることのメリットは、メソッドの引数が複雑になればなるほど発揮されます。 例えば以下のようなメソッドを考えてみます。
static decimal GetSubTotal( int productId, int amount, int taxCategory, int discountId) { // do something }
このメソッドは4つの int
型引数を取ります。
すべて型が同じであるため、呼び出す側は引数の設定誤りがないように注意しながらコーディングするはずです。
またできあがったプログラムをコードレビュー等で確認するときも、本当に正しく引数を指定できているか、確認する負荷が高くなります。
int productId = 123; int amount = 2; int taxCategory = 1; int discountId = 0; GetSubTotal(productId, amount, discountId, taxCategory);
実際上記のプログラムは、引数の設定誤りがあります。 これを瞬時に判断するのは、もはや不可能と言えるのではないでしょうか。
それぞれの引数に専用型を作ると、以下のようなプログラムになります。
var productId = new ProductId(123); var amount = new Amount(2); var taxCategory = new TaxCategory(1); var discountId = new DiscountId(0); GetSubTotal(productId, amount, taxCategory, discountId); static decimal GetSubTotal( ProductId productId, Amount amount, TaxCategory taxCategory, DiscountId discountId) { // do something } record struct ProductId(int Value); record struct Amount(int Value); record struct TaxCategory(int Value); record struct DiscountId(int Value);
引数の順序を間違えたとしても、コンパイルエラーで不具合を検出できてしまうのです。 プログラムが複雑になればなるほど、効果を発揮する方法だと言えます。
タイプセーフであることのデメリット
タイプセーフなプログラムを作成しようとすると、必要となるプログラムの記述量が多少多くなります。 今回ご紹介した簡易な方法であれば、ほんのわずかな手間でタイプセーフを保証できますが、突き詰めようとするとまだまだできることが存在します。
また通常このような専用型は、データベースやWeb APIなど、外部システムと連携する際、何かしらの型変換を実装しなければなりません。 ここにも、多少の手間の増大があるのです。
まとめ
タイプセーフによって享受できるメリットと手間の増大、どちらに重きを置くかで、採用の判断が変わると思います。 銀の弾丸ではないですし、状況に合わせて使い分けるべきものとも思います。 一部の重要な型のみタイプセーフにする、という判断もあるべきです。
食わず嫌いをせずに、一度試してみることを個人的にはおすすめします。 一度タイプセーフの世界を味わうと、抜け出せなくなるのではないかと想像しています。 実際に私は、複雑なプログラムを書く際、タイプセーフであることを優先することが多いです。 ぜひ一度試して、感覚を得てもらえればと思います。