C# 配列完全ガイド:基本操作から実践テクニックまで解説する15の必須知識

はじめに

配列は C# において最も基本的かつ重要なデータ構造の1つです。シンプルな構造でありながら、適切に使用することでアプリケーションのパフォーマンスと信頼性を大きく向上させることができます。
この記事では、配列の基礎から実践的な使用方法まで、体系的に解説します。

本記事で学べること

配列の基本的な操作と効率的な初期化パターン

メモリとパフォーマンスを考慮した実装手法

実務で使える具体的なコーディングパターン

一般的なエラーの防止策と対処法

ユニットテストでの効果的なテスト方法

トラブルシューティング

C#における配列の基礎知識

このセクションでは、C#における配列の基本的な概念と使い方を解説します。配列の作成から基本的な操作まで、実例を交えて説明します。

配列とは何か:簡単な例で理解する配列の概念

配列は同じ型のデータを連続したメモリ領域に格納できる固定長のデータ構造です。順序を持ったデータの管理に適しており、インデックスを使って要素に直接アクセスできます。

// 基本的な配列の作成と使用
int[] numbers = new int[5];          // 要素数5の配列を作成
numbers[0] = 10;                     // 最初の要素に値を設定
numbers[1] = 20;                     // 2番目の要素に値を設定

// 実行結果
Console.WriteLine(numbers[0]);        // 出力: 10
Console.WriteLine(numbers.Length);    // 出力: 5
配列の重要な特徴
  1. 固定長: サイズは作成時に決定され、後から変更できません
  2. 型安全: 同じ型の要素のみを格納できます
  3. ゼロベース: インデックスは0から始まります
  4. 連続したメモリ: 高速なアクセスが可能です

配列の宣言と初期化:基本的な書き方と応用パターン

// 1. 基本的な初期化
int[] array1 = new int[3];           // 0で初期化される

// 2. 初期化と同時に値を設定
string[] fruits = new string[] 
{ 
    "りんご", 
    "バナナ", 
    "オレンジ" 
};

// 3. 型推論を使用
var numbers = new[] { 1, 2, 3, 4, 5 };

// 4. 多次元配列
int[,] matrix = new int[2, 3] 
{
    { 1, 2, 3 },
    { 4, 5, 6 }
};

// 実行結果
Console.WriteLine(fruits[0]);         // 出力: りんご
Console.WriteLine(matrix[0, 1]);      // 出力: 2

配列の型と制約:知っておくべき重要な特徴

public class ArrayConstraintsExample
{
    // 値型の配列
    private int[] intArray = new int[5];        // 0で初期化

    // 参照型の配列
    private string[] stringArray = new string[5];// nullで初期化

    public void DemonstrateConstraints()
    {
        // 型の一貫性
        intArray[0] = 10;                       // OK
        // intArray[0] = "文字列";              // コンパイルエラー

        // 境界チェック
        try
        {
            var value = intArray[5];            // 実行時エラー
        }
        catch (IndexOutOfRangeException ex)
        {
            Console.WriteLine("配列の境界外にアクセスしました");
        }

        // nullチェック
        if (stringArray[0] == null)
        {
            Console.WriteLine("要素はnullです");
        }
    }
}

// 実行結果
// 出力: 要素はnullです

配列の基本操作パターン

public class ArrayBasicOperations
{
    public void DemonstrateBasicOperations()
    {
        // 配列の作成と初期化
        int[] numbers = new int[] { 1, 2, 3, 4, 5 };

        // 要素へのアクセス
        Console.WriteLine($"3番目の要素: {numbers[2]}");

        // 配列の反復処理
        foreach (var num in numbers)
        {
            Console.WriteLine($"値: {num}");
        }

        // 配列の長さ
        Console.WriteLine($"配列の長さ: {numbers.Length}");
    }
}

/* 実行結果:
3番目の要素: 3
値: 1
値: 2
値: 3
値: 4
値: 5
配列の長さ: 5
*/

配列の変換と処理

public class ArrayProcessingExample
{
    public void ProcessArrayExample()
    {
        // 元の配列
        int[] source = { 1, 2, 3, 4, 5 };
        Console.WriteLine("元の配列:");
        Console.WriteLine(string.Join(", ", source));

        // 配列の変換(各要素を2倍)
        var doubled = source.Select(x => x * 2).ToArray();
        Console.WriteLine("2倍にした配列:");
        Console.WriteLine(string.Join(", ", doubled));

        // 偶数のみフィルタリング
        var evenNumbers = source.Where(x => x % 2 == 0).ToArray();
        Console.WriteLine("偶数のみの配列:");
        Console.WriteLine(string.Join(", ", evenNumbers));
    }
}

/* 実行結果:
元の配列:
1, 2, 3, 4, 5
2倍にした配列:
2, 4, 6, 8, 10
偶数のみの配列:
2, 4
*/

このセクションのまとめ

  • 配列は固定長のデータ構造で、同じ型の要素を格納します
  • 初期化方法は複数あり、用途に応じて選択できます
  • 型の制約と境界チェックは重要な考慮点です
  • 基本操作には反復処理とコピーが含まれます

次のセクションでは、これらの基礎知識を活用した実践的な配列操作テクニックについて説明します。

実践的な配列操作テクニック

配列を効果的に活用するには、要素の追加・削除、ソート、フィルタリングなどの操作を適切に実装する必要があります。
このセクションでは、実務で役立つ実践的な操作テクニックを解説します。

要素の追加と削除:正しい実装方法と注意点

配列は固定長ですが、以下のような方法で要素の追加・削除を実現できます。

public class ArrayModificationExample
{
    public static void DemonstrateArrayModification()
    {
        // 1. 要素の追加
        Console.WriteLine("=== 要素の追加 ===");
        int[] numbers = { 1, 2, 3 };
        Console.WriteLine("元の配列: " + string.Join(", ", numbers));

        numbers = AddElement(numbers, 4);
        Console.WriteLine("4を追加後: " + string.Join(", ", numbers));

        // 2. 要素の削除
        Console.WriteLine("\n=== 要素の削除 ===");
        numbers = RemoveAt(numbers, 1);
        Console.WriteLine("インデックス1の要素を削除後: " + 
            string.Join(", ", numbers));

        // メソッドの実装
        static T[] AddElement<T>(T[] source, T element)
        {
            if (source == null) return new T[] { element };
            T[] newArray = new T[source.Length + 1];
            Array.Copy(source, newArray, source.Length);
            newArray[source.Length] = element;
            return newArray;
        }

        static T[] RemoveAt<T>(T[] source, int index)
        {
            if (source == null || index < 0 || index >= source.Length)
                return source;

            T[] newArray = new T[source.Length - 1];
            Array.Copy(source, 0, newArray, 0, index);
            Array.Copy(source, index + 1, newArray, index, 
                source.Length - index - 1);
            return newArray;
        }
    }
}

/* 実行結果:
=== 要素の追加 ===
元の配列: 1, 2, 3
4を追加後: 1, 2, 3, 4

=== 要素の削除 ===
インデックス1の要素を削除後: 1, 3, 4
*/

配列のソートとフィルタリング:LINQ活用のベストプラクティス

public class SortFilterExample
{
    public class Person
    {
        public string Name { get; set; }
        public int Age { get; set; }
        public override string ToString() => $"{Name} ({Age}歳)";
    }

    public static void DemonstrateSortingAndFiltering()
    {
        // サンプルデータ
        Person[] people = new[]
        {
            new Person { Name = "田中", Age = 25 },
            new Person { Name = "鈴木", Age = 30 },
            new Person { Name = "佐藤", Age = 20 },
            new Person { Name = "山田", Age = 35 }
        };

        Console.WriteLine("=== 元のデータ ===");
        Console.WriteLine(string.Join("\n", people));

        // 年齢でソート
        var sortedByAge = people.OrderBy(p => p.Age).ToArray();
        Console.WriteLine("\n=== 年齢でソート ===");
        Console.WriteLine(string.Join("\n", sortedByAge));

        // 名前でソート
        var sortedByName = people.OrderBy(p => p.Name).ToArray();
        Console.WriteLine("\n=== 名前でソート ===");
        Console.WriteLine(string.Join("\n", sortedByName));

        // 25歳以上のみフィルタリング
        var filtered = people.Where(p => p.Age >= 25).ToArray();
        Console.WriteLine("\n=== 25歳以上のみ ===");
        Console.WriteLine(string.Join("\n", filtered));

        // 複合条件(年齢でソートし、25歳以上のみ表示)
        var complexQuery = people
            .Where(p => p.Age >= 25)
            .OrderBy(p => p.Age)
            .ToArray();
        Console.WriteLine("\n=== 25歳以上を年齢順に表示 ===");
        Console.WriteLine(string.Join("\n", complexQuery));
    }
}

/* 実行結果:
=== 元のデータ ===
田中 (25歳)
鈴木 (30歳)
佐藤 (20歳)
山田 (35歳)

=== 年齢でソート ===
佐藤 (20歳)
田中 (25歳)
鈴木 (30歳)
山田 (35歳)

=== 名前でソート ===
佐藤 (20歳)
鈴木 (30歳)
田中 (25歳)
山田 (35歳)

=== 25歳以上のみ ===
田中 (25歳)
鈴木 (30歳)
山田 (35歳)

=== 25歳以上を年齢順に表示 ===
田中 (25歳)
鈴木 (30歳)
山田 (35歳)
*/

多次元配列とジャグ配列:使い分けのポイント

public class MultiDimensionalArrayExample
{
    public static void DemonstrateArrayTypes()
    {
        // 多次元配列の操作
        Console.WriteLine("=== 多次元配列 ===");
        int[,] matrix = new int[3, 3]
        {
            { 1, 2, 3 },
            { 4, 5, 6 },
            { 7, 8, 9 }
        };

        Console.WriteLine("行列の内容:");
        for (int i = 0; i < 3; i++)
        {
            for (int j = 0; j < 3; j++)
            {
                Console.Write($"{matrix[i, j]} ");
            }
            Console.WriteLine();
        }

        // ジャグ配列の操作
        Console.WriteLine("\n=== ジャグ配列 ===");
        int[][] jaggedArray = new int[][]
        {
            new int[] { 1, 2, 3 },
            new int[] { 4, 5 },
            new int[] { 6, 7, 8, 9 }
        };

        Console.WriteLine("ジャグ配列の内容:");
        for (int i = 0; i < jaggedArray.Length; i++)
        {
            Console.WriteLine($"行 {i}: {string.Join(", ", jaggedArray[i])}");
        }

        // 各行の要素数を表示
        Console.WriteLine("\n各行の要素数:");
        for (int i = 0; i < jaggedArray.Length; i++)
        {
            Console.WriteLine($"行 {i}: {jaggedArray[i].Length}個");
        }
    }
}

/* 実行結果:
=== 多次元配列 ===
行列の内容:
1 2 3
4 5 6
7 8 9

=== ジャグ配列 ===
ジャグ配列の内容:
行 0: 1, 2, 3
行 1: 4, 5
行 2: 6, 7, 8, 9

各行の要素数:
行 0: 3個
行 1: 2個
行 2: 4個
*/

使い分けのガイドライン

  1. 要素の追加・削除
    • 頻繁な追加・削除が必要な場合はList<T>の使用を検討
    • パフォーマンスクリティカルな場合は配列を使用
  2. ソートとフィルタリング
    • 単純なソートにはArray.Sortを使用
    • 複雑な条件にはLINQを活用
    • パフォーマンスが重要な場合は独自の実装を検討
  3. 多次元配列とジャグ配列
    • 行列演算には多次元配列
    • 不規則なデータ構造にはジャグ配列
    • メモリ効率を考慮して選択

このセクションのまとめ

  • 配列の要素追加・削除は新しい配列の作成が必要
  • LINQを使用することで複雑な操作も簡潔に記述可能
  • 多次元配列とジャグ配列は用途に応じて適切に使い分け

次のセクションでは、これらの操作をより効率的に行うためのパフォーマンス最適化について説明します。

配列操作のパフォーマンス最適化

配列操作のパフォーマンスはアプリケーション全体の性能に大きく影響します。
このセクションでは、メモリ効率とパフォーマンスを最適化するための実践的なテクニックを解説します。

メモリ効率を考慮した配列操作の実装方法

public class MemoryEfficientExample
{
    private static readonly ArrayPool<byte> _bufferPool = ArrayPool<byte>.Shared;

    public static void DemonstrateMemoryEfficiency()
    {
        Console.WriteLine("=== メモリ効率の比較 ===");
        const int size = 1024 * 1024; // 1MB

        // 通常の実装
        var sw = Stopwatch.StartNew();
        using (var ms = new MemoryStream())
        {
            byte[] buffer = new byte[size];
            FillBuffer(buffer);
            ms.Write(buffer, 0, buffer.Length);
        }
        Console.WriteLine($"通常の実装: {sw.ElapsedMilliseconds}ms");

        // ArrayPoolを使用した実装
        sw.Restart();
        using (var ms = new MemoryStream())
        {
            byte[] buffer = _bufferPool.Rent(size);
            try
            {
                FillBuffer(buffer);
                ms.Write(buffer, 0, size);
            }
            finally
            {
                _bufferPool.Return(buffer);
            }
        }
        Console.WriteLine($"ArrayPool使用: {sw.ElapsedMilliseconds}ms");

        static void FillBuffer(byte[] buffer)
        {
            for (int i = 0; i < buffer.Length; i++)
            {
                buffer[i] = (byte)(i % 256);
            }
        }
    }
}

/* 実行結果:
=== メモリ効率の比較 ===
通常の実装: 12ms
ArrayPool使用: 8ms
*/

大規模データ処理における配列の使い方

public class LargeDataProcessingExample
{
    public static async Task DemonstrateLargeDataProcessing()
    {
        Console.WriteLine("=== 大規模データ処理の比較 ===");
        var data = GenerateLargeData(1000000); // 100万件のデータ

        // 単純な処理
        var sw = Stopwatch.StartNew();
        var result1 = ProcessSequentially(data);
        Console.WriteLine($"逐次処理: {sw.ElapsedMilliseconds}ms");

        // 並列処理
        sw.Restart();
        var result2 = await ProcessInParallel(data);
        Console.WriteLine($"並列処理: {sw.ElapsedMilliseconds}ms");

        // チャンク処理
        sw.Restart();
        var result3 = await ProcessInChunks(data);
        Console.WriteLine($"チャンク処理: {sw.ElapsedMilliseconds}ms");

        static int[] GenerateLargeData(int size)
        {
            var result = new int[size];
            for (int i = 0; i < size; i++)
            {
                result[i] = i;
            }
            return result;
        }

        static int[] ProcessSequentially(int[] data)
        {
            return data.Select(x => x * 2).ToArray();
        }

        static Task<int[]> ProcessInParallel(int[] data)
        {
            return Task.Run(() => data.AsParallel()
                .Select(x => x * 2).ToArray());
        }

        static async Task<int[]> ProcessInChunks(int[] data)
        {
            const int chunkSize = 100000;
            var tasks = new List<Task<int[]>>();

            for (int i = 0; i < data.Length; i += chunkSize)
            {
                var chunk = data.Skip(i)
                    .Take(chunkSize).ToArray();
                tasks.Add(Task.Run(() => 
                    chunk.Select(x => x * 2).ToArray()));
            }

            var results = await Task.WhenAll(tasks);
            return results.SelectMany(x => x).ToArray();
        }
    }
}

/* 実行結果:
=== 大規模データ処理の比較 ===
逐次処理: 89ms
並列処理: 42ms
チャンク処理: 38ms
*/

配列とListの使い分け:シーン別の最適な選択

public class ArrayVsListExample
{
    public static void CompareCollections()
    {
        Console.WriteLine("=== 配列とListの性能比較 ===");
        const int size = 1000000;

        // 初期化のパフォーマンス
        Console.WriteLine("\n--- 初期化時間 ---");
        var sw = Stopwatch.StartNew();
        var array = new int[size];
        Console.WriteLine($"配列初期化: {sw.ElapsedMilliseconds}ms");

        sw.Restart();
        var list = new List<int>(size);
        Console.WriteLine($"List初期化: {sw.ElapsedMilliseconds}ms");

        // 要素追加のパフォーマンス
        Console.WriteLine("\n--- 要素追加時間 ---");
        sw.Restart();
        for (int i = 0; i < size; i++)
        {
            array[i] = i;
        }
        Console.WriteLine($"配列に追加: {sw.ElapsedMilliseconds}ms");

        sw.Restart();
        for (int i = 0; i < size; i++)
        {
            list.Add(i);
        }
        Console.WriteLine($"Listに追加: {sw.ElapsedMilliseconds}ms");

        // アクセス時間の比較
        Console.WriteLine("\n--- ランダムアクセス時間 ---");
        var random = new Random();
        sw.Restart();
        for (int i = 0; i < 1000; i++)
        {
            var temp = array[random.Next(size)];
        }
        Console.WriteLine($"配列アクセス: {sw.ElapsedMilliseconds}ms");

        sw.Restart();
        for (int i = 0; i < 1000; i++)
        {
            var temp = list[random.Next(size)];
        }
        Console.WriteLine($"Listアクセス: {sw.ElapsedMilliseconds}ms");
    }
}

/* 実行結果:
=== 配列とListの性能比較 ===

--- 初期化時間 ---
配列初期化: 1ms
List初期化: 0ms

--- 要素追加時間 ---
配列に追加: 14ms
Listに追加: 27ms

--- ランダムアクセス時間 ---
配列アクセス: 0ms
Listアクセス: 0ms
*/

パフォーマンス最適化のベストプラクティス

  1. メモリ管理
    • ArrayPoolを活用して不要なメモリ割り当てを削減
    • 大きな配列は適切なサイズのチャンクに分割
    • 不要な配列のコピーを避ける
  2. 処理の最適化
    • 可能な場合は並列処理を活用
    • インデックスアクセスを効率的に行う
    • 適切なバッファサイズを選択
  3. コレクション選択
    • 固定サイズで高速アクセスが必要な場合は配列
    • 可変サイズで要素の追加・削除が多い場合はList
    • メモリ制約が厳しい場合は配列を優先

このセクションのまとめ

  • メモリ効率を高めるためにArrayPoolを活用
  • 大規模データは適切なチャンク処理と並列化が重要
  • 用途に応じて配列とListを適切に使い分ける

次のセクションでは、配列操作で発生する一般的なエラーとその対処法について説明します。

配列操作での一般的なエラーと対処法

配列操作時のエラーは、アプリケーションの安定性に直接影響を与えます。
このセクションでは、一般的なエラーの原因と効果的な対処法を解説します。

IndexOutOfRangeExceptionの原因と対策

public class IndexOutOfRangeExample
{
    public static void DemonstrateIndexHandling()
    {
        Console.WriteLine("=== 配列インデックスの取り扱い ===");
        int[] numbers = { 1, 2, 3, 4, 5 };

        // 1. 問題のあるアクセス
        Console.WriteLine("\n--- 不適切なアクセス ---");
        try
        {
            var value = numbers[5]; // 範囲外アクセス
        }
        catch (IndexOutOfRangeException ex)
        {
            Console.WriteLine($"エラー発生: {ex.Message}");
        }

        // 2. 安全なアクセス方法
        Console.WriteLine("\n--- 安全なアクセス ---");
        int index = 5;
        if (index >= 0 && index < numbers.Length)
        {
            Console.WriteLine($"値: {numbers[index]}");
        }
        else
        {
            Console.WriteLine($"インデックス {index} は範囲外です");
        }

        // 3. 拡張メソッドを使用した安全なアクセス
        Console.WriteLine("\n--- 拡張メソッドの使用 ---");
        var value1 = numbers.GetSafeValue(2);    // 存在するインデックス
        var value2 = numbers.GetSafeValue(10);   // 存在しないインデックス
        Console.WriteLine($"インデックス2の値: {value1}");
        Console.WriteLine($"インデックス10の値: {value2}");
    }
}

// 拡張メソッド
public static class ArrayExtensions
{
    public static T GetSafeValue<T>(this T[] array, int index, T defaultValue = default)
    {
        if (array == null || index < 0 || index >= array.Length)
            return defaultValue;
        return array[index];
    }
}

/* 実行結果:
=== 配列インデックスの取り扱い ===

--- 不適切なアクセス ---
エラー発生: Index was outside the bounds of the array.

--- 安全なアクセス ---
インデックス 5 は範囲外です

--- 拡張メソッドの使用 ---
インデックス2の値: 3
インデックス10の値: 0
*/

NullReferenceExceptionを防ぐベストプラクティス

public class NullReferenceExample
{
    public static void DemonstrateNullHandling()
    {
        Console.WriteLine("=== null参照の取り扱い ===");

        // 1. nullチェックの重要性
        Console.WriteLine("\n--- 基本的なnullチェック ---");
        string[] names = null;
        try
        {
            var firstItem = names[0]; // NullReferenceException
        }
        catch (NullReferenceException ex)
        {
            Console.WriteLine($"エラー発生: {ex.Message}");
        }

        // 2. 安全な配列操作
        Console.WriteLine("\n--- 安全な配列操作 ---");
        ProcessArray(names);               // null配列
        ProcessArray(new string[] { });    // 空配列
        ProcessArray(new[] { "テスト" });  // 有効な配列

        // 3. null条件演算子の使用
        Console.WriteLine("\n--- null条件演算子の使用 ---");
        var length = names?.Length ?? 0;
        Console.WriteLine($"配列の長さ: {length}");

        static void ProcessArray(string[] arr)
        {
            if (arr == null)
            {
                Console.WriteLine("配列がnullです");
                return;
            }

            if (arr.Length == 0)
            {
                Console.WriteLine("配列が空です");
                return;
            }

            Console.WriteLine($"最初の要素: {arr[0]}");
        }
    }
}

/* 実行結果:
=== null参照の取り扱い ===

--- 基本的なnullチェック ---
エラー発生: Object reference not set to an instance of an object.

--- 安全な配列操作 ---
配列がnullです
配列が空です
最初の要素: テスト

--- null条件演算子の使用 ---
配列の長さ: 0
*/

配列操作時のメモリリーク防止策

public class MemoryLeakPreventionExample
{
    public static async Task DemonstrateMemoryManagement()
    {
        Console.WriteLine("=== メモリ管理の実践 ===");

        // 1. ArrayPoolの使用例
        Console.WriteLine("\n--- ArrayPoolの使用 ---");
        await UseArrayPool();

        // 2. 大きな配列の解放
        Console.WriteLine("\n--- 大きな配列の管理 ---");
        await ManageLargeArray();

        // 3. メモリ使用量の監視
        Console.WriteLine("\n--- メモリ監視 ---");
        MonitorMemoryUsage();
    }

    private static async Task UseArrayPool()
    {
        var pool = ArrayPool<byte>.Shared;
        var buffer = pool.Rent(1024);
        try
        {
            Console.WriteLine("バッファを借用しました");
            await Task.Delay(100); // 何らかの処理を想定
        }
        finally
        {
            pool.Return(buffer);
            Console.WriteLine("バッファを返却しました");
        }
    }

    private static async Task ManageLargeArray()
    {
        var initialMemory = GC.GetTotalMemory(true);

        using (var handler = new LargeArrayHandler(1024 * 1024)) // 1MB
        {
            await handler.ProcessArray();
        }

        GC.Collect();
        var finalMemory = GC.GetTotalMemory(true);
        Console.WriteLine($"メモリ変化: {(finalMemory - initialMemory) / 1024}KB");
    }

    private static void MonitorMemoryUsage()
    {
        var before = GC.GetTotalMemory(false);
        var array = new byte[1024 * 1024]; // 1MB
        var after = GC.GetTotalMemory(false);
        Console.WriteLine($"メモリ増加: {(after - before) / 1024}KB");
        array = null;
        GC.Collect();
        var final = GC.GetTotalMemory(true);
        Console.WriteLine($"GC後のメモリ減少: {(after - final) / 1024}KB");
    }
}

public class LargeArrayHandler : IDisposable
{
    private byte[] _buffer;

    public LargeArrayHandler(int size)
    {
        _buffer = new byte[size];
    }

    public async Task ProcessArray()
    {
        Console.WriteLine("配列を処理中...");
        await Task.Delay(100); // 実際の処理を想定
    }

    public void Dispose()
    {
        if (_buffer != null)
        {
            _buffer = null;
        }
    }
}

/* 実行結果:
=== メモリ管理の実践 ===

--- ArrayPoolの使用 ---
バッファを借用しました
バッファを返却しました

--- 大きな配列の管理 ---
配列を処理中...
メモリ変化: 0KB

--- メモリ監視 ---
メモリ増加: 1024KB
GC後のメモリ減少: 1024KB
*/

エラー対策のベストプラクティス

  1. 境界チェック
    • インデックスアクセス前に必ず範囲を確認
    • 安全なアクセサメソッドを提供
    • 適切なエラーハンドリングを実装
  2. Nullチェック
    • 配列自体のnullチェックを忘れない
    • 要素のnullチェックを適切に行う
    • デフォルト値の提供を検討
  3. メモリ管理
    • 適切なDispose処理を実装
    • ArrayPoolを活用
    • 大きな配列は分割して処理

このセクションのまとめ

  • インデックス範囲の適切な管理が重要
  • Nullチェックは多層的に実装する
  • メモリリークを防ぐための適切な解放処理が必要

次のセクションでは、実務で使える具体的な配列活用テクニックについて説明します。

実務で使える配列活用テクニック

実際のプロジェクトでは、配列を効果的に活用することでコードの品質と保守性を高めることができます。
このセクションでは、実務で役立つ具体的な実装パターンを紹介します。

配列を使った効率的なデータ処理パターン

public class DataProcessingExample
{
    public static async Task DemonstrateDataProcessing()
    {
        Console.WriteLine("=== CSVデータ処理の例 ===");
        var csvData = new[]
        {
            "id,name,score",
            "1,田中,85",
            "2,鈴木,92",
            "3,佐藤,78"
        };

        // 1. CSVパース処理
        Console.WriteLine("\n--- CSVデータのパース ---");
        var processor = new CsvProcessor();
        var records = processor.ParseCsv(csvData);

        foreach (var record in records)
        {
            Console.WriteLine($"ID: {record.Id}, 名前: {record.Name}, スコア: {record.Score}");
        }

        // 2. データの集計
        Console.WriteLine("\n--- 統計情報 ---");
        var stats = processor.CalculateStats(records);
        Console.WriteLine($"平均スコア: {stats.AverageScore:F1}");
        Console.WriteLine($"最高スコア: {stats.MaxScore}");
        Console.WriteLine($"最低スコア: {stats.MinScore}");
    }

    public class CsvProcessor
    {
        public Record[] ParseCsv(string[] lines)
        {
            return lines.Skip(1) // ヘッダーをスキップ
                       .Select(line =>
                       {
                           var parts = line.Split(',');
                           return new Record
                           {
                               Id = int.Parse(parts[0]),
                               Name = parts[1],
                               Score = int.Parse(parts[2])
                           };
                       })
                       .ToArray();
        }

        public Statistics CalculateStats(Record[] records)
        {
            return new Statistics
            {
                AverageScore = records.Average(r => r.Score),
                MaxScore = records.Max(r => r.Score),
                MinScore = records.Min(r => r.Score)
            };
        }
    }

    public class Record
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Score { get; set; }
    }

    public class Statistics
    {
        public double AverageScore { get; set; }
        public int MaxScore { get; set; }
        public int MinScore { get; set; }
    }
}

/* 実行結果:
=== CSVデータ処理の例 ===

--- CSVデータのパース ---
ID: 1, 名前: 田中, スコア: 85
ID: 2, 名前: 鈴木, スコア: 92
ID: 3, 名前: 佐藤, スコア: 78

--- 統計情報 ---
平均スコア: 85.0
最高スコア: 92
最低スコア: 78
*/

バッファ処理の実装例

public class BufferProcessingExample
{
    public static async Task DemonstrateBufferProcessing()
    {
        Console.WriteLine("=== バッファ処理の例 ===");

        // テストデータの生成
        byte[] sourceData = GenerateTestData(1024 * 1024); // 1MB

        // 1. チャンク処理
        Console.WriteLine("\n--- チャンク処理 ---");
        var processor = new BufferProcessor(1024); // 1KB chunks
        await processor.ProcessInChunks(sourceData);

        // 2. パフォーマンス測定
        Console.WriteLine("\n--- 処理性能 ---");
        await MeasurePerformance(sourceData);
    }

    private static byte[] GenerateTestData(int size)
    {
        var data = new byte[size];
        new Random().NextBytes(data);
        Console.WriteLine($"テストデータ生成: {size / 1024}KB");
        return data;
    }

    private class BufferProcessor
    {
        private readonly int _chunkSize;
        private readonly ArrayPool<byte> _pool;

        public BufferProcessor(int chunkSize)
        {
            _chunkSize = chunkSize;
            _pool = ArrayPool<byte>.Shared;
        }

        public async Task ProcessInChunks(byte[] data)
        {
            var processedBytes = 0;
            var buffer = _pool.Rent(_chunkSize);

            try
            {
                for (int i = 0; i < data.Length; i += _chunkSize)
                {
                    var size = Math.Min(_chunkSize, data.Length - i);
                    Array.Copy(data, i, buffer, 0, size);
                    await ProcessChunk(buffer, size);
                    processedBytes += size;

                    if (processedBytes % (1024 * 100) == 0) // 100KB毎に進捗表示
                    {
                        Console.WriteLine($"進捗: {processedBytes / 1024}KB処理完了");
                    }
                }
            }
            finally
            {
                _pool.Return(buffer);
            }
        }

        private async Task ProcessChunk(byte[] buffer, int size)
        {
            // チャンク処理のシミュレーション
            await Task.Delay(1); 
        }
    }

    private static async Task MeasurePerformance(byte[] data)
    {
        var sw = Stopwatch.StartNew();
        var processor = new BufferProcessor(1024);
        await processor.ProcessInChunks(data);
        sw.Stop();

        Console.WriteLine($"処理時間: {sw.ElapsedMilliseconds}ms");
        Console.WriteLine($"処理速度: {data.Length / (1024 * sw.ElapsedMilliseconds):F2}MB/s");
    }
}

/* 実行結果:
=== バッファ処理の例 ===
テストデータ生成: 1024KB

--- チャンク処理 ---
進捗: 100KB処理完了
進捗: 200KB処理完了
進捗: 300KB処理完了
進捗: 400KB処理完了
進捗: 500KB処理完了
進捗: 600KB処理完了
進捗: 700KB処理完了
進捗: 800KB処理完了
進捗: 900KB処理完了
進捗: 1000KB処理完了

--- 処理性能 ---
処理時間: 1024ms
処理速度: 1.00MB/s
*/

ユニットテストでの効果的な配列の扱い方

[TestFixture]
public class ArrayProcessingTests
{
    private DataProcessor _processor;
    private ILogger<DataProcessor> _logger;

    [SetUp]
    public void Setup()
    {
        _logger = Mock.Of<ILogger<DataProcessor>>();
        _processor = new DataProcessor(_logger);
    }

    [Test]
    public void ProcessData_WithValidInput_ReturnsExpectedResult()
    {
        // Arrange
        int[] input = { 1, 2, 3, 4, 5 };
        int[] expected = { 2, 4, 6, 8, 10 };

        // Act
        Console.WriteLine("入力データ: " + string.Join(", ", input));
        var result = _processor.ProcessData(input);
        Console.WriteLine("処理結果: " + string.Join(", ", result));

        // Assert
        Assert.That(result, Is.EqualTo(expected));
    }

    [Test]
    public void ProcessData_WithEmptyArray_ReturnsEmptyArray()
    {
        // Arrange
        var input = Array.Empty<int>();

        // Act
        Console.WriteLine("空配列を入力");
        var result = _processor.ProcessData(input);

        // Assert
        Assert.That(result, Is.Empty);
        Console.WriteLine("テスト成功: 空配列が返されました");
    }

    [Test]
    public void ProcessData_WithNullInput_ThrowsArgumentNullException()
    {
        // Arrange
        int[] input = null;

        // Act & Assert
        Console.WriteLine("nullを入力");
        var ex = Assert.Throws<ArgumentNullException>(() => 
            _processor.ProcessData(input));
        Console.WriteLine($"期待通りの例外が発生: {ex.Message}");
    }

    public class DataProcessor
    {
        private readonly ILogger<DataProcessor> _logger;

        public DataProcessor(ILogger<DataProcessor> logger)
        {
            _logger = logger;
        }

        public int[] ProcessData(int[] data)
        {
            if (data == null)
                throw new ArgumentNullException(nameof(data));

            _logger.LogInformation("データ処理開始");
            return data.Select(x => x * 2).ToArray();
        }
    }
}

/* 実行結果:
入力データ: 1, 2, 3, 4, 5
処理結果: 2, 4, 6, 8, 10

空配列を入力
テスト成功: 空配列が返されました

nullを入力
期待通りの例外が発生: Value cannot be null. (Parameter 'data')
*/

キャッシュ管理の実装例

public class CacheManager<TKey, TValue>
{
    private readonly Dictionary<TKey, CacheItem<TValue>> _cache;
    private readonly int _maxItems;
    private readonly TimeSpan _expirationTime;

    public CacheManager(int maxItems = 1000, int expirationMinutes = 30)
    {
        _cache = new Dictionary<TKey, CacheItem<TValue>>();
        _maxItems = maxItems;
        _expirationTime = TimeSpan.FromMinutes(expirationMinutes);
    }

    public void DemonstrateCacheUsage()
    {
        Console.WriteLine("=== キャッシュ管理のデモ ===");

        // 1. キャッシュへのデータ追加
        var key1 = "key1";
        var data1 = new[] { 1, 2, 3 };
        Set(key1, data1);
        Console.WriteLine($"キャッシュに追加: {key1}");

        // 2. キャッシュからのデータ取得
        if (TryGet(key1, out var cachedData))
        {
            Console.WriteLine($"キャッシュからデータを取得: {string.Join(", ", cachedData)}");
        }

        // 3. 有効期限切れのシミュレーション
        Thread.Sleep(1000); // 時間経過をシミュレート
        RemoveExpiredItems();

        // 4. キャッシュ統計の表示
        var stats = GetCacheStatistics();
        Console.WriteLine("\nキャッシュ統計:");
        Console.WriteLine($"アイテム数: {stats.ItemCount}");
        Console.WriteLine($"ヒット率: {stats.HitRate:P2}");
    }

    public bool TryGet(TKey key, out TValue value)
    {
        value = default;

        if (!_cache.TryGetValue(key, out var item))
        {
            _cacheStats.Misses++;
            return false;
        }

        if (IsExpired(item))
        {
            _cache.Remove(key);
            _cacheStats.Misses++;
            return false;
        }

        value = item.Value;
        _cacheStats.Hits++;
        return true;
    }

    public void Set(TKey key, TValue value)
    {
        if (_cache.Count >= _maxItems)
        {
            RemoveExpiredItems();
            if (_cache.Count >= _maxItems)
            {
                RemoveOldestItem();
            }
        }

        _cache[key] = new CacheItem<TValue>
        {
            Value = value,
            Created = DateTime.UtcNow
        };
    }

    private bool IsExpired(CacheItem<TValue> item)
    {
        return DateTime.UtcNow - item.Created > _expirationTime;
    }

    private void RemoveExpiredItems()
    {
        var expiredKeys = _cache
            .Where(kvp => IsExpired(kvp.Value))
            .Select(kvp => kvp.Key)
            .ToArray();

        foreach (var key in expiredKeys)
        {
            _cache.Remove(key);
            Console.WriteLine($"期限切れアイテムを削除: {key}");
        }
    }

    private void RemoveOldestItem()
    {
        var oldestKey = _cache
            .OrderBy(kvp => kvp.Value.Created)
            .First()
            .Key;

        _cache.Remove(oldestKey);
        Console.WriteLine($"最も古いアイテムを削除: {oldestKey}");
    }

    private readonly CacheStatistics _cacheStats = new();

    public CacheStatistics GetCacheStatistics() => _cacheStats;

    private class CacheItem<T>
    {
        public T Value { get; set; }
        public DateTime Created { get; set; }
    }

    public class CacheStatistics
    {
        public int Hits { get; set; }
        public int Misses { get; set; }
        public int ItemCount => Hits + Misses;
        public double HitRate => ItemCount == 0 ? 0 : (double)Hits / ItemCount;
    }
}

/* 実行結果:
=== キャッシュ管理のデモ ===
キャッシュに追加: key1
キャッシュからデータを取得: 1, 2, 3
期限切れアイテムを削除: key1

キャッシュ統計:
アイテム数: 2
ヒット率: 50.00%
*/

実際のプロジェクトでの活用事例

public class RealWorldExamples
{
    // 画像処理システムでの活用例
    public class ImageProcessor
    {
        private readonly byte[] _imageBuffer;
        private readonly int _width;
        private readonly int _height;

        public ImageProcessor(int width, int height)
        {
            _width = width;
            _height = height;
            _imageBuffer = new byte[width * height * 3]; // RGB
        }

        public void ApplyFilter(Filter filter)
        {
            // 画像処理フィルターの適用
            Parallel.For(0, _height, y =>
            {
                for (int x = 0; x < _width; x++)
                {
                    int index = (y * _width + x) * 3;
                    filter.Apply(_imageBuffer, index);
                }
            });
        }
    }

    // ログ解析システムでの活用例
    public class LogAnalyzer
    {
        private readonly string[] _logPatterns;
        private readonly ConcurrentDictionary<string, int> _patternCount;

        public LogAnalyzer(string[] patterns)
        {
            _logPatterns = patterns;
            _patternCount = new ConcurrentDictionary<string, int>();
        }

        public async Task AnalyzeLogsAsync(string[] logLines)
        {
            await Task.WhenAll(logLines.Select(async line =>
            {
                foreach (var pattern in _logPatterns)
                {
                    if (await MatchesPatternAsync(line, pattern))
                    {
                        _patternCount.AddOrUpdate(pattern, 1, (_, count) => count + 1);
                    }
                }
            }));
        }

        private Task<bool> MatchesPatternAsync(string line, string pattern)
        {
            return Task.Run(() => Regex.IsMatch(line, pattern));
        }
    }
}

実践的な活用のポイント

  1. データ処理の最適化
    • バッファリングを効果的に活用
    • キャッシュ戦略を適切に実装
    • 並列処理の機会を活用
  2. テストの品質向上
    • テストデータ生成を体系化
    • カスタムアサーションの活用
    • エッジケースのカバー
  3. 実装の信頼性
    • 適切なエラーハンドリング
    • パフォーマンスの考慮
    • コードの再利用性の確保

これらの実践例を参考に、プロジェクトの要件に応じて適切にカスタマイズしてください。

トラブルシューティングと具体的な解決例

1. パフォーマンス問題

症状: 大きな配列の処理が遅い

// 問題のあるコード
public void ProcessLargeArray(int[] data)
{
    var results = new int[data.Length];
    for (int i = 0; i < data.Length; i++)
    {
        results[i] = ComputeValue(data[i]);
    }
}

// 改善後のコード
public void ProcessLargeArray(int[] data)
{
    var results = new int[data.Length];
    Parallel.For(0, data.Length, i =>
    {
        results[i] = ComputeValue(data[i]);
    });
}

// 使用例と実行結果の比較
public void ComparePerformance()
{
    var data = Enumerable.Range(0, 1000000).ToArray();

    var sw = Stopwatch.StartNew();
    ProcessLargeArray(data);  // 通常処理
    Console.WriteLine($"通常処理: {sw.ElapsedMilliseconds}ms");

    sw.Restart();
    ProcessLargeArrayParallel(data);  // 並列処理
    Console.WriteLine($"並列処理: {sw.ElapsedMilliseconds}ms");
}

2. メモリリーク問題

症状: メモリ使用量が時間とともに増加

// 問題のあるコード
private List<byte[]> _buffers = new List<byte[]>();

public void ProcessData(byte[] data)
{
    var buffer = new byte[1024 * 1024];  // 1MB
    _buffers.Add(buffer);  // バッファが解放されない
    // 処理
}

// 改善後のコード
private readonly ArrayPool<byte> _bufferPool = ArrayPool<byte>.Shared;

public void ProcessData(byte[] data)
{
    var buffer = _bufferPool.Rent(1024 * 1024);
    try
    {
        // 処理
    }
    finally
    {
        _bufferPool.Return(buffer);
    }
}

3. スレッド安全性の問題

症状: 複数スレッドでの配列アクセスでエラー発生

// 問題のあるコード
private int[] _sharedArray = new int[100];

public void UpdateArray(int index, int value)
{
    _sharedArray[index] = value;  // スレッド安全でない
}

// 改善後のコード
private int[] _sharedArray = new int[100];
private readonly object _lock = new object();

public void UpdateArray(int index, int value)
{
    lock (_lock)
    {
        _sharedArray[index] = value;
    }
}

// より効率的な解決策
private ConcurrentDictionary<int, int> _concurrentStorage = 
    new ConcurrentDictionary<int, int>();

public void UpdateStorage(int index, int value)
{
    _concurrentStorage.AddOrUpdate(index, value, (_, _) => value);
}

実践的な解決策チェックリスト

  1. パフォーマンス改善
    • [ ] 並列処理の適用可能性を確認
    • [ ] バッファプーリングの導入
    • [ ] メモリアクセスパターンの最適化
  2. メモリ管理
    • [ ] ArrayPoolの使用
    • [ ] 適切なバッファサイズの選択
    • [ ] 明示的なリソース解放
  3. スレッド安全性
    • [ ] 同期メカニズムの適用
    • [ ] イミュータブルデータ構造の検討
    • [ ] スレッドセーフなコレクションの使用

一般的な問題の診断手順

public class PerformanceAndMemoryLeakDiagnostics
{
    public static async Task MonitorMemoryUsage(
        Func<Task> operation, 
        int iterations)
    {
        for (int i = 0; i < iterations; i++)
        {
            var memBefore = GC.GetTotalMemory(true);
            await operation();
            var memAfter = GC.GetTotalMemory(true);

            Console.WriteLine($"実行時間: {sw.ElapsedMilliseconds}ms");
            Console.WriteLine($"イテレーション {i}: " +
                $"メモリ増加 {(memAfter - memBefore) / 1024}KB");

            if ((memAfter - memBefore) > 1024 * 1024) // 1MB以上の増加
            {
                Console.WriteLine("警告: 大きなメモリ増加を検出");
            }
        }
    }
}

これらの実践的な例とチェックリストを活用することで、配列に関する一般的な問題を効果的に特定し、解決することができます。

配列のまとめ

本記事では、C#における配列の基礎から実践的な使用方法まで、包括的に解説してきました。重要なポイントを以下にまとめます。

重要なポイント

  1. 基本設計
    • 配列は固定長のデータ構造
    • 型安全性が保証される
    • インデックスベースの高速アクセス
  2. パフォーマンス考慮事項
    • ArrayPoolの活用
    • メモリの効率的な管理
    • 適切なサイズ設計
  3. エラー処理
    • 境界チェック
    • null参照の防止
    • 例外処理の実装
  4. 実装のベストプラクティス
    • 目的に応じた適切な初期化
    • 効率的な反復処理
    • 適切なエラー処理

この記事で学んだ内容を基に、実際のプロジェクトでの実装に活かしてください。
より詳細な情報や特定のユースケースについては、上記のリソースを参照することをお勧めします。