C#の型システムを活用して高品質コードを書くための完全ガイド

C#の型システムは、高品質で保守性の高いコードを書くための強力なツールです。型安全性を確保し、エラーを事前に防ぐことで、開発効率とコードの信頼性を大幅に向上させることができます。本記事では、C#の型システムを活用して高品質なコードを書くための具体的な方法とベストプラクティスを紹介します。静的型付けの利点から型推論の活用方法、ヌル許容参照型の導入、ジェネリック型や型安全なコレクションの使用まで、幅広く解説していきます。

目次

静的型付けの利点

C#は静的型付けの言語であり、コンパイル時に型チェックが行われるため、実行時のエラーを減らし、コードの安全性と安定性を高めることができます。静的型付けの利点として以下の点が挙げられます。

1. 早期エラー検出

コンパイル時に型の不一致が検出されるため、実行時に発生するエラーを事前に防ぐことができます。これにより、デバッグ時間を削減し、開発効率が向上します。

2. 自己文書化コード

変数やメソッドの型が明示されることで、コードの読みやすさと理解しやすさが向上します。これにより、他の開発者がコードを理解しやすくなり、保守性が向上します。

3. パフォーマンスの向上

静的型付けにより、コンパイラが最適化を行いやすくなるため、実行時のパフォーマンスが向上します。特に大規模なプロジェクトでは、この最適化が大きな効果を発揮します。

4. IDEのサポート強化

静的型付けにより、IDE(統合開発環境)の補完機能やリファクタリング機能が強化されます。これにより、開発者はより効率的にコーディングを行うことができます。

C#の静的型付けは、コードの品質と開発効率を大幅に向上させるための基本的な要素です。次に、型推論の活用方法について詳しく見ていきましょう。

型推論の活用

C#では、型推論(Type Inference)を利用して、明示的に型を指定せずに変数やオブジェクトの型を決定することができます。これにより、コードの簡潔さと可読性が向上します。

1. varキーワードの使用

型推論の基本的な方法として、varキーワードを使用することが挙げられます。varを用いることで、コンパイラが右辺の式から型を推論します。

var number = 10; // numberはint型として推論される
var message = "Hello, World!"; // messageはstring型として推論される

2. 明示的な型宣言とのバランス

型推論を活用することでコードを簡潔に保つことができますが、すべてのケースでvarを使用するとコードが不明瞭になることがあります。特に、推論された型が明確でない場合や、他の開発者がコードを読む際に混乱を招く可能性がある場合は、明示的な型宣言を使用することが推奨されます。

int explicitNumber = 10; // 明示的な型宣言
var inferredNumber = 10; // 型推論

3. LINQクエリにおける型推論

LINQクエリでは、varを使用することでクエリ結果の型を簡単に扱うことができます。これにより、複雑な型を明示的に記述する必要がなくなり、コードがより直感的になります。

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(n => n % 2 == 0);

4. 無名型の利用

無名型を使用する場合、型推論は不可欠です。無名型は、名前のない型を動的に作成するために使用され、特にデータの一時的な集約に便利です。

var person = new { Name = "John", Age = 30 };
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");

型推論を適切に活用することで、コードの可読性と保守性を高めることができます。次に、ヌル許容参照型の導入について詳しく見ていきましょう。

ヌル許容参照型の導入

C# 8.0以降、ヌル参照によるエラーを防ぐためにヌル許容参照型が導入されました。これにより、コンパイル時にヌル参照の可能性を検出し、より安全なコードを書くことができます。

1. ヌル許容参照型とは

ヌル許容参照型(nullable reference types)は、変数がnullを許容するかどうかを明示的に指定する機能です。nullを許容する場合は型名に?を付けます。

string? nullableString = null; // `null`を許容する
string nonNullableString = "Hello"; // `null`を許容しない

2. コンパイラの警告と制約

ヌル許容参照型を使用すると、コンパイラが潜在的なヌル参照エラーを警告します。これにより、nullチェックを強制し、コードの安全性を高めることができます。

void PrintLength(string? input)
{
    if (input != null)
    {
        Console.WriteLine(input.Length);
    }
    else
    {
        Console.WriteLine("Input is null");
    }
}

3. 既存コードへの適用

既存のプロジェクトにヌル許容参照型を導入する際は、プロジェクトファイルに設定を追加します。これにより、既存コードのヌル許容参照型のチェックを有効にできます。

<PropertyGroup>
    <Nullable>enable</Nullable>
</PropertyGroup>

4. 非ヌル注釈と非ヌル許可

特定のコードブロックやプロジェクト全体でヌル許容参照型を有効または無効にすることができます。これにより、柔軟にヌル参照チェックを管理できます。

#nullable enable
string? possibleNull = null;

#nullable disable
string noNull = "Not null";

5. 実際の運用でのベストプラクティス

  • 初期化時にnullを避けるため、非ヌルフィールドには必ず値を設定する。
  • コンストラクタで必要な依存関係を注入し、null参照を防ぐ。
  • メソッドの戻り値でnullを返さないように設計する。

ヌル許容参照型を導入することで、ヌル参照によるランタイムエラーを減らし、より堅牢なコードベースを構築できます。次に、ジェネリック型の利用について見ていきましょう。

ジェネリック型の利用

ジェネリック型(Generic Types)は、C#の型システムの中でも特に強力な機能の一つです。ジェネリック型を利用することで、型安全性を保ちながら、再利用可能で柔軟なコードを記述することができます。

1. ジェネリック型とは

ジェネリック型とは、クラスやメソッドが扱うデータ型をパラメータ化することができる機能です。これにより、特定の型に依存しない汎用的なクラスやメソッドを定義できます。

public class GenericList<T>
{
    private T[] items;
    private int count;

    public GenericList(int capacity)
    {
        items = new T[capacity];
        count = 0;
    }

    public void Add(T item)
    {
        items[count++] = item;
    }

    public T Get(int index)
    {
        return items[index];
    }
}

2. コレクションにおけるジェネリック型の使用

ジェネリック型は、List<T>, Dictionary<TKey, TValue>, Queue<T>, Stack<T>などのコレクションで広く使用されており、これらのコレクションは型安全性を提供します。

List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);

Dictionary<string, int> ages = new Dictionary<string, int>();
ages.Add("Alice", 30);
ages.Add("Bob", 25);

3. 型安全なメソッドの作成

ジェネリックメソッドを使用することで、異なるデータ型に対して同じ操作を行うことができます。以下は、要素を交換するジェネリックメソッドの例です。

public static void Swap<T>(ref T a, ref T b)
{
    T temp = a;
    a = b;
    b = temp;
}

4. ジェネリック制約

ジェネリック型に制約を加えることで、特定の型に対してのみ動作するように制限することができます。これにより、より堅牢なコードを記述することが可能です。

public class Repository<T> where T : IEntity
{
    public void Add(T entity)
    {
        // 追加処理
    }

    public T GetById(int id)
    {
        // 取得処理
        return default(T);
    }
}

5. 実際の運用でのベストプラクティス

  • 必要な場合にのみジェネリックを使用し、過度な一般化を避ける。
  • 型制約を適切に設定し、意図しない型の使用を防ぐ。
  • ジェネリック型を利用する際のパフォーマンスインパクトを理解し、適切に管理する。

ジェネリック型を効果的に使用することで、コードの再利用性と保守性を大幅に向上させることができます。次に、型安全なコレクションの使用について見ていきましょう。

型安全なコレクション

型安全なコレクションを使用することで、バグを減らし、コードの可読性と保守性を向上させることができます。C#の標準ライブラリには、ジェネリックを活用した型安全なコレクションが多数用意されています。

1. Listの使用

List<T>は、最も一般的に使用される型安全なコレクションです。リストに格納される要素の型が指定されるため、型の不一致によるエラーを防ぐことができます。

List<string> names = new List<string>();
names.Add("Alice");
names.Add("Bob");

foreach (string name in names)
{
    Console.WriteLine(name);
}

2. Dictionaryの使用

Dictionary<TKey, TValue>は、キーと値のペアを格納する型安全なコレクションです。キーの型と値の型を指定することで、型安全なデータの管理が可能です。

Dictionary<int, string> employees = new Dictionary<int, string>();
employees.Add(1, "Alice");
employees.Add(2, "Bob");

foreach (var kvp in employees)
{
    Console.WriteLine($"ID: {kvp.Key}, Name: {kvp.Value}");
}

3. その他のジェネリックコレクション

C#には他にも多くの型安全なコレクションが用意されています。例えば、Queue<T>, Stack<T>, HashSet<T>などがあります。これらのコレクションも、型安全性を提供し、特定の用途に応じて効果的に使用できます。

Queue<string> tasks = new Queue<string>();
tasks.Enqueue("Task 1");
tasks.Enqueue("Task 2");

while (tasks.Count > 0)
{
    string task = tasks.Dequeue();
    Console.WriteLine(task);
}

4. 型安全なコレクションの利点

  • 型の一貫性: コレクション内の全ての要素が同じ型であることを保証し、ランタイムエラーを減らす。
  • 可読性の向上: 明示的な型宣言により、コードの可読性と理解しやすさが向上する。
  • パフォーマンスの最適化: 型安全なコレクションは、ボックス化やアンボックス化の必要がなく、パフォーマンスが向上する。

5. 実際の運用でのベストプラクティス

  • 必要な場合に適切な型安全なコレクションを選択し、使用する。
  • コレクションの初期化時に適切な初期容量を設定し、パフォーマンスを最適化する。
  • 不変コレクションを使用して、スレッドセーフなコードを確保する。

型安全なコレクションを効果的に使用することで、コードの品質と効率を向上させることができます。次に、カスタム型の定義について見ていきましょう。

カスタム型の定義

独自のカスタム型を定義することで、コードの意図を明確にし、再利用性を高めることができます。カスタム型を使うことで、複雑なデータ構造を扱いやすくし、ビジネスロジックをわかりやすくすることが可能です。

1. クラスの定義

クラスは、オブジェクト指向プログラミングの基本的な構成要素です。クラスを定義することで、関連するデータとメソッドを一つの単位としてまとめることができます。

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void Introduce()
    {
        Console.WriteLine($"Hello, my name is {Name} and I am {Age} years old.");
    }
}

2. 構造体の定義

構造体は、値型として動作する軽量なデータ構造です。値の比較や小規模なデータのグループ化に適しています。

public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    public void Display()
    {
        Console.WriteLine($"Point(X: {X}, Y: {Y})");
    }
}

3. 列挙型の定義

列挙型は、定数の集合を定義するために使用されます。特定の一連の値を持つ変数を扱う場合に便利です。

public enum DaysOfWeek
{
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
}

4. インターフェースの定義

インターフェースは、クラスや構造体が実装すべきメソッドやプロパティを定義するために使用されます。インターフェースを使うことで、異なるクラス間で共通の動作を定義できます。

public interface IAnimal
{
    void Speak();
}

public class Dog : IAnimal
{
    public void Speak()
    {
        Console.WriteLine("Woof!");
    }
}

public class Cat : IAnimal
{
    public void Speak()
    {
        Console.WriteLine("Meow!");
    }
}

5. 実際の運用でのベストプラクティス

  • 必要に応じてカスタム型を定義し、コードの明確さと保守性を向上させる。
  • 適切なアクセシビリティ修飾子(public, private, internalなど)を使用して、クラスのメンバーの可視性を制御する。
  • カスタム型の名前は、用途や意図が明確にわかるように付ける。

カスタム型を効果的に使用することで、コードの再利用性と保守性を向上させることができます。次に、型パラメータの制約について見ていきましょう。

型パラメータの制約

ジェネリック型の型パラメータに制約を設けることで、より堅牢で信頼性の高いコードを作成することができます。型パラメータの制約を利用すると、特定の条件を満たす型のみを許可することができ、誤った型の使用を防ぐことができます。

1. 型パラメータの基本制約

基本的な型パラメータの制約として、whereキーワードを使用して、型パラメータが特定のインターフェースを実装していることや、クラスの派生であることを指定できます。

public class Repository<T> where T : IEntity
{
    public void Add(T entity)
    {
        // 追加処理
    }

    public T GetById(int id)
    {
        // 取得処理
        return default(T);
    }
}

2. 複数の制約

型パラメータに対して複数の制約を設けることも可能です。例えば、特定のクラスを継承し、かつインターフェースを実装する型を指定できます。

public class MultiConstraintClass<T> where T : class, IComparable, new()
{
    public void Process(T item)
    {
        // 処理ロジック
    }
}

3. コンストラクタ制約

ジェネリック型で使用する型がパラメータなしのコンストラクタを持つことを要求する場合、new()制約を使用します。

public class Factory<T> where T : new()
{
    public T CreateInstance()
    {
        return new T();
    }
}

4. 制約の利点

  • 型安全性の向上: 制約により、特定の条件を満たす型のみを許可することで、型安全性を確保します。
  • コードの明確化: 制約を設定することで、ジェネリック型の使用意図が明確になり、コードの可読性が向上します。
  • エラー防止: 不適切な型の使用によるランタイムエラーを防ぎ、コンパイル時に問題を検出できます。

5. 制約の種類

  • クラス制約: 特定のクラスを継承する型のみを許可します。
  • インターフェース制約: 特定のインターフェースを実装する型のみを許可します。
  • コンストラクタ制約: パラメータなしのコンストラクタを持つ型のみを許可します。
  • 値型/参照型制約: 値型または参照型のみを許可します。

6. 実際の運用でのベストプラクティス

  • 制約を適切に使用して、ジェネリック型の使用範囲を明確にする。
  • 制約を使用することで、コードの意図を明確にし、可読性と保守性を向上させる。
  • 制約を追加することで、コードの堅牢性と信頼性を高める。

型パラメータの制約を適切に設定することで、ジェネリック型の安全性と効率を向上させることができます。次に、型変換とキャストについて見ていきましょう。

型変換とキャスト

型変換とキャストは、異なる型間で値を変換するための重要な技術です。C#では、安全な型変換とキャストを行うためのさまざまな方法が提供されています。

1. 暗黙的型変換と明示的型変換

暗黙的型変換は、コンパイラが自動的に行う型変換であり、データの損失がない場合に適用されます。一方、明示的型変換(キャスト)は、開発者が明示的に指定する必要があります。

// 暗黙的型変換
int intValue = 123;
double doubleValue = intValue;

// 明示的型変換(キャスト)
double anotherDoubleValue = 123.45;
int anotherIntValue = (int)anotherDoubleValue;

2. as演算子とis演算子

as演算子は、型変換が失敗した場合に例外をスローする代わりに、nullを返します。is演算子は、オブジェクトが特定の型であるかどうかをチェックします。

object obj = "Hello, World!";
string str = obj as string;
if (str != null)
{
    Console.WriteLine(str);
}

if (obj is string)
{
    Console.WriteLine((string)obj);
}

3. Convertクラスの利用

Convertクラスを使用すると、さまざまなプリミティブ型間の変換を簡単に行うことができます。

string numberStr = "123";
int number = Convert.ToInt32(numberStr);

string booleanStr = "true";
bool booleanValue = Convert.ToBoolean(booleanStr);

4. 型変換の例外処理

型変換を行う際に、変換が失敗する可能性があります。このため、例外処理を適切に行うことが重要です。

try
{
    string invalidNumberStr = "abc";
    int invalidNumber = Convert.ToInt32(invalidNumberStr);
}
catch (FormatException ex)
{
    Console.WriteLine("FormatException: " + ex.Message);
}
catch (OverflowException ex)
{
    Console.WriteLine("OverflowException: " + ex.Message);
}

5. ユーザー定義型変換

クラスにユーザー定義の型変換を追加することで、特定の型間での変換をカスタマイズすることができます。

public class Celsius
{
    public double Degrees { get; set; }

    public Celsius(double degrees)
    {
        Degrees = degrees;
    }

    public static implicit operator Fahrenheit(Celsius c)
    {
        return new Fahrenheit(c.Degrees * 9 / 5 + 32);
    }
}

public class Fahrenheit
{
    public double Degrees { get; set; }

    public Fahrenheit(double degrees)
    {
        Degrees = degrees;
    }
}

6. 実際の運用でのベストプラクティス

  • 暗黙的型変換は安全である場合にのみ使用し、明示的型変換が必要な場合はキャストを使用する。
  • as演算子とis演算子を利用して、安全に型変換を行う。
  • Convertクラスを使用して、プリミティブ型間の変換を行う際に例外処理を適切に実装する。
  • ユーザー定義型変換を適切に定義して、特定の型間での変換をカスタマイズする。

型変換とキャストを正しく理解し、適切に使用することで、コードの安全性と信頼性を高めることができます。次に、型の継承と多態性について見ていきましょう。

型の継承と多態性

型の継承と多態性(ポリモーフィズム)は、オブジェクト指向プログラミングの重要な概念であり、コードの再利用性と柔軟性を高めるために不可欠です。これらの概念を理解し、効果的に活用することで、より拡張性の高いコードを書くことができます。

1. クラスの継承

クラスの継承を利用すると、既存のクラス(親クラス)の機能を新しいクラス(子クラス)に継承させることができます。これにより、コードの再利用が容易になります。

public class Animal
{
    public string Name { get; set; }
    public void Eat()
    {
        Console.WriteLine($"{Name} is eating.");
    }
}

public class Dog : Animal
{
    public void Bark()
    {
        Console.WriteLine($"{Name} is barking.");
    }
}

2. メソッドのオーバーライド

子クラスで親クラスのメソッドを再定義(オーバーライド)することができます。これにより、特定の動作をカスタマイズすることができます。

public class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal sound");
    }
}

public class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Woof!");
    }
}

3. 抽象クラスと抽象メソッド

抽象クラスは、直接インスタンス化できないクラスであり、抽象メソッドを含むことができます。抽象メソッドは、子クラスで必ず実装しなければなりません。

public abstract class Animal
{
    public abstract void Speak();
}

public class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Woof!");
    }
}

4. インターフェースの実装

インターフェースは、クラスや構造体が実装すべきメソッドやプロパティを定義します。インターフェースを使用することで、多態性を実現し、異なるクラス間で共通の動作を保証できます。

public interface IAnimal
{
    void Speak();
}

public class Dog : IAnimal
{
    public void Speak()
    {
        Console.WriteLine("Woof!");
    }
}

public class Cat : IAnimal
{
    public void Speak()
    {
        Console.WriteLine("Meow!");
    }
}

5. 多態性の実現

多態性を利用すると、異なるクラスのオブジェクトを同じインターフェース型または親クラス型として扱うことができます。これにより、柔軟で拡張性の高いコードを記述することが可能です。

public void MakeAnimalSpeak(IAnimal animal)
{
    animal.Speak();
}

IAnimal dog = new Dog();
IAnimal cat = new Cat();

MakeAnimalSpeak(dog); // Output: Woof!
MakeAnimalSpeak(cat); // Output: Meow!

6. 実際の運用でのベストプラクティス

  • 継承は「is-a」関係を持つ場合にのみ使用し、適切な抽象化を行う。
  • オーバーライドを使用して、親クラスの機能を適切にカスタマイズする。
  • 抽象クラスとインターフェースを利用して、共通の動作を定義し、コードの再利用性を高める。
  • 多態性を活用して、柔軟で拡張性の高いコード設計を行う。

型の継承と多態性を効果的に利用することで、コードの再利用性と柔軟性を大幅に向上させることができます。次に、型システムのベストプラクティスについて見ていきましょう。

型システムのベストプラクティス

C#の型システムを最大限に活用するためには、いくつかのベストプラクティスを守ることが重要です。これにより、コードの品質を向上させ、保守性を高めることができます。

1. 明示的な型の使用

明示的な型を使用することで、コードの可読性と理解しやすさを向上させることができます。特に、varキーワードを使用する場合でも、推論された型が明確であることを確認してください。

// 明示的な型宣言
int count = 10;
string name = "Alice";

// 型推論を使用する場合、明確な場合に限る
var age = 30; // 明確な型

2. 不変オブジェクトの利用

不変オブジェクトを使用すると、オブジェクトの状態が変更されないことを保証できるため、バグを減らし、コードの信頼性を高めることができます。

public class ImmutablePerson
{
    public string Name { get; }
    public int Age { get; }

    public ImmutablePerson(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

3. Nullable Reference Typesの活用

C# 8.0以降で導入されたNullable Reference Typesを活用することで、ヌル参照によるランタイムエラーを防ぐことができます。これにより、コードの安全性が大幅に向上します。

#nullable enable
string? nullableString = null;
if (nullableString != null)
{
    Console.WriteLine(nullableString.Length);
}

4. ジェネリック型の適切な使用

ジェネリック型を使用して、型安全で再利用可能なコードを作成します。必要に応じて型パラメータの制約を設定し、意図しない型の使用を防ぎます。

public class GenericRepository<T> where T : IEntity
{
    public void Add(T entity)
    {
        // 追加処理
    }
}

5. インターフェースの使用

インターフェースを使用して、異なるクラス間で共通の動作を定義し、多態性を実現します。これにより、コードの柔軟性と拡張性が向上します。

public interface IRepository<T>
{
    void Add(T item);
    T Get(int id);
}

public class SqlRepository<T> : IRepository<T>
{
    // 実装
}

6. 型変換とキャストの安全な実施

型変換やキャストを行う際は、安全に行うための方法を使用します。as演算子やis演算子、例外処理を適切に実装して、ランタイムエラーを防ぎます。

object obj = "Hello";
if (obj is string str)
{
    Console.WriteLine(str);
}

7. 適切なアクセシビリティの設定

クラスやメンバーのアクセシビリティを適切に設定し、不必要に公開しないようにします。これにより、外部からのアクセスを制限し、セキュリティとカプセル化を強化します。

public class Person
{
    private string name;
    public string Name { get => name; }
}

型システムのベストプラクティスを守ることで、コードの品質、可読性、安全性を大幅に向上させることができます。次に、応用例と演習問題について見ていきましょう。

応用例と演習問題

C#の型システムを理解し、実践的に活用するために、いくつかの応用例と演習問題を提供します。これらの例と問題を通じて、実際のプロジェクトでの活用方法を学びましょう。

1. 応用例: 型安全なデータアクセスレイヤー

型安全なデータアクセスレイヤーを構築することで、データベース操作の安全性と保守性を向上させることができます。以下は、ジェネリックリポジトリパターンの例です。

public interface IEntity
{
    int Id { get; set; }
}

public class Product : IEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

public interface IRepository<T> where T : IEntity
{
    void Add(T entity);
    T GetById(int id);
    IEnumerable<T> GetAll();
}

public class Repository<T> : IRepository<T> where T : class, IEntity, new()
{
    private readonly List<T> _entities = new();

    public void Add(T entity)
    {
        _entities.Add(entity);
    }

    public T GetById(int id)
    {
        return _entities.FirstOrDefault(e => e.Id == id);
    }

    public IEnumerable<T> GetAll()
    {
        return _entities;
    }
}

// 使用例
var productRepository = new Repository<Product>();
productRepository.Add(new Product { Id = 1, Name = "Laptop", Price = 1000m });
var product = productRepository.GetById(1);
Console.WriteLine($"Product: {product.Name}, Price: {product.Price}");

2. 応用例: 型安全な設定管理

アプリケーションの設定を型安全に管理することで、設定の読み書きを簡単に行えます。以下は、型安全な設定管理の例です。

public class AppSettings
{
    public string ConnectionString { get; set; }
    public int Timeout { get; set; }
}

public static class ConfigurationManager
{
    private static readonly AppSettings _settings = new AppSettings
    {
        ConnectionString = "Data Source=myServer;Initial Catalog=myDB;User Id=myUser;Password=myPass;",
        Timeout = 30
    };

    public static AppSettings Settings => _settings;
}

// 使用例
var settings = ConfigurationManager.Settings;
Console.WriteLine($"Connection String: {settings.ConnectionString}, Timeout: {settings.Timeout}");

3. 演習問題: 型安全なコレクションの作成

次の演習問題を解いて、型安全なコレクションの理解を深めましょう。

演習問題 1: 型安全なスタック
ジェネリック型を使用して、型安全なスタック(LIFO)クラスを実装してください。以下の操作をサポートする必要があります。

  • Push(T item): スタックに要素を追加する
  • T Pop(): スタックから要素を取り出す
  • T Peek(): スタックのトップ要素を取得する
public class GenericStack<T>
{
    private readonly List<T> _items = new();

    public void Push(T item)
    {
        _items.Add(item);
    }

    public T Pop()
    {
        if (_items.Count == 0)
            throw new InvalidOperationException("Stack is empty.");
        var item = _items[^1];
        _items.RemoveAt(_items.Count - 1);
        return item;
    }

    public T Peek()
    {
        if (_items.Count == 0)
            throw new InvalidOperationException("Stack is empty.");
        return _items[^1];
    }
}

// 使用例
var stack = new GenericStack<int>();
stack.Push(1);
stack.Push(2);
Console.WriteLine(stack.Pop()); // 出力: 2
Console.WriteLine(stack.Peek()); // 出力: 1

演習問題 2: 型安全なキュー
ジェネリック型を使用して、型安全なキュー(FIFO)クラスを実装してください。以下の操作をサポートする必要があります。

  • Enqueue(T item): キューに要素を追加する
  • T Dequeue(): キューから要素を取り出す
  • T Peek(): キューの先頭要素を取得する
public class GenericQueue<T>
{
    private readonly Queue<T> _queue = new();

    public void Enqueue(T item)
    {
        _queue.Enqueue(item);
    }

    public T Dequeue()
    {
        if (_queue.Count == 0)
            throw new InvalidOperationException("Queue is empty.");
        return _queue.Dequeue();
    }

    public T Peek()
    {
        if (_queue.Count == 0)
            throw new InvalidOperationException("Queue is empty.");
        return _queue.Peek();
    }
}

// 使用例
var queue = new GenericQueue<string>();
queue.Enqueue("first");
queue.Enqueue("second");
Console.WriteLine(queue.Dequeue()); // 出力: first
Console.WriteLine(queue.Peek()); // 出力: second

これらの応用例と演習問題を通じて、C#の型システムをより深く理解し、実践的に活用するスキルを身につけてください。次に、この記事のまとめを見ていきましょう。

まとめ

本記事では、C#の型システムを活用して高品質なコードを書くための様々な方法とベストプラクティスを紹介しました。静的型付けの利点、型推論の活用、ヌル許容参照型の導入、ジェネリック型の利用、型安全なコレクション、カスタム型の定義、型パラメータの制約、型変換とキャスト、型の継承と多態性、そして型システムのベストプラクティスについて詳しく解説しました。

これらの概念とテクニックを理解し、実践することで、コードの品質、保守性、再利用性を大幅に向上させることができます。さらに、応用例と演習問題を通じて、実際のプロジェクトでこれらの知識をどのように活用するかを学びました。

今後のプロジェクトで、C#の型システムを効果的に活用し、高品質なコードを作成してください。

コメント

コメントする

目次