うら干物書き

ゲームを作りたい!

【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が返ってきた」のかがわからない点です。
 その点には気を付けて、七兆回使ってみてください。