C#プログラミングにおいて、配列を特定の値で初期化する作業は非常に頻繁に発生します。
例えば、アルゴリズムの初期値として「-1」を設定したり、文字列配列を空文字で埋めたり、あるいはゲーム開発でフラグを一括でリセットしたりする場合など、その用途は多岐にわたります。
C#には初期のバージョンから存在する「ループ処理」による方法に加え、.NETの進化とともに登場したArray.FillやLINQを活用した効率的な記述方法が数多く用意されています。
本記事では、単純な数値配列から参照型の配列、さらには最新のC#で推奨されるパフォーマンスを意識した書き方までを網羅的に解説します。
状況に合わせた最適な手法を選択できるよう、各メソッドの内部挙動やメリット・デメリットを深く掘り下げていきましょう。
配列初期化の基本と既定値の振る舞い
C#で配列を宣言した際、明示的に値を指定しない場合、各要素は型に応じた既定値(default値)で自動的に初期化されます。
例えば、数値型であれば「0」、参照型(クラスなど)であれば「null」、論理型であれば「false」となります。
しかし、実務では「全要素を100にしたい」「すべての要素を特定の文字列で埋めたい」といったケースがほとんどです。
まずは、最も伝統的で理解しやすいループによる初期化から見ていきましょう。
forループによる手動初期化
もっとも基本的な方法は、forループを使用して要素一つひとつに値を代入する方法です。
using System;
class Program
{
static void Main()
{
// 長さ10の整数配列を宣言
int[] numbers = new int[10];
// すべての要素を「-1」で初期化
for (int i = 0; i < numbers.Length; i++)
{
numbers[i] = -1;
}
// 結果の出力
Console.WriteLine(string.Join(", ", numbers));
}
}
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1
この方法の利点は、古いバージョンの.NET環境でも動作し、ロジックが極めて明快であることです。
しかし、配列のサイズが大きくなるとコードの記述量が増え、単純な代入作業にしては冗長に感じられることがあります。
Array.Fillによる高速かつ簡潔な初期化
.NET Core 2.0および.NET Standard 2.1以降では、Array.Fillという静的メソッドが導入されました。
現在、C#で配列を同じ値で埋める際のデファクトスタンダード(標準的な手法)となっています。
Array.Fillの基本的な使い方
Array.Fillを使用すると、ループを書く必要がなくなり、一行で記述が完了します。
using System;
class Program
{
static void Main()
{
string[] labels = new string[5];
// すべての要素を "Empty" で初期化
Array.Fill(labels, "Empty");
Console.WriteLine(string.Join(", ", labels));
}
}
Empty, Empty, Empty, Empty, Empty
指定した範囲だけを埋める方法
Array.Fillにはオーバーロードが存在し、配列の一部だけを特定の値で埋めることも可能です。
これは、バッファの一部をリセットする場合などに非常に便利です。
using System;
class Program
{
static void Main()
{
int[] data = new int[10];
// インデックス3から4つの要素を「99」で埋める
Array.Fill(data, 99, 3, 4);
Console.WriteLine(string.Join(", ", data));
}
}
0, 0, 0, 99, 99, 99, 99, 0, 0, 0
Array.Fill(配列, 値, 開始インデックス, 要素数)という引数の構成になっています。
要素数を超えたインデックスを指定すると例外が発生するため、境界チェックには注意が必要です。
LINQを使用した宣言的な初期化
関数型プログラミングに近い記述を好む場合、あるいは配列の生成と同時に初期化を行いたい場合は、LINQのEnumerable.Repeatが有用です。
Enumerable.Repeatの活用
このメソッドは、指定された値を指定された回数繰り返す列挙(IEnumerable)を生成します。
using System;
using System.Linq;
class Program
{
static void Main()
{
// 5を10個持つ配列を生成
int[] repeatedValues = Enumerable.Repeat(5, 10).ToArray();
Console.WriteLine(string.Join(", ", repeatedValues));
}
}
5, 5, 5, 5, 5, 5, 5, 5, 5, 5
LINQを使用する際の注意点
Enumerable.Repeatは非常に読みやすいコードになりますが、Array.Fillと比較するとパフォーマンス面で不利になる場合があります。
内部的に列挙子(Enumerator)を生成し、ToArray()によって新しいメモリ割り当てが発生するためです。
大量のデータを扱うループ内で頻繁に呼び出す場合は、パフォーマンス劣化を招く恐れがあるため、利便性と速度のトレードオフを考慮する必要があります。
参照型の配列を初期化する際の落とし穴
数値(int)や構造体(struct)などの値型であれば問題ありませんが、クラスなどの参照型を配列で扱う場合には、重大な注意点があります。
同一インスタンスの共有問題
Array.FillやEnumerable.Repeatを使って参照型を初期化すると、配列の全要素が「同じインスタンス」を指してしまいます。
using System;
class User
{
public string Name { get; set; }
}
class Program
{
static void Main()
{
User[] users = new User[3];
// すべての要素に同じインスタンスを代入
Array.Fill(users, new User { Name = "Unknown" });
// 0番目の要素の名前を変更すると...
users[0].Name = "Alice";
// 全要素の名前が変わってしまう
Console.WriteLine($"[0]: {users[0].Name}, [1]: {users[1].Name}, [2]: {users[2].Name}");
}
}
[0]: Alice, [1]: Alice, [2]: Alice
このように、一つの要素を書き換えたつもりが、すべての要素に影響を与えてしまうバグは非常に発見しにくいものです。
各要素に個別のインスタンスを割り当てたい場合は、必ずループ内で個別にnewを行う必要があります。
個別のインスタンスで初期化する正しい方法
User[] users = new User[3];
for (int i = 0; i < users.Length; i++)
{
users[i] = new User { Name = "Unknown" };
}
あるいは、LINQのSelectを利用して生成することも可能です。
User[] users = Enumerable.Range(0, 3)
.Select(_ => new User { Name = "Unknown" })
.ToArray();
Span<T>を活用した最新の初期化手法
パフォーマンスが極めて重視されるシステムや、スタック領域を利用するモダンなC#開発においては、Span<T>のFillメソッドも選択肢に入ります。
Span<T>.Fillのメリット
Span<T>はメモリの連続した領域を抽象化する型であり、配列よりも抽象度が高く、かつ高速な操作が可能です。
using System;
class Program
{
static void Main()
{
int[] numbers = new int[100];
Span<int> span = numbers;
// Span経由で一括初期化
span.Fill(-1);
Console.WriteLine(numbers[0]); // -1
}
}
Span.Fillは、内部的にランタイムの最適化(SIMDの活用など)が強く効くため、非常に巨大な配列を初期化する際に、Array.Fillと同等以上のパフォーマンスを発揮します。
また、配列の一部を切り出した「スライス」に対しても同一の記法で操作できるため、柔軟性に優れています。
パフォーマンス比較と手法の選び方
初期化手法を検討する際の判断基準を以下の表にまとめました。
| 手法 | 簡潔さ | パフォーマンス | 推奨される用途 |
|---|---|---|---|
| forループ | △ | ◎ | 独自の複雑な計算が必要な場合 |
| Array.Fill | ◎ | ◎ | 一般的な配列の一括初期化(推奨) |
| Enumerable.Repeat | ○ | △ | 配列の生成と初期化を一行で書きたい場合 |
| Span<T>.Fill | ○ | ◎ | 高パフォーマンスが要求されるメモリ操作 |
| 手動代入(リテラル) | ○ | ◎ | 要素数が極めて少なく、値が固定の場合 |
基本的には「Array.Fill」を選択すれば間違いありません。
コードの可読性が高く、フレームワーク側で最適化されているためです。
大規模配列における最適化のヒント
さらに高度な最適化が必要な場合、たとえば数百万、数千万要素の配列を扱うシーンでは、単なる初期化すらボトルネックになることがあります。
- ArrayPool (System.Buffers.ArrayPool<T>)
頻繁に巨大な配列を生成・破棄するとガベージコレクション(GC)の負荷が高まります。
System.Buffers.ArrayPool<T>を使用して配列を再利用し、必要に応じてArray.Fillでリセットすることで再利用時の安全性を保ち、GC負荷の低減とパフォーマンス改善が期待できます。- stackalloc
stackallocは小さな一時的な配列に対してスタック上にメモリを確保します。Span.Fillで初期化すればヒープ割り当てを避けてGCを完全に回避できます。ただしスタックサイズに制約があるため、用途は小さな配列や短命なバッファに限定してください。
まとめ
C#で配列を同じ値で初期化する方法は、言語とフレームワークの進化とともに洗練されてきました。
- Array.Fill は、最も汎用的で高速な標準手法です。
- LINQ (Enumerable.Repeat) は、宣言的で読みやすいものの、オーバーヘッドに注意が必要です。
- 参照型の配列を初期化する際は、全要素が同じインスタンスを指さないよう、ループや
Selectで個別にnewする必要があります。 - パフォーマンスがクリティカルな場面では、Span<T>.Fillや
ArrayPoolの活用を検討してください。
用途に応じた適切な初期化方法を選択することで、コードの可読性を保ちつつ、実行効率の高いアプリケーションを構築することができます。
まずは、最もバランスの良いArray.Fillから積極的に活用していきましょう。






