配列(array)とは同じデータ型の要素が集まったデータ構造で、固定長の集合、ベクトルです。 ここでは配列の宣言・作成方法と、配列を使った基本的な操作について見ていきます。 配列内にある要素の検索や、配列のリサイズといった操作については配列操作で解説します。
なお、ここでは次元のない配列(1次元配列)のみを扱います。 多次元配列については多次元配列・ジャグ配列で個別に解説します。 また可変長のデータ構造としてリストなどのコレクションなども存在しますが、ここでは扱いません。
配列とは
プログラム要素として見た場合の配列とは、複数の同じ種類(=同じ型)の値に名前を与えてひとまとめにして扱うものです。 複数の変数の場合は個々の値にそれぞれ名前が与えられますが、配列では値の集まりにひとつの名前(=配列の変数名)が与えられ、配列内の個々の値(要素)はインデックスによって区別します(インデックスは添え字とも呼ばれる)。
using System;
class Sample {
static void Main()
{
// 複数の変数
var user0 = "Alice";
var user1 = "Bob";
var user2 = "Charlie";
// 配列
var users = new string[3];
users[0] = "Alice";
users[1] = "Bob";
users[2] = "Charlie";
}
}
Imports System
Class Sample
Shared Sub Main()
' 複数の変数
Dim user0 As String = "Alice"
Dim user1 As String = "Bob"
Dim user2 As String = "Charlie"
' 配列
Dim users(2) As String
users(0) = "Alice"
users(1) = "Bob"
users(2) = "Charlie"
End Sub
End Class
上記の例で言えば、user0
, user1
, user2
の各変数は名前から関連性が示唆されるものの独立した値として存在します。 一方users
は、インデックスによって個々の値が区別されると同時に、その値の集合全体にも名前が与えられていて、ひとまとめの値として存在します。
user0 | "Alice" |
---|
user1 | "Bob" |
---|
user2 | "Charlie" |
---|
users | |
---|---|
0 | "Alice" |
1 | "Bob" |
2 | "Charlie" |
配列の作成
言語によって構文に多少の違いはありますが、配列を作成するには、次のように格納される要素の型と格納できる要素数(配列の長さ)を指定して行います。 構文は似通っていますが、C#では格納できる要素数、VBではインデックスの最大値を指定するという間違いやすい相違点があるので、両言語を並行して扱う際は注意が必要です。
using System;
class Sample {
static void Main()
{
// 長さが3(格納できる要素数が3)の文字列型配列を作成
var arr1 = new string[3];
// 長さが12(格納できる要素数が12)のint型配列を作成
var arr2 = new int[12];
// 長さが0(格納できる要素数が0)のint型配列を作成
var arr3 = new int[0];
}
}
Imports System
Class Sample
Shared Sub Main()
' 長さが3(インデックスの最大値が2)の文字列型配列を作成
Dim arr1(2) As String
' 長さが12(インデックスの最大値が11)のInteger型配列を作成
Dim arr2(11) As Integer
' 長さが0(インデックスの最大値が-1)のInteger型配列を作成
Dim arr3(-1) As Integer
End Sub
End Class
上記の例のように長さ(要素数)が0の配列(=要素が1つも格納されない配列)も作成することが出来ます。 長さ・要素数を0には出来ますが、当然マイナスにすることは出来ません。
長さが0の配列については後述の§.空の配列でも詳しく解説します。
配列の作成方法のバリエーションについては後述の§.配列の宣言と初期化を参照してください。
要素の参照
.NETにおける配列では、要素のインデックス(通番・要素の番号)は0を基準とします(zero-based)。 つまり、最初の要素のインデックスは0、その次の要素のインデックスは1、さらにその次は2…、となります。 以降の解説でも、0番目とは最初の要素を指すものとして説明していきます。 C#では配列の変数に続けてかぎ括弧 [ ]、VBでは丸括弧 ( )を使ってインデックスを指定します。
using System;
class Sample {
static void Main()
{
// 長さが3(格納できる要素数が3)の文字列型配列を作成
var users = new string[3];
users[0] = "Alice"; // 配列の0番目(最初)の要素として"Alice"を設定
users[1] = "Bob"; // 配列の1番目の要素として"Bob"を設定
users[2] = "Charlie"; // 配列の2番目(最後)の要素として"Charlie"を設定
// 配列の1番目に格納されているの値を取得して表示
var user1 = users[1];
Console.WriteLine(user1);
}
}
Imports System
Class Sample
Shared Sub Main()
' 長さが3(インデックスの最大値が2)の文字列型配列を作成
Dim users(2) As String
users(0) = "Alice" ' 配列の0番目(最初)の要素として"Alice"を設定
users(1) = "Bob" ' 配列の1番目の要素として"Bob"を設定
users(2) = "Charlie" ' 配列の2番目(最後)の要素として"Charlie"を設定
' 配列の1番目に格納されているの値を取得して表示
Dim user1 As String = users(1)
Console.WriteLine(user1)
End Sub
End Class
Bob
配列は固定長であり、配列を作成するときに格納できる要素数が決定されます。 配列の長さ(格納できる要素数)を超えるインデックスを指定すると実行時に例外IndexOutOfRangeExceptionがスローされます。 配列が自動的に拡張されるといったことは行われません。
using System;
class Sample {
static void Main()
{
// 長さが3(格納できる要素数が3)の文字列型配列を作成
var users = new string[3];
users[2] = "Charlie"; // 配列の2番目(最後)の要素として"Charlie"を設定 (問題なく動作する)
users[3] = "Dave"; // 配列の3番目(最後の一つ後)の要素として"Dave"を設定 (インデックスが範囲外のため例外がスローされる)
}
}
Imports System
Class Sample
Shared Sub Main()
' 長さが3(インデックスの最大値が2)の文字列型配列を作成
Dim users(2) As String
users(2) = "Charlie" ' 配列の2番目(最後)の要素として"Charlie"を設定 (例外がスローされる)
users(3) = "Dave" ' 配列の3番目(最後の一つ後)の要素として"Dave"を設定 (インデックスが範囲外のため例外がスローされる)
End Sub
End Class
ハンドルされていない例外: System.IndexOutOfRangeException: インデックスが配列の境界外です。 場所 Sample.Main()
つまり、JavaScript等の言語では認められている次のような操作は、.NETにおける配列ではサポートされません。
// 配列を作成
var users = new Array();
users[2] = "Charlie"; // 配列の2番目に"Charlie"を設定 (ここでarr.length = 3となる)
users[3] = "Dave"; // 配列の3番目に"Dave"を設定 (ここでarr.length = 4となる)
可変長の配列が必要な場合は、ジェネリックコレクションクラスの一つであるListクラスを使うことができます。 ただし、Listクラスでも上記のように途中のインデックスに要素を追加するようなことはできません。
既存の配列を拡張する必要がある場合はArray.Resizeメソッドを使うことができます。
長さの取得
実行時に配列の長さ(配列の要素数・サイズ)を取得するには、Lengthプロパティを参照します。
using System;
class Sample {
static void Main()
{
// 長さが3の配列
var arr1 = new string[3];
// 長さが12の配列
var arr2 = new int[12];
// 長さが0の配列
var arr3 = new double[0];
// それぞれの配列の長さを取得して表示
Console.WriteLine("arr1.Length = {0}", arr1.Length);
Console.WriteLine("arr2.Length = {0}", arr2.Length);
Console.WriteLine("arr3.Length = {0}", arr3.Length);
}
}
Imports System
Class Sample
Shared Sub Main()
' 長さが3の配列
Dim arr1(2) As String
' 長さが12の配列
Dim arr2(11) As Integer
' 長さが0の配列
Dim arr3(-1) As Double
' それぞれの配列の長さを取得して表示
Console.WriteLine("arr1.Length = {0}", arr1.Length)
Console.WriteLine("arr2.Length = {0}", arr2.Length)
Console.WriteLine("arr3.Length = {0}", arr3.Length)
End Sub
End Class
arr1.Length = 3 arr2.Length = 12 arr3.Length = 0
多次元配列の場合、Lengthプロパティは多次元配列内の全要素数を返します。 (多次元配列・ジャグ配列 §.次元ごとの長さ・次元数・要素数の取得 (Length/GetLength/Rank))
配列のバイト数を取得する場合は、Buffer.ByteLengthメソッドを使うことができます。 詳しくはバイト列操作 §.配列のバイト数の取得 (ByteLengthメソッド)を参照してください。
配列の宣言と初期化
ここまでで紹介した例では、配列変数の宣言と、配列の作成・代入を同時に行っていました。 ここでは配列の宣言と、配列の作成・初期化の方法を分けて見ていきます。
配列の宣言
まず、配列の宣言について見ていきます。 配列を格納する変数を宣言するには、次のようにします。
using System;
class Sample {
static void Main()
{
string[] arr1; // string型の配列を格納する変数
int[] arr2; // int型の配列を格納する変数
double[] arr3; // double型の配列を格納する変数
}
}
Imports System
Class Sample
Shared Sub Main()
Dim arr1 As String() ' String型の配列を格納する変数
Dim arr2 As Integer() ' Integer型の配列を格納する変数
Dim arr3 As Double() ' Double型の配列を格納する変数
' 次のように丸括弧を変数名の後ろに付けても同じ
'Dim arr1() As String ' String型の配列を格納する変数
'Dim arr2() As Integer ' Integer型の配列を格納する変数
'Dim arr3() As Double ' Double型の配列を格納する変数
End Sub
End Class
VBでは、丸括弧( )を変数名の後ろに付けた場合も、型名の後ろに付けた場合も、同じ配列変数として扱われます。 どちらも意味は同じです。
このようにして宣言した変数は、配列が格納できる変数が用意されるだけで、実際にはどの配列も参照していない(配列の実体を参照していない)状態となっています。 C#では未初期化の変数、VBではヌル参照の変数となります。 この状態から、配列を作成して変数に代入したり、他の変数が参照している配列やメソッドの戻り値として返される配列などを変数に代入したりする必要があります。
配列の初期化
続いて配列の初期化について見ていきます。 次のようにすることで、配列変数の宣言と同時に配列を作成して変数に代入することが出来ます。
using System;
class Sample {
static void Main()
{
// 長さが3の配列を作成し、各要素にそれぞれ文字列"Alice", "Bob", "Charlie"を設定する
var arr1 = new string[3] {"Alice", "Bob", "Charlie"};
var arr2 = new string[] {"Alice", "Bob", "Charlie"};
string[] arr3 = {"Alice", "Bob", "Charlie"};
// 長さが5の配列を作成し、各要素にそれぞれ数値0, 1, 2, 3, 4を設定する
var arr4 = new int[5] {0, 1, 2, 3, 4};
var arr5 = new int[] {0, 1, 2, 3, 4};
int[] arr6 = {0, 1, 2, 3, 4};
}
}
Imports System
Class Sample
Shared Sub Main()
' 長さが3の配列を作成し、各要素にそれぞれ文字列"Alice", "Bob", "Charlie"を設定する
Dim arr1 As String() = New String(2) {"Alice", "Bob", "Charlie"}
Dim arr2 As String() = New String() {"Alice", "Bob", "Charlie"}
Dim arr3 As String() = {"Alice", "Bob", "Charlie"}
' 長さが5の配列を作成し、各要素にそれぞれ数値0, 1, 2, 3, 4を設定する
Dim arr4 As Integer() = New Integer(4) {0, 1, 2, 3, 4}
Dim arr5 As Integer() = New Integer() {0, 1, 2, 3, 4}
Dim arr6 As Integer() = {0, 1, 2, 3, 4}
End Sub
End Class
このコードにおいて、arr1, arr2, arr3はすべて同じ長さで同じ内容が格納された配列となります。 arr4, arr5, arr6も同様に同じ内容が格納されています。 このコードのnew以降の部分は配列作成式、中括弧 { } の部分は配列初期化子と呼ばれます。 配列作成式で配列の長さを指定する場合、配列初期化子で記述されている要素数と合っていないとコンパイルエラーとなります。
using System;
class Sample {
static void Main()
{
// 配列の長さと、配列初期化子の要素数が一致していないのでコンパイルエラーとなる
var arr = new string[2] {"Alice", "Bob", "Charlie"};
// error CS0847: An array initializer of length `2' was expected
}
}
Imports System
Class Sample
Shared Sub Main()
' 配列の長さと、配列初期化子の要素数が一致していないのでコンパイルエラーとなる
Dim arr As String() = New String(1) {"Alice", "Bob", "Charlie"}
' error BC30568: 配列初期化子に 1 個の要素が余分に含まれています
End Sub
End Class
配列初期化子を記述しない場合は、指定された長さの配列が作成されますが、作成される配列内の各要素にはデフォルト値が設定されます。
using System;
class Sample {
static void Main()
{
var arr1 = new string[3]; // 長さが3のstring型配列
var arr2 = new int[5]; // 長さが5のint型配列
var arr3 = new double[12]; // 長さが12のdouble型配列
}
}
Imports System
Class Sample
Shared Sub Main()
Dim arr1 As String() = New String(2) {} ' 長さが3のString型配列
Dim arr2 As Integer() = New Integer(4) {} ' 長さが5のInteger型配列
Dim arr3 As Double() = New Double(11) {} ' 長さが12のDouble型配列
' 次のように記述しても同じ
'Dim arr1(2) As String ' 長さが3のString型配列
'Dim arr2(4) As Integer ' 長さが5のInteger型配列
'Dim arr3(11) As Double ' 長さが12のDouble型配列
End Sub
End Class
デフォルト値は型によって異なりますが、数値などの値型では0
や0
に相当する値、文字列型やクラスなどの参照型ではヌル参照(null/Nothing)となります。
型とデフォルト値の詳細については型の種類・サイズ・精度・値域 §.型のデフォルト値、型の種類については値型と参照型を参照してください。
配列初期化子を記述しない場合は、次のように変数に代入されている値を長さとして配列を作成することもできます。 この場合も、各要素にデフォルト値が設定された状態の配列が作成されます。
using System;
class Sample {
static void Main()
{
var length = 3;
// 長さに変数lengthの値を指定して文字列配列を作成する
var arr = new string[length];
// 作成された配列の長さを取得して表示する
Console.WriteLine(arr.Length);
}
}
Imports System
Class Sample
Shared Sub Main()
Dim length As Integer = 3
' 長さに変数lengthの値を指定して文字列配列を作成する
Dim arr As String() = New String(length - 1) {}
' 作成された配列の長さを取得して表示する
Console.WriteLine(arr.Length)
End Sub
End Class
3
長さ0の配列
長さ0の配列(空の配列)の作成については後述の§.空の配列で解説します。
配列フィールドの宣言
ローカル変数の場合と同様、クラスや構造体のフィールドとしても配列を宣言することもできます。
using System;
class C {
// クラス内の配列フィールド
int[] ArrayField;
}
struct S {
// 構造体内の配列フィールド
int[] ArrayField;
}
Imports System
Class C
' クラス内の配列フィールド
Dim ArrayField() As Integer
End Class
Structure S
' 構造体内の配列フィールド
Dim ArrayField() As Integer
End Structure
ただし、構造体内の配列フィールドには初期化子を指定することはできません。 一方、クラスの場合は配列フィールドに初期化子を指定することができます。
using System;
class C {
// クラス内では配列フィールドに初期化子を指定することができる
int[] ArrayField = new int[5];
}
struct S {
// 構造体内では配列フィールドに初期化子を指定することはできない
int[] ArrayField = new int[5]; // error CS0573: 'S.ArrayField': 構造体にインスタンスフィールド初期化子を指定することはできません。
}
Imports System
Class C
' クラス内では配列フィールドに初期化子を指定することができる
Dim ArrayField() As Integer = New Integer(4) {}
End Class
Structure S
' 構造体内では配列フィールドに初期化子を指定することはできない
Dim ArrayField() As Integer = New Integer(4) {} ' error BC31049: 構造体メンバー上の初期化子は、'Shared' メンバーおよび定数にのみ有効です。
End Structure
構造体内の配列フィールドと初期化子については構造体と配列フィールドで詳しく解説しています。
配列の代入
配列は参照型です。 そのため、配列の変数に代入を行う場合、配列がまるごとコピーされて代入されるのではなく、配列への参照が代入されるだけとなる点に注意が必要です。
次の例のように、配列の変数に代入を行っても配列の実体は一つであるため、異なる変数を経由した変更でも実際に行われる変更は同一の配列に対するものとなります。
using System;
class Sample {
static void Main()
{
// 配列を作成してarr1に代入
string[] arr1 = {"Alice", "Bob", "Charlie"};
// arr1の各要素を表示
Console.WriteLine("arr1[0] = {0}", arr1[0]);
Console.WriteLine("arr1[1] = {0}", arr1[1]);
Console.WriteLine("arr1[2] = {0}", arr1[2]);
Console.WriteLine();
// arr1に代入されている配列への参照をarr2に代入
var arr2 = arr1;
arr2[1] = "Dave"; // arr2の1番目の値を変更
// arr1の各要素を表示
Console.WriteLine("arr1[0] = {0}", arr1[0]);
Console.WriteLine("arr1[1] = {0}", arr1[1]);
Console.WriteLine("arr1[2] = {0}", arr1[2]);
}
}
Imports System
Class Sample
Shared Sub Main()
' 配列を作成してarr1に代入
Dim arr1 As String() = {"Alice", "Bob", "Charlie"}
' arr1の各要素を表示
Console.WriteLine("arr1(0) = {0}", arr1(0))
Console.WriteLine("arr1(1) = {0}", arr1(1))
Console.WriteLine("arr1(2) = {0}", arr1(2))
Console.WriteLine()
' arr1に代入されている配列への参照をarr2に代入
Dim arr2 As String() = arr1
arr2(1) = "Dave" ' arr2の1番目の値を変更
' arr1の各要素を表示
Console.WriteLine("arr1(0) = {0}", arr1(0))
Console.WriteLine("arr1(1) = {0}", arr1(1))
Console.WriteLine("arr1(2) = {0}", arr1(2))
End Sub
End Class
arr1[0] = Alice arr1[1] = Bob arr1[2] = Charlie arr1[0] = Alice arr1[1] = Dave arr1[2] = Charlie
配列のコピーを行いたい場合はArray.CloneメソッドまたはArray.Copyメソッドを使います。
要素の列挙
配列内の各要素を列挙するには、for文とforeach文が使えます。 for文では配列のインデックスを使って各要素を参照するのに対し、foreach文では個別の変数によって列挙された各要素を参照します。
using System;
class Sample {
static void Main()
{
// 配列を作成
string[] arr = {"Alice", "Bob", "Charlie"};
// for文を使って配列内の各要素を列挙
for (var i = 0; i < arr.Length; i++) {
Console.WriteLine("arr[{0}] = {1}", i, arr[i]);
}
Console.WriteLine();
// foreach文を使って配列内の各要素を列挙
// (列挙した各要素は変数elemに代入される)
foreach (var elem in arr) {
Console.WriteLine(elem);
}
}
}
Imports System
Class Sample
Shared Sub Main()
' 配列を作成
Dim arr As String() = {"Alice", "Bob", "Charlie"}
' Forステートメントを使って配列内の各要素を列挙
For i As Integer = 0 To arr.Length - 1
Console.WriteLine("arr({0}) = {1}", i, arr(i))
Next
Console.WriteLine()
' For Eachステートメントを使って配列内の各要素を列挙
' (列挙した各要素は変数elemに代入される)
For Each elem As String In arr
Console.WriteLine(elem)
Next
End Sub
End Class
arr[0] = Alice arr[1] = Bob arr[2] = Charlie Alice Bob Charlie
foreach文では常に配列の先頭から一つずつ列挙されるため、配列の末尾から先頭に向けて列挙したり、一つ飛ばしで列挙したりといったことは出来ません。 そういった列挙が必要な場合はforeach文ではなくfor文を使って記述する必要があります。
for文とforeach文の使い分けは概ね以下のようになると言えます。
- for文
- 列挙する際に要素の位置(インデックス)が重要となる場合
インデックスによって各要素に対して行う処理を振り分けたい場合(インデックスn以上の要素だけ参照したい、インデックスがnのときだけ処理を行いたいなど) - foreach文
- 列挙する際に要素の位置(インデックス)が重要でない場合
インデックスに関わらずすべての要素に同じ処理を行いたい場合
配列内から条件に合う要素を検索・取得したりするにはArrayクラスのFindメソッドやExistsメソッドなどを使うことができます。 また、配列内の要素の順番を逆にして列挙したい場合はReverseメソッドを使うことができます。
for文とforeach文による配列の列挙では、パフォーマンス上の差はほとんどありません。
foreach文での列挙は配列に実装されているIEnumerableインターフェイスから提供される機能によって実現しています。 配列とIEnumerableインターフェイスについては後述の§.配列のインターフェイス、IEnumerableインターフェイスと列挙についてはIEnumerable・IEnumeratorで詳しく解説しています。
要素の検索・ソート、配列全体への変更
配列内にある要素を検索する、ソートを行う、また配列のサイズを変更する、コピーする、配列全体の型変換を行うといった操作は、Arrayクラスに用意されているユーティリティメソッドを使うことで行うことができます。 Arrayクラスを使った配列の操作については配列操作で解説しています。
また、ソートに関しては個別に基本型のソートと昇順・降順でのソートでも詳しく解説しています。
空の配列
.NETでは空の配列を作成することができます。 空の配列とは、長さが0で要素が1つも格納されていないあるいは1つも格納できない配列です。
構文を用いた作成
空の配列(長さ0の配列)を作成したい場合は次のように記述することができます。
using System;
class Sample {
static void Main()
{
// 以下のいずれも空の配列となる
var arr1 = new string[0];
var arr2 = new string[] {};
}
}
Imports System
Class Sample
Shared Sub Main()
' 以下のいずれも空の配列となる
Dim arr1 As String() = New String(-1) {}
Dim arr2 As String() = New String() {}
Dim arr3(-1) As String
End Sub
End Class
空の配列はヌル参照の状態とは明確に異なります。 空の配列は、配列の実体を参照しているがその配列にはなにも格納されていない状態になります。 ヌル参照では、配列自体を参照していない状態になります。
using System;
class Sample {
static void Main()
{
var arrEmpty = new string[0];
// 空の配列ではプロパティの参照などを行うことができる
Console.WriteLine(arrEmpty.Length);
string[] arrNull = null;
// ヌル参照の配列変数ではプロパティの参照などを行うことはできない
Console.WriteLine(arrNull.Length); // NullReferenceExceptionがスローされる
}
}
Imports System
Class Sample
Shared Sub Main()
Dim arrEmpty As String() = New String() {}
' 空の配列ではプロパティの参照などを行うことができる
Console.WriteLine(arrEmpty.Length)
Dim arrNull As String() = Nothing
' ヌル参照の配列変数ではプロパティの参照などを行うことはできない
Console.WriteLine(arrNull.Length) ' NullReferenceExceptionがスローされる
End Sub
End Class
Array.Emptyメソッドを用いた作成
.NET Framework 4.6以降ではArray.Empty<T>メソッドを使うことでも空の配列を取得することができます。 それより前のバージョンでは、Emumerable.Emptyメソッドの戻り値に対してToArrayメソッドを呼び出すことで空の配列を作成することができます。
using System;
using System.Linq;
class Sample {
static void Main()
{
// Array.Emptyメソッドを使って空の配列を取得する(.NET Framework 4.6以降)
var arr1 = Array.Empty<string>();
var arr2 = Array.Empty<int>();
// Enumerable.Emptyメソッドを使って空の配列を作成する(.NET Framework 3.5以降)
string[] arr3 = Enumerable.Empty<string>().ToArray();
int[] arr4 = Enumerable.Empty<int>().ToArray();
}
}
Imports System
Class Sample
Shared Sub Main()
' Array.Emptyメソッドを使って空の配列を取得する(.NET Framework 4.6以降)
Dim arr1 As String() = Array.Empty(Of String)()
Dim arr2 As Integer() = Array.Empty(Of Integer)()
' Enumerable.Emptyメソッドを使って空の配列を作成する(.NET Framework 3.5以降)
Dim arr3 As String() = Enumerable.Empty(Of String)().ToArray()
Dim arr4 As Integer() = Enumerable.Empty(Of Integer)().ToArray()
End Sub
End Class
呼び出される回数が膨大で配列の生成コストが無視できないようなメソッドや、使用可能なメモリが極端にシビアな実行環境などでは、空の配列を都度作成・取得するよりも、あらかじめキャッシュしておいたインスタンスを使用するようにするか、Type.EmptyTypesのようにあらかじめ用意された空の配列を使用することでパフォーマンスを向上させることができます。
Array.Emptyメソッドが常にキャッシュされたインスタンスを返すかどうか、呼び出しの度に空の配列のインスタンスを作成するのかどうかについてはドキュメントには明記されていません。
一方、Emumerable.Emptyメソッドでは以下のように記載されているため常にキャッシュされた空のシーケンスを返すことが保証されています。
Empty<TResult>() メソッドは、TResult 型の空のシーケンスをキャッシュします。
Enumerable.Empty<TResult> メソッド ()
しかし、Enumerable.Empty<T>().ToArray()
の呼び出しでは、ToArrayメソッドが空のシーケンスから配列の作成を行うため呼び出しの度に空の配列のインスタンスが作成されます。
ヌル参照と空の配列・戻り値や初期値としての空の配列
配列を返すメソッドでは、null
/Nothing
よりも空の配列を返したほうがよい場合があります。 配列を返すメソッドがnullを返す場合、呼び出し側は戻り値がnullかどうかの判定を行った上で列挙などの操作を行う必要がありますが、空の配列を返すようにすれば呼び出し側はnull判定を行わずに戻り値の配列に操作を行うことができます。
using System;
using System.IO;
class Sample {
// ファイルから複数の値を読み込むメソッド
static int[] ReadValuesFromFile(string file)
{
if (File.Exists(file)) {
int[] values;
// ファイルを開いて読み込んだが、読み込み対象の値が無かった場合を想定する
// 値が無かったことを表すため、nullではなく空の配列を返す
values = new int[0];
return values;
}
else {
throw new FileNotFoundException("ファイルがありません", file);
}
}
static void Main()
{
// ファイルから読み込んだ値を列挙する
// (ファイルに目的の値がなかった場合でもメソッドは空の配列を返すため、
// null判定を行うことなく列挙を行うことができる)
foreach (var val in ReadValuesFromFile("data.txt")) {
Console.WriteLine(val);
}
}
}
Imports System
Imports System.IO
Class Sample
' ファイルから複数の値を読み込むメソッド
Shared Function ReadValuesFromFile(ByVal file As String) As Integer()
If File.Exists(file) Then
Dim values() As Integer
' ファイルを開いて読み込んだが、読み込み対象の値が無かった場合を想定する
' 値が無かったことを表すため、Nothingではなく空の配列を返す
values = New Integer() {}
Return values
Else
Throw New FileNotFoundException("ファイルがありません", file)
End If
End Function
Shared Sub Main()
' ファイルから読み込んだ値を列挙する
' (ファイルに目的の値がなかった場合でもメソッドは空の配列を返すため、
' null判定を行うことなく列挙を行うことができる)
For Each val As Integer In ReadValuesFromFile("data.txt")
Console.WriteLine(val)
Next
End Sub
End Class
上記の例では、ファイルが無かった場合にもnullや空の配列を返すようにすることもできますが、推奨できません。 例えば、上記のメソッドにエラー処理を追加してファイルの内容が異常だった場合にもnullを返すような実装にした場合、メソッドの呼び出し側は戻り値がnullとなった原因が何なのかを知ることができません。
このため、なんらかのエラーがあった場合には、nullや空の配列を返すよりも、ファイルが無かった場合はFileNotFoundException、ファイルの内容やフォーマットが異常だった場合はInvalidDataExceptionをスローするというように、状況にあった例外をスローして呼び出し側に原因を通知すべきです。
配列フィールドでも、初期値としてnull
/Nothing
の代わりに空の配列を指定しておくことができます。 メソッドの場合と同様、初期値として空の配列を指定しておくことでnull判定をせずに配列フィールドの参照ができるようになります。
class Account {
public string Name;
public string[] Address = new string[0]; // 初期値として空の配列を代入しておく
}
Class Account
Public Name As String
Public Address() As String = New String() {} ' 初期値として空の配列を代入しておく
End Class
一方この場合、空の配列よりもnull
/Nothing
の方が初期値として妥当な場合もあります。 例えば、このフィールドがデータベースからの参照やユーザー入力によって設定されるものであれば、まだ設定が行われていないことを表す目的でnullを初期値としておいたほうが妥当と考えることもできます。 (もちろん、未入力を表すフラグは別に用意し、初期値は空の配列にしておく、という選択もありえます。)
このように、空の配列を用いることでヌル判定やヌル参照の状況をさけることができる一方で、空の配列とヌル参照では意味が全く異なるため、空の配列とヌル参照を混同したり、単純に置き換えることはできません。 どちらがより妥当か十分検討する必要があります。
部分配列
Pythonなどの言語においては、配列内の区間を指定して部分配列を取り出す次のような構文(arr[from..to]
やarr[from:until]
など)が用意されています。
lst = [0, 1, 2, 3, 4, 5]
segment = lst[2:5] # from 2 until 5
print lst
print segment
[0, 1, 2, 3, 4, 5] [2, 3, 4]
arr = [0, 1, 2, 3, 4, 5]
segment = arr[2..4] # from 2 to 4
p array
p segment
[0, 1, 2, 3, 4, 5] [2, 3, 4]
var arr = [0, 1, 2, 3, 4, 5];
var subarr = arr.slice(2, 5); // from 2 until 5
console.log(arr);
console.log(subarr);
Array [ 0, 1, 2, 3, 4, 5 ] Array [ 2, 3, 4 ]
C# 8.0以降(.NET Core 3.0以降)では、これに相当する範囲構文を使用することができます。 範囲構文は、Rubyと類似した構文[n..m]
を使いますが、表す範囲はPython・JavaScriptと同じく[from..until]
であり、終端側が開区間となっている半開区間の範囲になります。
using System;
class Sample {
static void Main()
{
var arr = new int[] {0, 1, 2, 3, 4, 5};
// arrのインデックス2から5より前まで(2以上5未満)の3要素分の部分配列を切り出す
var segment = arr[2..5];
Console.WriteLine(string.Join(", ", arr));
Console.WriteLine(string.Join(", ", segment));
}
}
0, 1, 2, 3, 4, 5 2, 3, 4
一方それ以前のバージョンのC#やVBではこれに相当する構文は用意されていません。 また、JavaScriptなどの言語におけるslice
のようなメソッドも用意されていません。
このような部分配列を切り出すような構文やメソッドは用意されていませんが、ArraySegment構造体を用いるとこれに似た操作を行うことができます。
using System;
class Sample {
static void Main()
{
var arr = new int[] {0, 1, 2, 3, 4, 5};
// C# 7.x以前では範囲構文は使用できない
//var segment = arr[2..4];
// 配列にはこのようなメソッドは用意されていない
//var segment = arr.Slice(2, 4);
// ArraySegmentを用いて、配列arrのインデックス2から3つ分の要素を参照する部分配列を作成する
var segment = new ArraySegment<int>(arr, 2, 3); // from 2 length 3
Console.WriteLine(string.Join(", ", arr));
Console.WriteLine(string.Join(", ", segment));
}
}
Imports System
Class Sample
Shared Sub Main()
Dim arr() As Integer = New Integer() {0, 1, 2, 3, 4, 5}
' VBでは以下のような構文は用意されていない
'Dim segment() As Integer = arr(2..4)
' 配列にはこのようなメソッドは用意されていない
'Dim segment() As Integer = arr.Slice(2, 4)
' ArraySegmentを用いて、配列arrのインデックス2から3つ分の要素を参照する部分配列を作成する
Dim segment As New ArraySegment(Of Integer)(arr, 2, 3) ' from 2 length 3
Console.WriteLine(String.Join(", ", arr))
Console.WriteLine(String.Join(", ", segment))
End Sub
End Class
0, 1, 2, 3, 4, 5 2, 3, 4
部分配列を取得する方法や、配列の一部分をコピーして部分配列を作成する方法については部分配列を参照してください。
配列のインターフェイス
配列(厳密には配列の基底クラスであるArrayクラス)は、IEnumerable, ICollection, IListなどのインターフェイスを実装しています。 これにより、配列も一種のコレクションとして、他のコレクションと同等に扱うことができるようになっています。
例えば、IEnumerableまたはIEnumerable<T>を引数にとるメソッドでは配列を指定することができます。 String.Concatメソッド(.NET Framework 4以降)では、任意の型T
のIEnumerable<T>を引数にとることができます。 このメソッドに配列を指定することで、配列内の各要素の値を結合した文字列を取得することができます。
using System;
class Sample {
static void Main()
{
int[] arr = {0, 1, 2, 3, 4};
var s = string.Concat(arr);
Console.WriteLine(s);
}
}
Imports System
Class Sample
Shared Sub Main()
Dim arr As Integer() = {0, 1, 2, 3, 4}
Dim s As String = String.Concat(arr)
Console.WriteLine(s)
End Sub
End Class
01234
またWhereなどのLINQの各メソッドもIEnumerable<T>を引数にとりますが、配列はIEnumerable<T>を実装しているため配列に対してもLINQのメソッドを使用することができます。
using System;
using System.Linq;
class Sample {
static void Main()
{
int[] arr = {0, 1, 2, 3, 4};
// 配列の中にある奇数の要素のみを抽出し、そのうち最大のものを取得して表示する
Console.WriteLine(arr.Where(e => e % 2 != 0).Max());
}
}
Imports System
Imports System.Linq
Class Sample
Shared Sub Main()
Dim arr As Integer() = {0, 1, 2, 3, 4}
' 配列の中にある奇数の要素のみを抽出し、そのうち最大のものを取得して表示する
Console.WriteLine(arr.Where(Function(e) e Mod 2 <> 0).Max())
End Sub
End Class
3
配列がICollectionなどのインターフェイスを実装しているといっても、その機能のすべてがサポートされるわけではありません。 たとえば、配列をICollectionインターフェイスにキャストして操作することは可能ですが、ICollection.AddやICollection.Removeなどのメソッドを呼び出して配列のサイズを変更するような操作を行うことはできません。 このような操作を行おうとした場合にはNotSupportedExceptionがスローされます。
using System;
using System.Collections.Generic;
class Sample {
static void Main()
{
int[] arr = {0, 1, 2, 3, 4};
// 配列をICollection<T>インターフェイスにキャスト
ICollection<int> coll = arr;
// ICollectionインターフェイスを介して要素を追加しようとしても、
// 配列は固定長であるためNotSupportedExceptionがスローされる
coll.Add(5);
}
}
Imports System
Imports System.Collections.Generic
Class Sample
Shared Sub Main()
Dim arr As Integer() = {0, 1, 2, 3, 4}
' 配列をICollection(Of T)インターフェイスにキャスト
Dim coll As ICollection(Of Integer) = arr
' ICollectionインターフェイスを介して要素を追加しようとしても、
' 配列は固定長であるためNotSupportedExceptionがスローされる
coll.Add(5)
End Sub
End Class
System.NotSupportedException: コレクションは固定サイズです。 at System.SZArrayHelper.Add[T](T value) at Sample.Main(String[] args)
配列が実装するインターフェイスや他のコレクションとの特徴の違いについてはコレクションの種類と特徴 §.コレクションクラスの特徴を参照してください。
配列の使用と代替手段
ここでは配列が適用できる場合と、それが不適切となる場合、またその場合の代替手段やより適切な実装方法などについて解説します。
object配列
object型の配列では任意の型の値を格納することができますが、object配列が必要になる場面はそう多くなく、また必要になった場合でもそれはデータ構造を定めていないことによる不適切な実装である可能性があります。
using System;
class Sample {
static void Main()
{
var data = new object[4];
data[0] = "Alice";
data[1] = 24;
data[2] = "Bob";
data[3] = 16;
}
}
Imports System
Class Sample
Shared Sub Main()
Dim data(3) As Object
data(0) = "Alice"
data(1) = 24
data(2) = "Bob"
data(3) = 16
End Sub
End Class
こういった場合、格納するデータの構造をクラスや構造体で適切に定め、それに対応した配列型を使用すべきです。 安易にobject型配列を用いる前に、データ構造が適切に定められているか、既存の型を使ってデータを扱えないか検討してください。
using System;
// 配列に格納するデータの構造を表すクラス
class Account {
public string Name;
public int Age;
}
class Sample {
static void Main()
{
var accounts = new Account[2];
accounts[0] = new Account() {Name = "Alice", Age = 24};
accounts[1] = new Account() {Name = "Bob", Age = 16};
}
}
Imports System
Class Account
Public Name As String
Public Age As Integer
End Class
Class Sample
Shared Sub Main()
Dim accounts(1) As Account
accounts(0) = New Account() With {.Name = "Alice", .Age = 24}
accounts(1) = New Account() With {.Name = "Bob", .Age = 16}
End Sub
End Class
可変長の引数
引数で配列を指定することにより、任意個の値をメソッドに渡すことができます。 この場合、あらかじめ引数に渡す値を配列としてまとめておく必要があります。
using System;
class Sample {
// 引数で与えられた配列の合計を求めるメソッド
static long Sum(int[] arr)
{
var sum = 0L;
foreach (var val in arr) {
sum += val;
}
return sum;
}
static void Main()
{
var arr = new int[] {2, 5, 3, 8, 4};
Console.WriteLine("合計 = {0}", Sum(arr));
}
}
Imports System
Class Sample
' 引数で与えられた配列の合計を求めるメソッド
Shared Function Sum(ByVal arr() As Integer) As Long
Dim sum As Long = 0L
For Each val As Integer In arr
sum += val
Next
Return sum
End Function
Shared Sub Main()
Dim arr() As Integer = New Integer() {2, 5, 3, 8, 4}
Console.WriteLine("合計 = {0}", Sum(arr))
End Sub
End Class
配列型の引数とは別に、params
キーワード(C#)・ParamArray
キーワード(VB)を使うことにより、引数を可変長とすることができます。 可変長の引数では、引数に渡す値は必ずしも配列にする必要はありません(配列を直接渡すこともできます)。 メソッド側では、可変長引数は通常の配列と同様に扱うことができます。
using System;
class Sample {
// 引数で与えられた複数の値の合計を求めるメソッド
static long Sum(params int[] arr)
{
var sum = 0L;
foreach (var val in arr) {
sum += val;
}
return sum;
}
static void Main()
{
Console.WriteLine("合計 = {0}", Sum(2, 5, 3));
Console.WriteLine("合計 = {0}", Sum(2, 5, 3, 8, 4));
Console.WriteLine("合計 = {0}", Sum(new int[] {2, 5, 3, 8, 4})); // 配列を渡すこともできる
}
}
Imports System
Class Sample
' 引数で与えられた配列の合計を求めるメソッド
Shared Function Sum(ByVal ParamArray arr() As Integer) As Long
Dim sum As Long = 0L
For Each val As Integer In arr
sum += val
Next
Return sum
End Function
Shared Sub Main()
Console.WriteLine("合計 = {0}", Sum(2, 5, 3))
Console.WriteLine("合計 = {0}", Sum(2, 5, 3, 8, 4))
Console.WriteLine("合計 = {0}", Sum(New Integer() {2, 5, 3, 8, 4})) ' 配列を渡すこともできる
End Sub
End Class
可変長引数は、必ずメソッドの一番最後の引数でなければなりません。 例えば、可変長引数のあとに通常の引数をとるようなメソッドを作成することはできません。
// このように通常の引数のあとに可変長引数をとるようなメソッドは宣言できる
void ParamsMethodOK(int firstValue, params int[] values) {}
// このように可変長引数のあとに通常の引数をとるようなメソッドは宣言できない
void ParamsMethodNG(params int[] values, int lastValue) {}
' このように通常の引数のあとに可変長引数をとるようなメソッドは宣言できる
Sub ParamsMethodOK(ByVal firstValue As Integer, ByVal ParamArray values() As Integer)
End Sub
' このように可変長引数のあとに通常の引数をとるようなメソッドは宣言できない
Sub ParamsMethodNG(ByVal ParamArray values() As Integer, ByVal lastValue As Integer)
End Sub
可変長引数の型をobjectにすることにより、任意の型の値を任意個とることができるようになります。 例えば、Console.WriteLineメソッドやString.Joinメソッドではobject型の可変長引数をとることができるようになっています。
複数の戻り値
メソッドの戻り値を配列にすることでメソッドから複数の値を返すことができます。 しかし、例えば意味の異なる値を配列でひとまとめにして返すようにすると、格納されている値の意味があいまいになり、また呼び出し側では戻り値を配列から展開する必要があります。
using System;
class Sample {
// xとyの商と剰余を求めるメソッド
// 商は戻り値のインデックス0、剰余はインデックス1に格納する
static int[] DivRem(int x, int y)
{
var ret = new int[2];
ret[0] = x / y;
ret[1] = x % y;
return ret;
}
static void Main()
{
// 7÷3の商と剰余を求めたい
var ret = DivRem(7, 3);
// 戻り値から商と剰余に展開しなければならない
// また戻り値の配列に格納されている値の意味があいまいになる
var div = ret[0];
var rem = ret[1];
}
}
Imports System
Class Sample
' xとyの商と剰余を求めるメソッド
' 商は戻り値のインデックス0、剰余はインデックス1に格納する
Shared Function DivRem(ByVal x As Integer, ByVal y As Integer) As Integer()
Dim ret(2) As Integer
ret(0) = x \ y
ret(1) = x Mod y
Return ret
End Function
Shared Sub Main()
' 7÷3の商と剰余を同時に求めたい
Dim ret() As Integer = DivRem(7, 3)
' 戻り値から商と剰余に展開しなければならない
' また戻り値の配列に格納されている値の意味があいまいになる
Dim div As Integer = ret(0)
Dim remn As Integer = ret(1)
End Sub
End Class
このような場合は、配列よりもoutパラメータ(VBではByRef引数)を使ったほうがより適切です。
using System;
class Sample {
// xとyの商と剰余を求めるメソッド
// 商は第3引数のoutパラメータ、剰余は第4引数のoutパラメータに格納する
static void DivRem(int x, int y, out int div, out int rem)
{
div = x / y;
rem = x % y;
}
static void Main()
{
// 7÷3の商と剰余を求めたい
int div, rem;
DivRem(7, 3, out div, out rem);
// メソッドの呼び出し後、変数divとremに結果が格納される
}
}
Imports System
Class Sample
' xとyの商と剰余を求めるメソッド
' 商は第3引数のByRef引数、剰余は第4引数のByRef引数に格納する
Shared Sub DivRem(ByVal x As Integer, ByVal y As Integer, ByRef div As Integer, ByRef remn As Integer)
div = x \ y
remn = x Mod y
End Sub
Shared Sub Main()
' 7÷3の商と剰余を同時に求めたい
Dim div As Integer, remn As Integer
DivRem(7, 3, div, remn)
' メソッドの呼び出し後、変数divとremnに結果が格納される
End Sub
End Class
outパラメータ・ByRef引数はメソッドの戻り値と併用することもできます。 実際、.NETで用意されている商と剰余を求めるMath.DivRemメソッドでは、商は戻り値として、剰余はoutパラメータで返されるようになっています。
Math.DivRemメソッドについては数学関数 §.積・商と剰余 (BigMul, DivRem)を参照してください。
これとは異なる方法で複数の値を返す手段として、Tuple型(タプル)を利用することもできます。 Tuple型は複数の値をひとまとめにするという点は配列と似ていますが、異なる型同士でもまとめることができる点で配列とは異なります。 これにより、配列では値を複数返す場合でも同じ型に限られるのに対し、Tuple型を用いれば型の異なる複数の値を返すことができます。
using System;
class Sample {
// 引数で与えられた配列の合計と平均を求めるメソッド
// 合計はTuple.Item1, 平均はTuple.Item2に格納する
static Tuple<long, double> SumAvr(int[] arr)
{
var sum = 0L;
foreach (var val in arr) {
sum += val;
}
var avr = sum / (double)arr.Length;
// Item1にsum, Item2にavrの値を格納したTupleを作成して返す
return Tuple.Create(sum, avr);
}
static void Main()
{
// この配列内の要素の合計と平均を求めたい
var arr = new int[] {2, 5, 3, 8, 4};
Tuple<long, double> ret = SumAvr(arr);
Console.WriteLine("合計 = {0}", ret.Item1);
Console.WriteLine("平均 = {0}", ret.Item2);
}
}
Imports System
Class Sample
' 引数で与えられた配列の合計と平均を求めるメソッド
' 合計はTuple.Item1, 平均はTuple.Item2に格納する
Shared Function SumAvr(ByVal arr() As Integer) As Tuple(Of Long, Double)
Dim sum As Long = 0L
For Each val As Integer In arr
sum += val
Next
Dim avr As Double = sum / arr.Length
' Item1にsum, Item2にavrの値を格納したTupleを作成して返す
Return Tuple.Create(sum, avr)
End Function
Shared Sub Main()
' この配列内の要素の合計と平均を求めたい
Dim arr() As Integer = New Integer() {2, 5, 3, 8, 4}
Dim ret As Tuple(Of Long, Double) = SumAvr(arr)
Console.WriteLine("合計 = {0}", ret.Item1)
Console.WriteLine("平均 = {0}", ret.Item2)
End Sub
End Class
合計 = 22 平均 = 4.4
このように、配列と違ってTuple型では型の異なる値をひとまとめにできる利点がある一方、Tuple型では格納されている値を参照するのにItem1, Item2といったメンバ名で参照する必要があるため、配列同様に格納されている値の意味があいまいになるという欠点もあります。
比較用の参考として、JavaScriptなどいくつかのスクリプト言語では以下のようなコードを記述することができます。
function SumAvr(arr) {
var sum = 0;
for (var i = 0; i < arr.length; i++) {
sum += arr[i];
}
var avr = sum / arr.length;
return [sum, avr];
}
var sum, avr;
[sum, avr] = SumAvr([2, 5, 3, 8, 4]);
C# 7.0以降、VB15.3以降では、上記のJavaScriptコードのような記述ができるタプルが構文としてサポートされています。 これにより、Tuple型を使わなくても、複数のスカラー値をタプルで返したり、タプルで返される戻り値を変数(スカラー値)に展開することができます。 基本的にはTuple型と同じですが、この構文では格納されている値に対して変数と同様に任意の名前をつけることができるようになっています。
using System;
class Sample {
// 引数で与えられた配列の合計と平均を同時に返すメソッド
static (long, double) SumAvr(int[] arr)
{
var sum = 0L;
foreach (var val in arr) {
sum += val;
}
var avr = sum / (double)arr.Length;
// 複数の値をひとまとめにして返す
return (sum, avr);
}
static void Main()
{
// 呼び出し側で戻り値を展開する
(long sum, double avr) = SumAvr(new int[] {2, 5, 3, 8, 4});
Console.WriteLine("合計 = {0}", sum);
Console.WriteLine("平均 = {0}", avr);
}
}
Imports System
Class Sample
' 引数で与えられた配列の合計と平均を同時に返すメソッド
Shared Function SumAvr(ByVal arr() As Integer) As (Long, Double)
Dim sum As Long = 0L
For Each val As Integer In arr
sum += val
Next
Dim avr As Double = sum / arr.Length
' 複数の値をひとまとめにして返す
Return (sum, avr)
End Function
Shared Sub Main()
' 呼び出し側で戻り値を展開する
(Dim sum As Long, avr As Double) = SumAvr(New Integer() {2, 5, 3, 8, 4})
Console.WriteLine("合計 = {0}", sum)
Console.WriteLine("平均 = {0}", avr)
End Sub
End Class
合計 = 22 平均 = 4.4