この記事では、C#における配列の基本的な使い方から応用まで包括的に学習できます。一次元配列・多次元配列・ジャグ配列の宣言と初期化方法、暗黙的型付け、範囲アクセスなどの基本操作に加え、配列のコピー・結合・ソート・要素数取得といった実用的な操作方法を習得できます。また配列とListの使い分けや共変性による注意点も理解でき、演習問題で実践的なスキルも身につきます。
目次
C#配列の基本概念と仕組み
C#における配列は、同じ型の複数の要素を連続したメモリ領域に格納するデータ構造です。配列は参照型として扱われ、.NET Frameworkの基本的なコレクション機能として位置づけられています。
配列の最も重要な特徴は、各要素にインデックス番号を使って直接アクセスできることです。C#の配列では、インデックスは必ず0から始まり、配列の長さをnとした場合、最後の要素のインデックスはn-1となります。この仕組みにより、O(1)の時間計算量で任意の要素にアクセスすることが可能になります。
C#配列は以下の基本的な特性を持っています:
- 型安全性:宣言時に指定した型の要素のみを格納可能
- 固定サイズ:作成後にサイズを変更することはできない
- 参照型:配列変数は実際のデータへの参照を保持
- ヒープ領域への格納:配列のデータはマネージヒープに配置される
配列のメモリ構造について詳しく見ると、配列オブジェクトはヒープ上に連続して配置され、各要素は同じサイズのメモリ領域を占有します。例えば、int型の配列の場合、各要素は4バイトの領域を使用し、配列の先頭アドレスから計算によって各要素の正確な位置を特定できます。
C#では配列はSystem.Array
クラスから継承されており、この基底クラスから様々な便利なプロパティやメソッドを利用できます。Length プロパティによる要素数の取得や、GetLength() メソッドによる次元別の長さ取得などが代表的な機能として挙げられます。
また、C#配列は境界チェック機能を持っており、実行時に無効なインデックスでアクセスしようとするとIndexOutOfRangeException
が発生します。この安全性メカニズムにより、メモリ破損やセキュリティ上の脆弱性を防ぐことができます。
配列の種類 | 特徴 | 用途 |
---|---|---|
一次元配列 | 単一の次元を持つ最も基本的な配列 | リスト状のデータ管理 |
多次元配列 | 複数の次元を持つ矩形状の配列 | 行列やテーブル形式のデータ |
ジャグ配列 | 配列の配列として実装される可変長配列 | 不規則なデータ構造の表現 |
配列を効果的に活用するためには、これらの基本概念を理解した上で、適切な配列の種類を選択し、メモリ効率とパフォーマンスを考慮したプログラム設計を行うことが重要です。
配列の宣言と初期化方法
C#における配列の宣言と初期化は、データを効率的に管理するための基本的な操作です。配列を使用するには、まず適切な型とサイズを指定して宣言し、その後に値を設定する必要があります。C#では複数の方法で配列を宣言・初期化できるため、状況に応じて最適な方法を選択することが重要です。
基本的な配列宣言の書き方
C#で配列を宣言する最も基本的な方法は、データ型の後に角括弧[]を付けて配列型を指定し、new演算子を使用してメモリ領域を確保する方法です。この方法では、配列のサイズを明示的に指定する必要があります。
// 整数型配列の宣言(サイズ5)
int[] numbers = new int[5];
// 文字列型配列の宣言(サイズ3)
string[] names = new string[3];
// double型配列の宣言(サイズ10)
double[] prices = new double[10];
この宣言方法では、配列は各データ型のデフォルト値で初期化されます。数値型の場合は0、文字列型の場合はnull、bool型の場合はfalseが自動的に設定されます。配列の各要素には、インデックス番号を使用してアクセスし、後から値を代入することができます。
// 宣言後に値を設定
int[] scores = new int[3];
scores[0] = 85;
scores[1] = 92;
scores[2] = 78;
値を同時に設定する初期化方法
配列の宣言と同時に初期値を設定する方法は、事前に格納したいデータが決まっている場合に非常に便利です。この方法を使用すると、配列のサイズは初期化リスト内の要素数によって自動的に決定されます。
// 配列リテラルを使用した初期化
int[] numbers = new int[] { 10, 20, 30, 40, 50 };
// 文字列配列の初期化
string[] cities = new string[] { "東京", "大阪", "名古屋", "福岡" };
// bool型配列の初期化
bool[] flags = new bool[] { true, false, true, false };
さらに簡潔な書き方として、new演算子と型指定を省略した初期化方法も利用できます。この方法では、波括弧内に初期値を直接記述することで、より読みやすいコードを作成できます。
// 簡潔な初期化方法
int[] ages = { 25, 30, 28, 35, 42 };
string[] colors = { "赤", "青", "緑", "黄", "紫" };
double[] temperatures = { 25.5, 28.3, 22.1, 30.8 };
型推論を活用した配列作成
C#のvar キーワードを使用することで、型推論機能を活用した配列宣言が可能です。この方法では、初期化時に指定された値から配列の型が自動的に推論されるため、コードの記述量を削減しながら型安全性を保つことができます。
// var キーワードを使用した型推論
var studentIds = new[] { 1001, 1002, 1003, 1004 }; // int[]として推論
var subjects = new[] { "数学", "英語", "理科", "社会" }; // string[]として推論
var rates = new[] { 0.08, 0.10, 0.05, 0.12 }; // double[]として推論
型推論を使用する際の重要な注意点として、初期化リスト内のすべての要素が同じ型である必要があります。異なる型の要素が混在している場合、コンパイルエラーが発生します。また、空の配列を作成する場合は、明示的に型を指定する必要があります。
// 混在型でのエラー例(コンパイルエラー)
// var mixedArray = new[] { 1, "文字列", 3.14 }; // エラー
// 空配列の場合は明示的な型指定が必要
var emptyNumbers = new int[0]; // OK
var emptyStrings = new string[] { }; // OK
型推論を活用することで、コードの保守性が向上し、特に配列の型が複雑な場合やジェネリック型を扱う際に効果的です。ただし、コードの可読性を考慮して、型が明確でない場合は明示的な型指定を使用することを推奨します。
一次元配列の操作方法
C#で配列を宣言した後は、実際にデータを格納したり取得したりする操作が必要になります。一次元配列の操作では、インデックスを使った要素アクセス、配列サイズの確認、そしてメソッドでの配列の受け渡しが基本的な操作となります。これらの操作を正しく理解することで、効率的なプログラムを作成できるようになります。
配列要素への値の設定と取得
C#配列では、インデックス番号を使用して特定の要素に値を設定したり、値を取得したりできます。インデックスは0から始まることに注意が必要です。
値の設定は以下のように行います:
int[] numbers = new int[5];
numbers[0] = 10; // 最初の要素に10を設定
numbers[1] = 20; // 2番目の要素に20を設定
numbers[2] = 30; // 3番目の要素に30を設定
配列要素の値を取得する場合も同様にインデックスを使用します:
int[] scores = {85, 92, 78, 96, 88};
int firstScore = scores[0]; // 85を取得
int lastScore = scores[4]; // 88を取得
Console.WriteLine($"最初のスコア: {firstScore}");
Console.WriteLine($"最後のスコア: {lastScore}");
配列の範囲外のインデックスにアクセスしようとすると、IndexOutOfRangeException例外が発生するため注意が必要です。配列操作時は常に有効な範囲内でのアクセスを心がけましょう。
配列サイズの確認方法
C#配列のサイズを確認するには、Lengthプロパティを使用します。このプロパティは配列が持つ要素数を返すため、ループ処理や範囲チェックで重要な役割を果たします。
string[] cities = {"東京", "大阪", "名古屋", "福岡", "札幌"};
int arraySize = cities.Length;
Console.WriteLine($"配列のサイズ: {arraySize}"); // 5が出力される
Lengthプロパティを活用したループ処理の例:
double[] temperatures = {25.5, 28.2, 23.8, 30.1, 26.9};
// 全ての要素を順番に表示
for (int i = 0; i temperatures.Length; i++)
{
Console.WriteLine($"気温[{i}]: {temperatures[i]}度");
}
// 平均気温の計算
double sum = 0;
for (int i = 0; i temperatures.Length; i++)
{
sum += temperatures[i];
}
double average = sum / temperatures.Length;
Console.WriteLine($"平均気温: {average:F1}度");
Lengthプロパティを使用することで、配列サイズが変更されてもコードを修正する必要がなくなり、保守性の高いプログラムを作成できます。
メソッドパラメータとしての配列渡し
C#では配列をメソッドの引数として渡すことができ、これにより配列を処理する機能を関数として分離できます。配列は参照型のため、メソッドに渡された配列への変更は呼び出し元にも反映されます。
基本的な配列の引数渡しの例:
static void DisplayArray(int[] array)
{
Console.WriteLine("配列の内容:");
for (int i = 0; i array.Length; i++)
{
Console.Write($"{array[i]} ");
}
Console.WriteLine();
}
static void Main()
{
int[] numbers = {1, 2, 3, 4, 5};
DisplayArray(numbers); // メソッドに配列を渡す
}
配列要素を変更するメソッドの例:
static void MultiplyByTwo(int[] array)
{
for (int i = 0; i array.Length; i++)
{
array[i] *= 2; // 各要素を2倍にする
}
}
static int FindMaxValue(int[] array)
{
int max = array[0];
for (int i = 1; i array.Length; i++)
{
if (array[i] > max)
{
max = array[i];
}
}
return max;
}
static void Main()
{
int[] values = {10, 5, 8, 3, 12};
Console.WriteLine("変更前:");
DisplayArray(values);
MultiplyByTwo(values); // 配列の各要素を2倍にする
Console.WriteLine("変更後:");
DisplayArray(values);
int maxValue = FindMaxValue(values);
Console.WriteLine($"最大値: {maxValue}");
}
メソッドから配列を返すことも可能です:
static int[] CreateSequence(int start, int count)
{
int[] sequence = new int[count];
for (int i = 0; i count; i++)
{
sequence[i] = start + i;
}
return sequence;
}
static void Main()
{
int[] mySequence = CreateSequence(5, 7); // 5から始まる7個の連続数
DisplayArray(mySequence); // 5 6 7 8 9 10 11 が表示される
}
配列をメソッドパラメータとして活用することで、コードの再利用性が高まり、プログラム全体の構造が明確になります。特に配列処理が複雑になる場合は、機能ごとにメソッドを分割することが推奨されます。
多次元配列の活用
C#では一次元配列だけでなく、二次元や三次元といった多次元配列も扱うことができます。多次元配列は表計算やゲームのマップデータ、画像処理など、複雑なデータ構造を表現する際に非常に有効です。C#には矩形多次元配列とジャグ配列という2つの異なるアプローチがあり、それぞれ特徴と適用場面が異なります。
矩形多次元配列の作成と操作
矩形多次元配列は、すべての行が同じ列数を持つ規則的な配列構造です。二次元配列を例にとると、表やマトリックスのような矩形のデータを効率的に管理できます。
// 矩形二次元配列の宣言と初期化
int[,] matrix = new int[3, 4]; // 3行4列の配列
// 値の設定
matrix[0, 0] = 10;
matrix[1, 2] = 25;
// 初期化時に値を設定
int[,] scores = {
{80, 90, 85},
{75, 88, 92},
{95, 87, 89}
};
// 三次元配列の例
int[,,] cube = new int[2, 3, 4]; // 2×3×4の三次元配列
矩形多次元配列の操作には、GetLength()メソッドを使用して各次元のサイズを取得できます。また、foreachループやfor文を使った要素の走査も可能です。
// 配列のサイズ取得
int rows = scores.GetLength(0); // 行数
int cols = scores.GetLength(1); // 列数
// 全要素の走査
for (int i = 0; i rows; i++)
{
for (int j = 0; j cols; j++)
{
Console.WriteLine($"scores[{i},{j}] = {scores[i, j]}");
}
}
ジャグ配列(配列の配列)の実装
ジャグ配列は配列の配列とも呼ばれ、各行が異なる列数を持つことができる柔軟な配列構造です。メモリ使用量の最適化や、不規則なデータ構造の表現に適しています。
// ジャグ配列の宣言と初期化
int[][] jaggedArray = new int[3][];
// 各行に異なるサイズの配列を割り当て
jaggedArray[0] = new int[4]; // 1行目は4要素
jaggedArray[1] = new int[2]; // 2行目は2要素
jaggedArray[2] = new int[5]; // 3行目は5要素
// 初期化時に値を設定する方法
int[][] studentGrades = {
new int[] {85, 90, 78, 92},
new int[] {88, 95},
new int[] {75, 80, 85, 90, 87}
};
// 値の設定と取得
jaggedArray[0][1] = 100;
int value = jaggedArray[2][3];
ジャグ配列の走査では、各行の長さが異なるため、Length プロパティを使用して動的にサイズを確認する必要があります。
// ジャグ配列の全要素走査
for (int i = 0; i studentGrades.Length; i++)
{
Console.Write($"Student {i}: ");
for (int j = 0; j studentGrades[i].Length; j++)
{
Console.Write($"{studentGrades[i][j]} ");
}
Console.WriteLine();
}
多次元配列をメソッドに渡す方法
多次元配列をメソッドのパラメータとして渡す際は、配列の型を正確に指定する必要があります。矩形多次元配列とジャグ配列では異なる記述方法を使用します。
// 矩形多次元配列を受け取るメソッド
public static void ProcessMatrix(int[,] matrix)
{
int rows = matrix.GetLength(0);
int cols = matrix.GetLength(1);
for (int i = 0; i rows; i++)
{
for (int j = 0; j cols; j++)
{
matrix[i, j] *= 2; // 各要素を2倍にする
}
}
}
// ジャグ配列を受け取るメソッド
public static void ProcessJaggedArray(int[][] jaggedArray)
{
for (int i = 0; i jaggedArray.Length; i++)
{
for (int j = 0; j jaggedArray[i].Length; j++)
{
jaggedArray[i][j] += 10; // 各要素に10を加算
}
}
}
// メソッドの使用例
int[,] scores = {{80, 90}, {75, 85}};
ProcessMatrix(scores);
int[][] grades = {new int[] {70, 80}, new int[] {90}};
ProcessJaggedArray(grades);
refやoutキーワードを使用することで、配列自体の参照を変更することも可能です。また、配列を返り値として返すメソッドも定義できます。
// 多次元配列を返すメソッド
public static int[,] CreateMatrix(int rows, int cols)
{
int[,] result = new int[rows, cols];
int counter = 1;
for (int i = 0; i rows; i++)
{
for (int j = 0; j cols; j++)
{
result[i, j] = counter++;
}
}
return result;
}
各配列形式の性能比較
矩形多次元配列とジャグ配列には、メモリ使用量とアクセス速度の面で異なる特性があります。適切な選択により、アプリケーションのパフォーマンスを最適化できます。
項目 | 矩形多次元配列 | ジャグ配列 |
---|---|---|
メモリ効率 | 高い(連続したメモリ領域) | やや劣る(間接参照が必要) |
アクセス速度 | 高速(直接アクセス) | やや遅い(二段階アクセス) |
柔軟性 | 低い(固定サイズ) | 高い(可変サイズ) |
初期化の容易さ | シンプル | やや複雑 |
実際のパフォーマンス測定では、以下のようなコードで比較できます:
// パフォーマンス比較用のサンプルコード
Stopwatch sw = new Stopwatch();
// 矩形多次元配列のテスト
int[,] rectArray = new int[1000, 1000];
sw.Start();
for (int i = 0; i 1000; i++)
{
for (int j = 0; j 1000; j++)
{
rectArray[i, j] = i + j;
}
}
sw.Stop();
Console.WriteLine($"矩形配列: {sw.ElapsedMilliseconds}ms");
// ジャグ配列のテスト
sw.Restart();
int[][] jaggedArray = new int[1000][];
for (int i = 0; i 1000; i++)
{
jaggedArray[i] = new int[1000];
for (int j = 0; j 1000; j++)
{
jaggedArray[i][j] = i + j;
}
}
sw.Stop();
Console.WriteLine($"ジャグ配列: {sw.ElapsedMilliseconds}ms");
選択の指針として、規則的なデータ構造で高いパフォーマンスが必要な場合は矩形多次元配列を、データ構造が不規則でメモリ使用量を最適化したい場合はジャグ配列を選択することが推奨されます。
配列の実用的な操作テクニック
C#での配列操作において、基本的な宣言や初期化を理解した後に必要となるのが、実際のプログラムで頻繁に使用される実用的な操作テクニックです。配列要素の追加、データのコピー、複数配列の結合、要素の並び替えといった操作は、日常的な開発作業で欠かせない技術となります。これらの操作を適切に実装することで、効率的で保守性の高いプログラムを作成できるようになります。
配列要素の追加処理
C#の配列は固定サイズであるため、直接的な要素追加はできませんが、新しい配列を作成して既存データを移すことで追加処理を実現できます。最も基本的な方法は、元の配列より1つ大きなサイズの新しい配列を作成し、既存要素をコピーしてから新要素を追加する手法です。
int[] originalArray = {1, 2, 3, 4, 5};
int[] newArray = new int[originalArray.Length + 1];
// 既存要素のコピー
for (int i = 0; i originalArray.Length; i++)
{
newArray[i] = originalArray[i];
}
// 新要素の追加
newArray[newArray.Length - 1] = 6;
より効率的な方法として、Array.Resize
メソッドを使用することで、参照を変更しながら配列サイズを動的に変更できます。この方法では、内部的に新しい配列が作成され、既存データが自動的にコピーされます。
int[] numbers = {1, 2, 3, 4, 5};
Array.Resize(ref numbers, numbers.Length + 1);
numbers[numbers.Length - 1] = 6;
配列データのコピー手法
配列データのコピーは、プログラム開発において重要な操作の一つです。C#では複数のコピー手法が提供されており、それぞれ異なる特徴と用途があります。最も基本的な方法は、Array.Copy
メソッドを使用した静的コピーです。
int[] sourceArray = {10, 20, 30, 40, 50};
int[] destinationArray = new int[sourceArray.Length];
// 配列全体のコピー
Array.Copy(sourceArray, destinationArray, sourceArray.Length);
// 部分コピーの例
int[] partialCopy = new int[3];
Array.Copy(sourceArray, 1, partialCopy, 0, 3); // インデックス1から3要素をコピー
インスタンスメソッドであるCopyTo
を使用する方法もあり、こちらは元配列のメソッドとして呼び出す形式で、より直感的な記述が可能です。
int[] original = {100, 200, 300};
int[] copy = new int[original.Length];
original.CopyTo(copy, 0);
浅いコピーと深いコピーの違いも重要な概念です。参照型オブジェクトを含む配列の場合、Array.Copy
は浅いコピーを行うため、オブジェクトの参照のみがコピーされ、実際のオブジェクトは共有されます。
複数配列の結合方法
複数の配列を結合して一つの配列にまとめる処理は、データの統合や集約処理でよく使用されます。手動でのループ処理による結合から、LINQを活用した効率的な結合まで、様々な手法があります。
基本的な手動結合では、結合後のサイズを計算してから新しい配列を作成し、各配列の要素を順次コピーします。
int[] array1 = {1, 2, 3};
int[] array2 = {4, 5, 6};
int[] array3 = {7, 8, 9};
// 結合後のサイズを計算
int totalLength = array1.Length + array2.Length + array3.Length;
int[] combinedArray = new int[totalLength];
// 各配列を順次コピー
Array.Copy(array1, 0, combinedArray, 0, array1.Length);
Array.Copy(array2, 0, combinedArray, array1.Length, array2.Length);
Array.Copy(array3, 0, combinedArray, array1.Length + array2.Length, array3.Length);
LINQのConcat
メソッドを使用すると、より簡潔で読みやすいコードで配列結合を実現できます。
using System.Linq;
int[] first = {1, 2, 3};
int[] second = {4, 5, 6};
int[] third = {7, 8, 9};
int[] result = first.Concat(second).Concat(third).ToArray();
配列要素の並び替え
配列要素の並び替えは、データ処理において基本的かつ重要な操作です。C#では、Array.Sort
メソッドによる標準的なソート機能から、カスタムの比較ロジックを用いた高度なソートまで、柔軟な並び替え機能が提供されています。
最も基本的な昇順ソートは、Array.Sort
メソッドを使用して簡単に実行できます。このメソッドは元の配列を直接変更するため、元の順序を保持したい場合は事前にコピーを作成する必要があります。
int[] numbers = {5, 2, 8, 1, 9, 3};
// 昇順ソート(元配列が変更される)
Array.Sort(numbers);
// 降順ソート
Array.Sort(numbers);
Array.Reverse(numbers);
カスタムの比較ロジックを実装したい場合は、IComparer
インターフェースを実装するか、比較デリゲートを指定することで実現できます。
string[] words = {"apple", "pie", "cherry", "cake"};
// 文字列長でソート
Array.Sort(words, (x, y) => x.Length.CompareTo(y.Length));
// カスタムクラスのソート例
Person[] people = {
new Person("Alice", 30),
new Person("Bob", 25),
new Person("Carol", 35)
};
// 年齢でソート
Array.Sort(people, (p1, p2) => p1.Age.CompareTo(p2.Age));
LINQを活用すると、元の配列を変更せずに新しいソート済み配列を生成できるため、関数型プログラミングのアプローチでより安全な並び替えが可能です。
int[] original = {5, 2, 8, 1, 9, 3};
int[] sortedAscending = original.OrderBy(x => x).ToArray();
int[] sortedDescending = original.OrderByDescending(x => x).ToArray();
動的配列(List型)との使い分け
C#における配列と動的配列は、それぞれ異なる特性を持つデータ構造です。配列は固定サイズでメモリ効率が良い一方、動的配列は要素数の変更が可能で柔軟性に優れています。開発において適切なデータ構造を選択することは、パフォーマンスと保守性の両面で重要な意味を持ちます。ここでは、動的配列の代表的な実装であるArrayListとジェネリック型List、そしてそれらの操作方法について詳しく解説します。
ArrayListクラスの特徴と使用法
ArrayListクラスは、.NET Frameworkの初期から提供されている動的配列の実装です。このクラスの最大の特徴は、異なる型のオブジェクトを同一のコレクションに格納できる点にあります。内部的にはobject型の配列として実装されており、要素の追加や削除に応じて自動的にサイズが調整されます。
using System.Collections;
ArrayList arrayList = new ArrayList();
arrayList.Add(10);
arrayList.Add("文字列");
arrayList.Add(3.14);
// 要素へのアクセス(キャストが必要)
int number = (int)arrayList[0];
string text = (string)arrayList[1];
ArrayListの利点は、型に関係なく様々なオブジェクトを格納できる柔軟性です。しかし、この柔軟性には代償があります。要素を取得する際には明示的な型キャストが必要であり、実行時に型の不整合によるエラーが発生する可能性があります。また、値型を格納する際にはボックス化とアンボックス化が発生し、パフォーマンスの低下を招くことがあります。
- 動的なサイズ変更が可能
- 異なる型のオブジェクトを同一コレクションに格納
- 型安全性の欠如とパフォーマンスの課題
- 現在では主にレガシーコードでの使用に限定
ジェネリック型Listの活用
ジェネリック型のList<T>クラスは、ArrayListの課題を解決する現代的な動的配列の実装です。型パラメータTを使用することで、コンパイル時に型安全性が保証され、実行時エラーのリスクを大幅に減らすことができます。また、値型を扱う際のボックス化も回避されるため、パフォーマンスの向上も期待できます。
using System.Collections.Generic;
// 整数型のList
List<int> numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);
numbers.Add(30);
// 文字列型のList
List<string> names = new List<string> { "田中", "佐藤", "山田" };
// 要素数の確認
int count = numbers.Count;
// 要素の削除
numbers.Remove(20);
List<T>は豊富なメソッドを提供しており、配列では煩雑になりがちな操作を簡潔に記述できます。例えば、Find()メソッドによる条件検索、ForEach()メソッドによる一括処理、Sort()メソッドによる並び替えなど、実用的な機能が充実しています。
メソッド | 機能 | 使用例 |
---|---|---|
Add() | 要素の追加 | list.Add(item) |
Insert() | 指定位置への挿入 | list.Insert(0, item) |
Remove() | 要素の削除 | list.Remove(item) |
Clear() | 全要素の削除 | list.Clear() |
Listデータのコピーと挿入操作
実際の開発において、Listのデータコピーや挿入操作は頻繁に必要となる処理です。これらの操作を正しく理解し、適切に実装することで、効率的なプログラムを作成できます。特に、参照型と値型の違いを理解した上でのコピー操作は、予期しないバグを防ぐために重要です。
List<int> originalList = new List<int> { 1, 2, 3, 4, 5 };
// シャローコピー(参照のコピー)
List<int> shallowCopy = new List<int>(originalList);
// 配列を経由したコピー
List<int> arrayCopy = originalList.ToArray().ToList();
// 範囲指定でのコピー
List<int> rangeCopy = originalList.GetRange(1, 3);
// 他のListとの結合
List<int> anotherList = new List<int> { 6, 7, 8 };
originalList.AddRange(anotherList);
挿入操作においては、Insert()メソッドとInsertRange()メソッドが利用できます。Insert()は単一要素の挿入、InsertRange()は複数要素の一括挿入に使用します。これらの操作では、挿入位置以降の要素が後方にシフトされるため、大量のデータを扱う際はパフォーマンスへの影響も考慮する必要があります。
List<string> fruits = new List<string> { "りんご", "みかん", "ぶどう" };
// 単一要素の挿入
fruits.Insert(1, "バナナ");
// 複数要素の挿入
List<string> newFruits = new List<string> { "いちご", "メロン" };
fruits.InsertRange(0, newFruits);
// 結果: いちご, メロン, りんご, バナナ, みかん, ぶどう
List<T>は配列と比較して、要素数の動的変更が可能である点で優れており、現代のC#開発では配列よりも頻繁に使用されるデータ構造です。一方で、固定サイズのデータを扱う場合や、最大限のパフォーマンスが要求される場面では、従来の配列の方が適している場合もあります。適切な選択により、効率的なアプリケーションを構築することが可能になります。
配列の範囲指定アクセス機能
C#では配列の特定の範囲にアクセスするための便利な機能が提供されており、これらを活用することで効率的な配列操作が可能になります。従来の単一要素へのアクセスに加えて、複数の要素を一括で処理する方法を理解することで、より高度なC#配列操作が実現できます。
C# 8.0以降では、Range演算子(..)とIndex演算子(^)が導入され、配列の範囲指定アクセスが大幅に簡素化されました。これらの機能により、従来のループ処理を使わずに配列の部分的な操作が可能となります。
Index演算子を使用した末尾からのアクセス方法では、配列の長さを意識せずに後方からの要素にアクセスできます:
int[] numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
// 末尾から1番目の要素(最後の要素)にアクセス
int lastElement = numbers[^1]; // 結果: 10
// 末尾から3番目の要素にアクセス
int thirdFromEnd = numbers[^3]; // 結果: 8
Range演算子を活用した範囲指定では、配列の一部分を効率的に取得できます。この機能により、配列のスライス操作が簡潔に記述できるようになります:
int[] originalArray = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
// インデックス2から5未満までの範囲を取得
int[] subArray1 = originalArray[2..5]; // 結果: {2, 3, 4}
// 最初から3つの要素を取得
int[] subArray2 = originalArray[..3]; // 結果: {0, 1, 2}
// インデックス7から末尾までを取得
int[] subArray3 = originalArray[7..]; // 結果: {7, 8, 9}
Index演算子とRange演算子を組み合わせた応用的な使用方法では、より柔軟な範囲指定が可能になります。特に動的なサイズの配列を扱う際に威力を発揮します:
string[] names = {"田中", "佐藤", "鈴木", "高橋", "伊藤", "渡辺"};
// 末尾から2番目の要素から末尾まで取得
string[] lastTwo = names[^2..]; // 結果: {"伊藤", "渡辺"}
// 最初の要素から末尾から2番目の要素まで取得
string[] exceptLastTwo = names[..^2]; // 結果: {"田中", "佐藤", "鈴木", "高橋"}
// 真ん中の範囲を取得(末尾基準で範囲指定)
string[] middle = names[1..^1]; // 結果: {"佐藤", "鈴木", "高橋", "伊藤"}
配列の範囲指定アクセス機能を活用する際の重要なポイントとして、パフォーマンス面での考慮があります。Range演算子による操作は新しい配列を作成するため、大量のデータを扱う場合はメモリ使用量に注意が必要です:
- Range演算子は元の配列をコピーして新しい配列を作成します
- 頻繁な範囲指定操作はガベージコレクションの負荷を増加させる可能性があります
- 読み取り専用の操作にはSpan<T>やMemory<T>の使用も検討できます
- 大きな配列の一部分のみを処理する場合は、インデックスループとの性能比較が重要です
実用的な応用例として、配列データの分割処理や条件に基づく部分抽出など、様々な場面でこれらの機能を活用できます。例えば、ログデータの最新部分の抽出や、配列の前半と後半を分けた処理などが簡潔に記述できるため、C#配列操作の効率化に大きく貢献します。
配列操作の実践問題と解答例
C#における配列の理解を深めるために、実際のプログラミング現場でよく遭遇する問題を取り上げ、詳細な解答例とともに解説します。これらの問題を通じて、配列操作の基本から応用まで幅広いテクニックを身につけることができるでしょう。
問題1: 配列内の最大値と最小値を見つける
整数配列から最大値と最小値を同時に見つけるプログラムを作成してください。
int[] numbers = { 45, 12, 89, 3, 67, 23, 91, 8 };
// 解答例
int max = numbers[0];
int min = numbers[0];
for (int i = 1; i numbers.Length; i++)
{
if (numbers[i] > max)
max = numbers[i];
if (numbers[i] min)
min = numbers[i];
}
Console.WriteLine($"最大値: {max}, 最小値: {min}");
この問題では、配列の最初の要素を初期値として設定し、残りの要素を順次比較する手法を使用しています。一度のループで両方の値を取得できるため、効率的な処理が可能です。
問題2: 配列の要素を逆順に並び替える
与えられた配列を破壊的に逆順に並び替えるメソッドを実装してください。
// 解答例
static void ReverseArray(int[] arr)
{
int left = 0;
int right = arr.Length - 1;
while (left right)
{
// 要素の交換
int temp = arr[left];
arr[left] = arr[right];
arr[right] = temp;
left++;
right--;
}
}
// 使用例
int[] testArray = { 1, 2, 3, 4, 5 };
ReverseArray(testArray);
// 結果: { 5, 4, 3, 2, 1 }
この解答では、両端からインデックスを進める手法を採用しており、配列の半分だけを処理することで効率的な逆順処理を実現しています。
問題3: 二次元配列の行列転置
正方行列の転置を行うプログラムを作成してください。
// 解答例
static void TransposeMatrix(int[,] matrix)
{
int size = matrix.GetLength(0);
for (int i = 0; i size; i++)
{
for (int j = i + 1; j size; j++)
{
// 要素の交換
int temp = matrix[i, j];
matrix[i, j] = matrix[j, i];
matrix[j, i] = temp;
}
}
}
// 使用例
int[,] matrix = {
{ 1, 2, 3 },
{ 4, 5, 6 },
{ 7, 8, 9 }
};
TransposeMatrix(matrix);
// 結果: { {1,4,7}, {2,5,8}, {3,6,9} }
多次元配列を扱うこの問題では、対角線より上の要素のみを処理することで、効率的な転置処理を実現しています。
問題4: ジャグ配列を使った成績管理システム
各クラスの生徒数が異なる学校で、全クラスの平均点を計算するプログラムを作成してください。
// 解答例
static double CalculateOverallAverage(int[][] classScores)
{
int totalStudents = 0;
int totalScore = 0;
foreach (int[] classScore in classScores)
{
foreach (int score in classScore)
{
totalScore += score;
totalStudents++;
}
}
return totalStudents > 0 ? (double)totalScore / totalStudents : 0.0;
}
// 使用例
int[][] scores = {
new int[] { 85, 90, 78, 92 }, // クラス1: 4人
new int[] { 88, 76, 95, 83, 91 }, // クラス2: 5人
new int[] { 79, 87, 84 } // クラス3: 3人
};
double average = CalculateOverallAverage(scores);
Console.WriteLine($"全体の平均点: {average:F2}");
この問題では、ジャグ配列の柔軟性を活かして、サイズが異なる配列群を効率的に処理する方法を示しています。
問題5: 配列の重複要素を除去
整数配列から重複する要素を除去し、ユニークな要素のみを含む新しい配列を返すメソッドを実装してください。
// 解答例
static int[] RemoveDuplicates(int[] arr)
{
List uniqueList = new List();
foreach (int item in arr)
{
if (!uniqueList.Contains(item))
{
uniqueList.Add(item);
}
}
return uniqueList.ToArray();
}
// より効率的な解答例(HashSetを使用)
static int[] RemoveDuplicatesEfficient(int[] arr)
{
HashSet uniqueSet = new HashSet(arr);
return uniqueSet.ToArray();
}
// 使用例
int[] numbers = { 1, 2, 2, 3, 4, 4, 5, 1 };
int[] unique = RemoveDuplicatesEfficient(numbers);
// 結果: { 1, 2, 3, 4, 5 }
この問題では、二つの異なるアプローチを示しており、HashSetを使用した方法は大量のデータに対してより高いパフォーマンスを発揮します。
手法 | 時間計算量 | 適用場面 |
---|---|---|
Listによる重複チェック | O(n²) | 小規模なデータセット |
HashSetによる重複除去 | O(n) | 大規模なデータセット |
これらの実践問題を通じて、C#における配列操作の様々なテクニックを習得できます。実際の開発現場では、パフォーマンスと可読性のバランスを考慮して最適な手法を選択することが重要です。