うら干物書き

ゲームを作りたい!

【C#,LINQ】ElementAt,ElementAtOrDefault~配列やリストの指定したインデックスの要素がほしいとき~

 C#のLINQの関数であるElementAt()ElementAtOrDefault()の使い方についてです。
 配列やリストといったシーケンスの指定したインデックスの要素を取得することが出来ます。
f:id:urahimono:20180605232809p:plain


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

指定したインデックスの要素を取得してみよう

 突然ですが、ここでくえすちょんだ。
 Applesという配列の変数があるとしよう。
 この配列の4番目の要素を取得するにはどうすればいいだろうか?

 では、そこのお嬢さん、お答えをどうぞ!
 ……正解です!
 Apples[3]と記述すればいいんですね。
 コード的には1番目が 0 から始まるため、4番目の場合は 3 と記述するのが正しいですね。

 配列やListクラスの場合は、[]によるアクセサが用意されているので、指定したインデックスの要素を取得するのはとっても簡単です。
 では用意されていないシーケンス型のクラスの場合はどうすればいいでしょうか?

 では、そこのジェントルマン、お答えをどうぞ!
 ……その通り!
 LINQElementAt()を使えばいいのです。
 IEnumerable<T>を継承しているクラスなら、この力を使うことが出来ます。

public static TSource ElementAt<TSource>( this IEnumerable<TSource> source, int index );
Enumerable.ElementAt(TSource) メソッド (IEnumerable(TSource), Int32) (System.Linq)

 では実演してみましょう。

Program.cs

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

public static class Program
{
    // 指定するインデックスの番号
    private static readonly int SELECTED_INDEX = 4;

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

        int result  = numbers.ElementAt( SELECTED_INDEX );


        // 結果発表
        System.Console.WriteLine( "データ:{0}", numbers.Text() );
        System.Console.WriteLine( "指定したインデックス:{0}", SELECTED_INDEX );
        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],
指定したインデックス:4
結果:7

 []を使った場合と同じ結果になります。
 今回の場合は配列に対して使っているので、[]アクセサを使った方が早いと思うので、ElementAt()を使う旨みはほとんど無いんですが、自作でシーケンスクラスを作った場合などには重宝しそうです。

不正なインデックスのとき、デフォルト値を返すElementAtOrDefault

 さて、先ほどのElementAt()をこのように使ったらどうなるでしょうか。

Program.cs

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

public static class Program
{
    // 指定するインデックスの番号
    // (要素数を超える数値を指定しちゃう)
    private static readonly int SELECTED_INDEX = 10;

    static void Main( string[] args )
    {
        // データ
        int[] numbers = new int[] { 1, 2, 3, 5, 7, 11 };
        
        int result = 0;
        try
        {
            result  = numbers.ElementAt( SELECTED_INDEX );
        }
        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}", SELECTED_INDEX );
        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.ArgumentOutOfRangeException: インデックスが範囲を超えています。
負でない値で、シーケンスのサイズよりも小さくなければなりません。

 []アクセサを使う際に一度は経験があるであろう、アウトオブレンジの例外の発生。
 例外が発生するC#ならまだしも、C++などでこれをやると、大惨事になります。
 ElementAt()でも同様の例外が発生します。

 ですが、ElementAt()のお友達にElementAtOrDefault()というものがいます。

public static TSource ElementAtOrDefault<TSource>( this IEnumerable<TSource> source, int index );
Enumerable.ElementAtOrDefault(TSource) メソッド (IEnumerable(TSource), Int32) (System.Linq)

 先ほどのコードのElementAt()の部分をElementAtOrDefault()に差し替えてみましょう。

Program.cs

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

public static class Program
{
    // 指定するインデックスの番号
    // (要素数を超える数値を指定しちゃう)
    private static readonly int SELECTED_INDEX = 10;

    static void Main( string[] args )
    {
        // データ
        int[] numbers = new int[] { 1, 2, 3, 5, 7, 11 };
        
        int result = 0;
        try
        {
            result  = numbers.ElementAtOrDefault( SELECTED_INDEX );
        }
        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}", SELECTED_INDEX );
        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],
指定したインデックス:10
結果:0

 なんと例外が発生しない!

 ElementAtOrDefault()ではこのような不正なインデックスが渡された場合は、型のデフォルトの値が返ってきます。

 そのため、このような書き方でも例外は発生しません。

Program.cs

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

public static class Program
{
    // 指定するインデックスの番号
    // (負数を指定しちゃう!)
    private static readonly int SELECTED_INDEX = -1;

    static void Main( string[] args )
    {
        // データ
        int[] numbers = new int[] { 1, 2, 3, 5, 7, 11 };
        
        int result = 0;
        try
        {
            result  = numbers.ElementAtOrDefault( SELECTED_INDEX );
        }
        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}", SELECTED_INDEX );
        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
結果:0

 例外が発生しないことは大変助かるのですが、必ずしも良いかと言われればそうではありません。
 今回の場合でしたら、int型のデフォルトの値である 0 が返ってきています。
 これは「指定したインデックスの要素に0が入っていた」のか「要素が見つからなかったため、デフォルトの値である 0 が返ってきた」のかがわからないという欠点があるのです。

 その点に気をつけて、ElementAt()ElementAtOrDefault()を七兆回使ってみてください。

LINQのリンク