うら干物書き

ゲームを作りたい!

【C#,LINQ】First,FirstOrDefault~配列やリストの先頭の要素がほしいとき~

 C#のLINQの関数であるFirst()FirstOrDefault()の使い方についてです。
 配列やリストといったシーケンスの先頭の要素を取得することが出来ます。
 取得する要素に条件を指定すれば、ListクラスのFind()のように使用することもできます。
f:id:urahimono:20180603210528p:plain


この記事には.NET Framework 4.6.1を使用しています。

先頭の要素を取得する

 配列やリストなどのシーケンスにおいて、先頭の要素を取得したいとき、どうしています?
 [0]で取得する?
 うん、それでいいと思う。

 ただそれを言われてしまうと、話がこれ以上進まなくなってしまうので、聞かなかったことにします。
 まあ、シーケンスの中にはインデックスを指定して要素を取得できないものもありますしね。
 パッと例が思いつきませんが。

 LINQの機能であるFirst()を使うことで、IEnumerableを継承したシーケンスならば、簡単に先頭の要素が取得することできるのです。

public static TSource First<TSource>( this IEnumerable<TSource> source );
Enumerable.First(TSource) メソッド (IEnumerable(TSource)) (System.Linq)

 まあ、とりあえず使ってみましょうよ。
 簡単ですし。

Program.cs

using System.Linq;
using System.Collections;
using System.Collections.Generic;

public static class Program
{
    static void Main( string[] args )
    {
        // データ
        int[] numbers = new int[] { 1, 2, 3, 5, 7, 11 };

        int result  = numbers.First();


        // 結果発表
        System.Console.WriteLine( "データ:{0}", numbers.Text() );
        System.Console.WriteLine( "結果:{0}", result );
        // 入力待ち用
        System.Console.ReadKey();
    }

    /// <summary>
    /// 簡易的なシーケンスのテキスト取得処理
    /// </summary>
    public static string Text<TSource>( this IEnumerable<TSource> i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

データ:[1], [2], [3], [5], [7], [11],
結果:1

 ねっ、簡単でしょ。

条件に合った先頭の要素を取得する

 ここからがきっとメインです。
 僕も先ほど説明してこそいましたが、先頭の要素を取得する状況……、あんまり無いなぁと思っていました。
 あったとしてもFirst()を使わずに、[0]を使っていますね。

 ただ、配列の中で指定した条件の要素を検索し取得することは多いんじゃないでしょうか。
 First()では引数に条件を指定することで、最初に条件に合った要素を取得することもできます。

public static TSource First<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate );

 では先ほどの配列から、偶数の値を取得する処理を書いてみましょう。

Program.cs

using System.Linq;
using System.Collections;
using System.Collections.Generic;

public static class Program
{
    static void Main( string[] args )
    {
        // データ
        int[] numbers = new int[] { 1, 2, 3, 5, 7, 11 };

        // 偶数のデータを探すよ!
        int result  = numbers.First( value => value % 2 == 0 );


        // 結果発表
        System.Console.WriteLine( "データ:{0}", numbers.Text() );
        System.Console.WriteLine( "結果:{0}", result );
        // 入力待ち用
        System.Console.ReadKey();
    }

    /// <summary>
    /// 簡易的なシーケンスのテキスト取得処理
    /// </summary>
    public static string Text<TSource>( this IEnumerable<TSource> i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

データ:[1], [2], [3], [5], [7], [11],
結果:2

 条件はラムダ式で書けるので、簡単な条件ならサラッと書けます。

 では、自作したクラスデータの配列から、指定した名前のデータを探して取得する処理を書いてみましょう。

Program.cs

using System.Linq;
using System.Collections;
using System.Collections.Generic;

public static class Program
{
    private class Parameter
    {
        public int      ID      { get; set; }
        public string   Name    { get; set; }

        public override string ToString()
        {
            return string.Format( "ID:{0}, Name:{1}", ID, Name );
        }
    }

    static void Main( string[] args )
    {
        // データ
        Parameter[] parameters = new Parameter[]
        {
            new Parameter() { ID =  5, Name = "正一郎" },
            new Parameter() { ID = 13, Name = "清次郎" },
            new Parameter() { ID = 25, Name = "誠三郎" },
            new Parameter() { ID = 42, Name = "征史郎" },
        };

        // 征史郎のデータを探すよ!
        Parameter result    = parameters.First( value => value.Name == "征史郎" );


        // 結果発表
        System.Console.WriteLine( "データ:{0}", parameters.Text() );
        System.Console.WriteLine( "結果:{0}", result );
        // 入力待ち用
        System.Console.ReadKey();
    }

    /// <summary>
    /// 簡易的なシーケンスのテキスト取得処理
    /// </summary>
    public static string Text<TSource>( this IEnumerable<TSource> i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

データ:[ID:5, Name:正一郎], [ID:13, Name:清次郎], [ID:25, Name:誠三郎], [ID:42, Name:征史郎],
結果:ID:42, Name:征史郎

 First()はこんな風に使えます。

例外が発生するとき

 First()の便利さが分かってもらえたと思います。
 ただ、このような時に使用すると例外が発生してしまいます。

リストが空のとき

 配列やリストの要素がからのときにFirst()を使ってみましょう。

Program.cs

using System.Linq;
using System.Collections;
using System.Collections.Generic;

public static class Program
{
    static void Main( string[] args )
    {
        // データ(あえて空のデータにしてみました)
        int[] numbers = new int[] {};

        int result = 0;
        try
        {
            result = numbers.First();
        }
        catch( System.Exception i_exception )
        {
            System.Console.WriteLine( "例外だよ……:{0}", i_exception );
            // 入力待ち用
            System.Console.ReadKey();
            return;
        }        

        // 結果発表
        System.Console.WriteLine( "データ:{0}", numbers.Text() );
        System.Console.WriteLine( "結果:{0}", result );
        // 入力待ち用
        System.Console.ReadKey();
    }

    /// <summary>
    /// 簡易的なシーケンスのテキスト取得処理
    /// </summary>
    public static string Text<TSource>( this IEnumerable<TSource> i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

例外だよ……:System.InvalidOperationException: シーケンスに要素が含まれていません

 例外が出ましたね。
 ちゃんとtrycatchしないと面倒なことになります。

条件に合う要素が見つからないとき

 指定した条件の要素が一つもないときはどうなってしまうのでしょうか。

Program.cs

using System.Linq;
using System.Collections;
using System.Collections.Generic;

public static class Program
{
    static void Main( string[] args )
    {
        // データ
        int[] numbers = new int[] { 1, 2, 3, 5, 7, 11 };

        int result = 0;
        try
        {
            // 20より大きい数値を探すよ!
            result = numbers.First( value => value > 20 );
        }
        catch( System.Exception i_exception )
        {
            System.Console.WriteLine( "例外だよ……:{0}", i_exception );
            // 入力待ち用
            System.Console.ReadKey();
            return;
        }        

        // 結果発表
        System.Console.WriteLine( "データ:{0}", numbers.Text() );
        System.Console.WriteLine( "結果:{0}", result );
        // 入力待ち用
        System.Console.ReadKey();
    }

    /// <summary>
    /// 簡易的なシーケンスのテキスト取得処理
    /// </summary>
    public static string Text<TSource>( this IEnumerable<TSource> i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

例外だよ……:System.InvalidOperationException: シーケンスに、一致する要素は含まれてません

 これもやっぱり例外が出ますね。

例外を出したくないならFirstOrDefault

 例外が出てしまうというのは、なかなかいただけませんね。
 条件に合う要素が無いときも、それはそれで処理を先に進めませたい。
 でもtrycatchをそこら中に書くのは面倒くさい。

 そんなあなたにFirstOrDefault()
 上記の例外が出るパターンの場合には、型のデフォルトの値が返ってきます。

public static TSource FirstOrDefault<TSource>( this IEnumerable<TSource> source );
public static TSource FirstOrDefault<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate );
Enumerable.FirstOrDefault(TSource) メソッド (IEnumerable(TSource)) (System.Linq)

 さっそく試してみましょう。

Program.cs

using System.Linq;
using System.Collections;
using System.Collections.Generic;

public static class Program
{
    static void Main( string[] args )
    {
        // データ
        int[] numbers = new int[] { 1, 2, 3, 5, 7, 11 };

        int result = 0;
        try
        {
            // 20より大きい数値を探すよ!
            result = numbers.FirstOrDefault( value => value > 20 );
        }
        catch( System.Exception i_exception )
        {
            System.Console.WriteLine( "例外だよ……:{0}", i_exception );
            // 入力待ち用
            System.Console.ReadKey();
            return;
        }        

        // 結果発表
        System.Console.WriteLine( "データ:{0}", numbers.Text() );
        System.Console.WriteLine( "結果:{0}", result );
        // 入力待ち用
        System.Console.ReadKey();
    }

    /// <summary>
    /// 簡易的なシーケンスのテキスト取得処理
    /// </summary>
    public static string Text<TSource>( this IEnumerable<TSource> i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

データ:[1], [2], [3], [5], [7], [11],
結果:0

 int型のデフォルトの値は0ですから、0が返ってきていますね。

 ではリストが空の場合はどうでしょうか。

Program.cs

using System.Linq;
using System.Collections;
using System.Collections.Generic;

public static class Program
{
    static void Main( string[] args )
    {
        // データ(あえて空のデータにしてみました)
        int[] numbers = new int[] { };

        int result = 0;
        try
        {
            // 20より大きい数値を探すよ!
            result = numbers.FirstOrDefault();
        }
        catch( System.Exception i_exception )
        {
            System.Console.WriteLine( "例外だよ……:{0}", i_exception );
            // 入力待ち用
            System.Console.ReadKey();
            return;
        }        

        // 結果発表
        System.Console.WriteLine( "データ:{0}", numbers.Text() );
        System.Console.WriteLine( "結果:{0}", result );
        // 入力待ち用
        System.Console.ReadKey();
    }

    /// <summary>
    /// 簡易的なシーケンスのテキスト取得処理
    /// </summary>
    public static string Text<TSource>( this IEnumerable<TSource> i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

データ:
結果:0

 この場合も例外を出さずに0が返ってきています。

 では自作したデータクラスの配列の場合はどうでしょうか。

Program.cs

using System.Linq;
using System.Collections;
using System.Collections.Generic;

public static class Program
{
    private class Parameter
    {
        public int      ID      { get; set; }
        public string   Name    { get; set; }

        public override string ToString()
        {
            return string.Format( "ID:{0}, Name:{1}", ID, Name );
        }
    }

    static void Main( string[] args )
    {
        // データ
        Parameter[] parameters = new Parameter[]
        {
            new Parameter() { ID =  5, Name = "正一郎" },
            new Parameter() { ID = 13, Name = "清次郎" },
            new Parameter() { ID = 25, Name = "誠三郎" },
            new Parameter() { ID = 42, Name = "征史郎" },
        };

        Parameter result = new Parameter();
        try
        {
            // IDが30のデータを探すよ!
            result = parameters.FirstOrDefault( value => value.ID == 30 );
        }
        catch( System.Exception i_exception )
        {
            System.Console.WriteLine( "例外だよ……:{0}", i_exception );
            // 入力待ち用
            System.Console.ReadKey();
            return;
        }        

        // 結果発表
        System.Console.WriteLine( "データ:{0}", parameters.Text() );
        System.Console.WriteLine( "結果:{0}", result );
        // 入力待ち用
        System.Console.ReadKey();
    }

    /// <summary>
    /// 簡易的なシーケンスのテキスト取得処理
    /// </summary>
    public static string Text<TSource>( this IEnumerable<TSource> i_source )
    {
        string text = string.Empty;
        foreach( var value in i_source )
        {
            text += string.Format( "[{0}], ", value );
        }
        return text;
    }

} // class Program

データ:[ID:5, Name:正一郎], [ID:13, Name:清次郎], [ID:25, Name:誠三郎], [ID:42, Name:征史郎],
結果:

 classのデフォルト値はnullのため、結果は空になっています。

 FirstOrDefault()は例外が発生しないため、重宝しそうですね。
 ただ注意したい点があります。
 classなどの参照型の場合はnullが返ってくるため、要素が見つからなかったんだな、ということが分かりやすいと思います。
 ただ値型の場合は、デフォルトの値が何かしら意味を持った値になってしまうため、見つからなかったかの判断が難しいです。
 先ほどの例では、偶数の要素を探して0が返ってきた場合は、「0の要素を見つけた」のか「見つからなかったので、デフォルトの値である0が返ってきた」のかがわからないです。
 その点に気を付けて、七兆回使ってみてください。