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

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

【C#,LINQ】Single,SingleOrDefault~配列やリストの唯一の要素がほしいとき~

 C#のLINQの関数であるSingle()SingleOrDefault()の使い方についてです。
 配列やリストといったシーケンスの唯一の要素を取得することが出来ます。
f:id:urahimono:20180605232507p:plain


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

唯一の要素を取得してみよう

 さあ、皆さんの中にただひとつ、唯一の要素を取得したい!、という方はいらっしゃいませんか。
 ……えっ、唯一の要素って何のことだって?

 唯一の要素というのは、配列やリストの中に一つしか要素がないときですね。
 そしてその一つしかない要素を取得したい人を募っておるのです。
 ……えっ、お前は何を言っているんだって?

 ふむ、それでは仕方ありません。
 実演しながら説明した方が早いのかもしれません。
 では唯一の要素を取得できるLINQの処理、Single()の力をお見せしましょう。

public static TSource Single<TSource>( this IEnumerable<TSource> source );
Enumerable.Single(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[] { 5 };

        int result  = numbers.Single();


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

データ:[5],
結果:5

 どうです、すばらしいでしょう!
 5 という数値のみが入った配列の中から、見事 5 の値を取得することができましたよ!
 これがSingle()の力なのです。

 ……えっ、これどういう状況で使用するんだ、ですって?
 もちろん、今回のような配列の中に要素が一つだけあって、それを取得するときに使用するに決まっているじゃないですか!
 会場がどよめいているのが分かりますね。
 そうです、これがSingle()の力なのです。

条件に合った唯一の要素を取得してみよう

 先ほどの力だけでもSingle()のすごさが分かっていただけたと思いますが、Single()の力はこんなものではないのです!

 Single()には、引数に配列やリストの中から要素を探す条件となる処理を記述することによって、要素を探して取得することができるのです。

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

 な、なんと恐ろしきかなSingle()の力。
 それでは早速ご覧入れましょう。

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.Single( 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],
結果:11

 す、すばらしい。
 5つの要素の中から、見事10より大きい要素を見つけ出しました。
 Single()マジックです!

 これだけではありません。
 今回は分かりやすいように、int型の値を使用しました。
 ですが、Single()はどんな型でも対応するジェネリック型の処理なのです。
 そのため、floatだろうと、stringだろうとなんでも対応できてしまうのです。
 以下の例では自作のclassの配列に対して、Single()を使ってみた例です。

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が10未満の要素を探すよ!
        Parameter result  = parameters.Single( value => value.ID < 10 );


        // 結果発表
        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:5, Name:正一郎

 ふぁっふぁっふぁっ、すばらしいでしょうSingle()の力は。
 皆さんも使ってみたくなったのではありませんか?

唯一ではなければ例外だよ

 ……えっ、リストの中に要素が複数あったり、条件に対応する要素が複数ある場合はどうなるかって?
 ふぁっふぁっふぁ、Single()の力を舐めていらっしゃるようですね。
 決まっているじゃないですか。
 例外が発生しますよ。

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
        {
            result = numbers.Single();
        }
        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: シーケンスに複数の要素が含まれています

 Single()は唯一の要素を取得するための業ですから、複数の結果があるような状況では使用してはいけません。
 以下の場合も同様です。

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
        {
            // 奇数の要素を探すよ!
            result = numbers.Single( value => value % 2 == 1 );
        }
        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: シーケンスに複数の一致する要素が含まれています

 当然ですが、リストや配列の中身が空でも例外です。

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

 Single()は唯一の要素を取得するために使う奥義なのです!
 そのため、複数だのだのの状況には使うべきではないのです!

 わかりましたね?

 えっ、それって使いにくくない、ですって?
 えっ、例外は勘弁してほしい、ですって?

 ……ふぅ、仕方ありませんね。
 では皆様には特別に、禁技SingleOrDefault()を伝授して差しあげましょう。

デフォルト返すよSingleOrDefault

 Single()には双子の兄弟、SingleOrDefault()があるのです。
 SingleOrDefault()は要素の返りがだった場合、型のデフォルトの値を返すのです。
 お、恐ろしい関数なのです。

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

 では先ほどのコードをSingleOrDefault()に置きかえてみましょう。

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
        {
            // Single()の代わりに、SingleOrDefault()を使ってみよう!
            // result = numbers.Single();
            result = numbers.SingleOrDefault();
        }
        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

 れ、例外が発生しない。

 この場合はどうでしょうか。

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より大きいの要素を探すよ!
            // Single()の代わりに、SingleOrDefault()を使ってみよう!
            // result = numbers.Single( value => value > 20 );
            result = numbers.SingleOrDefault( 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 が返ってきています。
 これがSingleOrDefault()の力なのか。

 ただし、このSingleOrDefault()が禁技と呼ばれているのにはある理由があります。
 確かに、要素が無い場合などに例外を発生させないという利点があるのですが、「デフォルトの値を返す」というのは、必ずしも良い点だけでないのに注意せねばなりません。

 今回のコードの場合でしたら、 0 という値が返ってきた場合、「要素が見つからなかったため、デフォルトの値である 0 が返ってきた」のか、「 0 という要素を見つけたので、それを返した」のかが把握しづらいのです。
 この点には気をつけていただきたいです。

 いかがでしたでしょうか、Single()SingleOrDefault()は。
 ……えっ、SingleOrDefault()は例外が発生しなくて使いやすそうだった、ですって?
 ……えっ、SingleOrDefault()なら複数の要素でも安心して使える、ですって?

 何を言っているんですか。
 SingleOrDefault()でも要素が複数だった場合は、例外が発生しますよ!

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
        {
            // 奇数の要素を探すよ!
            // Single()の代わりに、SingleOrDefault()を使ってみよう!
            // result = numbers.Single( value => value % 2 == 1 );
            result = numbers.SingleOrDefault( value => value % 2 == 1 );
        }
        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: シーケンスに複数の一致する要素が含ま
れています

 繰り返しますが、Single()SingleOrDefault()は唯一の要素を取得するために使う奥義なのです!
 そのため、複数の状況には使うべきではないのです!

 それでは皆さんも、Single()SingleOrDefault()を七兆回は使ってみてくださいねー。

LINQのリンク