コレクション【C#】

C#文法:コレクション:B09_Find

前回は、条件に合う要素の「位置」を取得するArray.FindIndexを取り上げました。今回はその仲間であるArray.Findを見ていきます。使い方はFindIndexとほとんど同じですが、返ってくるものがインデックスではなく、値そのもの(要素そのもの)である点が異なります。

あわせて、条件に合う要素が見つからなかったときに何が返るのか、そして数値型の配列で使う場合の注意点も確認します。

ボタンの作成とデータの準備

これまでと同じように、フォームに新しいボタンを1つ配置し、Textを「Find」にします。配置したボタンをダブルクリックして、クリックイベントのハンドラー(button8_Click)を生成します。

図1 フォームに「Find」ボタンを追加する

図1 フォームに「Find」ボタンを追加する

検索対象のデータは、前回のFindIndexのレクチャーで作成した文字列の配列をそのまま使います。button7_Clickの中にあるdataの定義をコピーして、今回のbutton8_Clickに貼り付けます。

図2 前回(button7_Click)の文字列データをコピーする

図2 前回(button7_Click)の文字列データをコピーする

リスト1 検索対象のデータを用意する

private void button8_Click(object sender, EventArgs e)
{
    //ラムダ式(条件)で検索
    var data = new[]
    { "AAA", "BBBB", "CCC", "D", "EEEE", "ABCDE" };

    //Findで値そのもの(要素)が1件とれる
}

図3 貼り付けたデータと、今回のテーマを示すコメント

図3 貼り付けたデータと、今回のテーマを示すコメント

Array.Findで要素そのものを取得する

Array.Findの書き方はFindIndexと同じです。第1引数に検索対象の配列、第2引数に条件を指定します。Array.Find(まで入力すると、インテリセンスにArray.Find<T>(T[] array, Predicate<T> match)というシグネチャが表示されます。

図4 Array.Find<T>(T[] array, Predicate<T> match) のシグネチャ

図4 Array.Find<T>(T[] array, Predicate<T> match) のシグネチャ

第2引数の型はPredicate<T>です。これは「T型の値を1つ受け取ってboolを返す」処理を表すデリゲートなので、前回と同様にラムダ式で条件を書けます。ここでは「文字数が4文字のもの」という条件を指定します。

リスト2 4文字の要素をFindで検索する

//Findで値そのもの(要素)が1件とれる
var result1 = Array.Find(data, x => x.Length == 4);

ブレークポイントを置いて実行し、result1にマウスカーソルを合わせて中身を確認します。

図5 result1 には "BBBB" が入っている

図5 result1 には “BBBB” が入っている

結果は"BBBB"です。配列の中には4文字の要素として"BBBB"と"EEEE"の2つがありますが、Findが返すのは最初に見つかった1件だけです。前回のFindIndexでは同じ条件でインデックスの1が返ってきましたが、Findではその位置にある要素そのものが返ってくる、という関係になっています。

リスト3 FindIndexとFindの戻り値の違い

var data = new[]
{ "AAA", "BBBB", "CCC", "D", "EEEE", "ABCDE" };

var index = Array.FindIndex(data, x => x.Length == 4); //1(位置)
var value = Array.Find(data, x => x.Length == 4);      //"BBBB"(要素そのもの)

見つからない場合はどうなるか

次に、条件に合う要素が1つもない場合を試します。99文字の文字列は配列の中に存在しないので、次のコードはヒットしないはずです。

リスト4 ヒットしない条件で検索する

var result1 = Array.Find(data, x => x.Length == 4);
var result1b = Array.Find(data, x => x.Length == 99);

図6 ヒットしない場合、result1b は null になる

図6 ヒットしない場合、result1b は null になる

result1bはnullになりました。ただし、「見つからなければ必ずnullになる」というわけではありません。Findは、見つからなかったときにその型の既定値、つまりdefault(T)を返します。既定値は型ごとに決まっています。

図7 型ごとの既定値(数値=0、bool=false、class=null)

図7 型ごとの既定値(数値=0、bool=false、class=null)

型の種類 例 見つからないときの戻り値
数値型 int、double など 0
bool bool false
参照型(クラス) string、自作クラス など null

stringは参照型なので、今回のresult1bはnullになっていた、ということです。

int配列で試す

それでは、数値型の場合を実際に確認してみます。intの配列を用意し、「2より大きいもの」という条件でFindします。

リスト5 int配列をFindで検索する

var ints = new[] { 1, 2, 3, 80 };
var result2 = Array.Find(ints, x => x > 2);

図8 result2 には 3 が入っている

図8 result2 には 3 が入っている

2より大きい要素は3と80の2つですが、最初に見つかるのは3なので、result2は3になります。ここまでは文字列のときと同じ動きです。

続いて、ヒットしない条件として「99より大きいもの」を指定します。

リスト6 int配列でヒットしない条件を指定する

var result2b = Array.Find(ints, x => x > 99);

図9 ヒットしない場合、result2b は 0 になる

図9 ヒットしない場合、result2b は 0 になる

result2bは0になりました。intの既定値が0だからです。このように、数値型では見つからなかったときに0系の値が返ってきます。

数値型では「0」に注意する

ここが今回いちばん注意したいところです。数値の配列には、データとしての0が入っているかもしれません。たとえば配列を次のように変更したとします。

リスト7 データとして0を含む配列

var ints = new[] { 1, 2, 3, 0, 80 };
var result2 = Array.Find(ints, x => x > 2);
//数値型などで存在しない場合ゼロになるので注意
var result2b = Array.Find(ints, x => x > 99);

図10 配列に 0 を追加し、注意点をコメントに残す

図10 配列に 0 を追加し、注意点をコメントに残す

このときFindの結果が0だったとしても、それが「配列の中にあったデータの0」なのか、「見つからなかったために返ってきた既定値の0」なのか、戻り値だけでは区別がつきません。次のコードを見ると、その紛らわしさがよくわかります。

リスト8 戻り値の0だけでは区別できない例

var ints = new[] { 1, 2, 3, 0, 80 };

var a = Array.Find(ints, x => x < 1);  //0(データとして存在する0)
var b = Array.Find(ints, x => x > 99); //0(見つからなかったときの既定値)

対策:Index系のメソッドやExistsでチェックする

対策の1つ目は、前回扱ったIndex系のメソッドで先に確認することです。FindIndexであれば、見つからない場合は-1が返るので、存在しないことをはっきり判定できます。

リスト9 FindIndexなら見つからない場合に-1が返る

var result2b = Array.Find(ints, x => x > 99);      //0
var result2c = Array.FindIndex(ints, x => x > 99); //-1

図11 同じ条件でも FindIndex は -1 を返す

図11 同じ条件でも FindIndex は -1 を返す

IndexOfやFindIndexのように、存在しなければ-1が返るメソッドで判定し、見つかった場合はそのインデックスで要素を取り出す、という書き方ができます。

リスト10 FindIndexで存在を確認してから要素を取り出す

var index = Array.FindIndex(ints, x => x > 99);
if (index >= 0)
{
    //見つかった場合だけ要素を取り出す
    var value = ints[index];
}
else
{
    //見つからなかった場合の処理
}

対策の2つ目は、次回取り上げるArray.Existsを併用することです。Existsは条件に合う要素が存在するかどうかをtrue/falseで返します。先にExistsで確認し、存在していたらFindする、という形にすれば、データの0と見つからなかったときの0を区別できます。ExistsがtrueなのにFindの結果が0だった場合は、それは本当にデータとしての0だと判断できるわけです。

リスト11 Existsで存在を確認してからFindする(Existsの詳細は次回)

if (Array.Exists(ints, x => x > 99))
{
    //存在する場合だけFindする
    //ここで0が返ってきたら、それはデータとしての0
    var value = Array.Find(ints, x => x > 99);
}
else
{
    //存在しない(Findを呼ぶと既定値の0が返る状態)
}

図12 対策をコメントとして残す

図12 対策をコメントとして残す

今回のコード全体

リスト12 button8_Click(完成形)

private void button8_Click(object sender, EventArgs e)
{
    //ラムダ式(条件)で検索
    var data = new[]
    { "AAA", "BBBB", "CCC", "D", "EEEE", "ABCDE" };

    //Findで値そのもの(要素)が1件とれる
    var result1 = Array.Find(data, x => x.Length == 4);   //"BBBB"
    var result1b = Array.Find(data, x => x.Length == 99); //null

    var ints = new[] { 1, 2, 3, 0, 80 };
    var result2 = Array.Find(ints, x => x > 2);           //3
    //数値型などで存在しない場合ゼロになるので注意
    var result2b = Array.Find(ints, x => x > 99);         //0
    var result2c = Array.FindIndex(ints, x => x > 99);    //-1
    //index系でチェックするか、Existsなどを使う
}

まとめ

Array.Findは、条件に合う最初の要素そのものを返します。書き方はFindIndexと同じで、第2引数にラムダ式で条件を指定します。

条件に合う要素が見つからない場合は、その型の既定値(default(T))が返ります。stringなどの参照型ならnull、数値型なら0、boolならfalseです。

とくに数値型では、データとしての0と、見つからなかったときの0が区別できなくなります。IndexOfやFindIndexのように-1で判定できるメソッドを使うか、Existsで存在を確認してからFindするようにしてください。次回は、そのExistsを取り上げます。

C#文法:コレクション

■非公開コース「C#14新機能」プレゼント:
非公開コース「C#14新機能」(80分)をご覧になりたい方は
こちらからURLとパスワードを発行していますので、ご覧になってみてください。
非公開コース「C#14新機能」を観る

A01_はじめに
A02_プロジェクトの作成
B01_配列とは
B02_配列の生成とアクセス
B03_生成と同時に値を設定する
B04_型推論による生成
B05_メソッドの引数などにする場合の注意点
B06_Length
B07_IndexOfでの検索
B08_FindIndexでの検索
B09_Find
B10_Exists
B11_FindAllとFindLast
B12_誤ったコピー
B13_Array.Copy
B14_範囲指定のコピー
B15_Resize
C01_ArrayList
C02_List
C03_List 動的な要素の変更
C04_Listのコンストラクタ
C05_Listのコンストラクタ_Capacity
C06_ListTからArrayクラスのメソッドが呼ばれている
D01_コレクションインタフェースとは
D02_異なるコレクションクラスに互換性を持たせる
D03_インタフェースの階層構造
D04_クライアントができる操作を制限させる
D05_クライアントができる操作を制限させる_後半
D06_Enumerableの拡張メソッドに関して
D07_ReadOnly系のコレクションインタフェース
D08_AsReadOnly
D09_ToListでコピーする
D10_ListTはprivateで使う

■非公開コース「C#14新機能」プレゼント:
非公開コース「C#14新機能」(80分)をご覧になりたい方は
こちらからURLとパスワードを発行していますので、ご覧になってみてください。
非公開コース「C#14新機能」を観る