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

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

【C#,LINQ】Last,LastOrDefault~配列やリストの最後の要素がほしいとき~

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


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

最後の要素を取得する

 配列やリストなどシーケンスにおいて、最後の要素を取得したいとき、どうしています?
 ん、そんなときなどない?
 そ、そっかー……。

 まぁ、でもそのうちあるかもしれませんよ。
 LINQの機能であるLast()を使うことで、IEnumerableを継承したシーケンスならば、簡単に最後の要素が取得することできるのです。

public static TSource Last<TSource>( this IEnumerable<TSource> source );
Enumerable.Last(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.Last();


        // 結果発表
        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],
結果:11

 簡単に出来て便利ですね。

条件に合った最後の要素を取得する

 先ほどは、配列の最後の要素を取得しましたが、Last()は取得条件を指定することもできます。
public static TSource Last<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 };

        // 10未満の要素を探すよ!
        int result  = numbers.Last( value => value < 10 );


        // 結果発表
        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],
結果:7

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

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

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 = "征史郎" },
        };

        // IDが20未満の要素を取得するよ!
        Parameter result    = parameters.Last( value => value.ID < 20 );


        // 結果発表
        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:13, Name:清次郎

例外が発生するとき

 Last()はこのような時に使用すると例外が発生してしまいます。

リストが空のとき

 配列やリストの要素が空のときにLast()を使うと例外が発生します。

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.Last();
        }
        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: シーケンスに要素が含まれていません

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

 指定した条件の要素が一つもないときも例外が発生します。

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.Last( 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: シーケンスに、一致する要素は含まれてません

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

 trycatchを書けば、Last()を使って例外が発生した場合でも対応できます。
 でも、そこら中にtrycatchを書くのは面倒くさいなぁ。

 そんな状況にはLastOrDefault()というものがあります。
 上記の例外が出るパターンの場合には、型のデフォルトの値が返ってきます。

public static TSource LastOrDefault<TSource>( this IEnumerable<TSource> source );
public static TSource LastOrDefault<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate );
Enumerable.LastOrDefault(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.LastOrDefault( 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.LastOrDefault();
        }
        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が50より大きい要素を取得するよ!
            result = parameters.LastOrDefault( value => value.ID > 50 );
        }
        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のため、結果は空になっています。

 LastOrDefault()の方が便利そうですが、注意したい点があります。
 先ほどの例では、要素を探して0が返ってきた場合、「0の要素を見つけた」のか「見つからなかったので、デフォルトの値である0が返ってきた」のかがわからない点です。
 その点には気を付けて、七兆回使ってみてください。

LINQのリンク

  • LINQ一覧
    www.urablog.xyz

  • First, FirstOrDefault
     先頭の要素を取得したい!
    www.urablog.xyz

  • Single, SingleOrDefault
     一つだけの要素を取得したい!
    www.urablog.xyz

  • ElementAt, ElementAtOrDefault
     指定したインデックスの要素を取得したい!
    www.urablog.xyz