徳島ゲーム開発ごっこ 技術ブログ

ゲームを作るために役に立ったり立たなかったりする技術を学んでいきます!

【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のリンク