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

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

【C#,LINQ】Take,TakeWhile~配列やリストの特定の要素までの要素がほしいとき~

 C#のLINQの関数であるTake(), TakeWhile()の使い方についてです。
 配列やリストなどのシーケンスの指定した要素までの要素群を取得することが出来ます。
f:id:urahimono:20180617130249p:plain


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

指定した数までの要素がほしい

 この配列の特定の部分までの要素が欲しいなぁ。
 ある特定以降の要素は削除して、残りの要素だけ取得する方法はないだろうか。

 そんなときにはLINQTake()

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

 引数に、欲しい要素の数を指定すれば、先頭から指定した数だけの要素のシーケンスを返してくれます。

Program.cs

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

public static class Program
{
    static void Main( string[] args )
    {
        // 0 ~ 9 の数値データ(Range()で作った方が早いけどね!)
        int[]       numbers = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
        // 日曜日から土曜日までの文字列データ
        string[]    texts   = new string[] { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };

        // 最初から3個までの要素を取得するよ。
        IEnumerable<int>    takenNumbers    = numbers.Take( 3 );
        // 最初から4個までの要素を取得するよ。
        IEnumerable<string> takenTexts      = texts.Take( 4 );

        // 結果発表
        System.Console.WriteLine( "takenNumbers:{0}", takenNumbers.Text() );
        System.Console.WriteLine( "takenTexts  :{0}", takenTexts.Text() );
        // 入力待ち用
        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

takenNumbers:[0], [1], [2],
takenTexts :[Sun], [Mon], [Tue], [Wed],

 この指定する引数には、要素の数より大きい数値負数を渡してもきちんと動作します。
 負数の場合は0を指定したこと扱いになるみたいですね。

Program.cs

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

public static class Program
{
    static void Main( string[] args )
    {
        // 0 ~ 9 の数値データ(Range()で作った方が早いけどね!)
        int[]       numbers = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
        // 日曜日から土曜日までの文字列データ
        string[]    texts   = new string[] { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };

        // 明らかに、データ数より多い数を求める暴挙!
        IEnumerable<int>    takenNumbers    = numbers.Take( 100 );
        // 負数を渡すという、サイコパス的な行動!
        IEnumerable<string> takenTexts      = texts.Take( -5 );

        // 結果発表
        System.Console.WriteLine( "takenNumbers:{0}", takenNumbers.Text() );
        System.Console.WriteLine( "takenTexts  :{0}", takenTexts.Text() );
        // 入力待ち用
        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

takenNumbers:[0], [1], [2], [3], [4], [5], [6], [7], [8], [9],
takenTexts :

指定した条件までの要素がほしい

 先ほどのTake()は、先頭から欲しい要素の数を指定してするかたちをとっていました。
 ですが、欲しい要素を指定する場合に、数ではなく、条件を指定した場合もあると思います。

 そんな場合はTakeWhile()
 引数に条件を記述できます。

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

  先頭の要素から、この条件を満たさなくなるまでの要素を取得できます。
  以下の例では、ラムダ式を用いて条件文を記述しています。

Program.cs

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

public static class Program
{
    static void Main( string[] args )
    {
        // 0 ~ 9 の数値データ(Range()で作った方が早いけどね!)
        int[]       numbers = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
        // 日曜日から土曜日までの文字列データ
        string[]    texts   = new string[] { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };

        // 4未満の数値までを取得する
        IEnumerable<int>    takenNumbers    = numbers.TakeWhile( value => value < 4 );
        // 最後の文字が"i"のものまで取得する
        IEnumerable<string> takenTexts      = texts.TakeWhile( value => value.EndsWith( "n" )  );

        // 結果発表
        System.Console.WriteLine( "takenNumbers:{0}", takenNumbers.Text() );
        System.Console.WriteLine( "takenTexts  :{0}", takenTexts.Text() );
        // 入力待ち用
        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

takenNumbers:[0], [1], [2], [3],
takenTexts :[Sun], [Mon],

要素のインデックスを取得できちゃう

 TakeWhile()では条件を指定する際に、各要素の情報だけでなく、各要素のインデックスも取得することができます。

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

 条件を記述する関数の引数にint型の引数を追加すればいいだけです。
 以下の例ではラムダ式の条件文にて、indexの引数でインデックスの番号を渡しています。

Program.cs

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

public static class Program
{
    static void Main( string[] args )
    {
        // 0 ~ 9 の"降順"の数値データ
        int[]       numbers = new int[] { 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
        // 日曜日から土曜日までの文字列データ
        string[]    texts   = new string[] { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };

        // 数値が各要素のインデックスより、小さい値のものまでを取得する
        IEnumerable<int>    takenNumbers    = numbers.TakeWhile( ( value, index ) => value > index );
        // 文字数が各要素のインデックスより、大きいものを取得する
        IEnumerable<string> takenTexts      = texts.TakeWhile( ( value, index ) => value.Length > index );

        // 結果発表
        System.Console.WriteLine( "takenNumbers:{0}", takenNumbers.Text() );
        System.Console.WriteLine( "takenTexts  :{0}", takenTexts.Text() );
        // 入力待ち用
        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

takenNumbers:[9], [8], [7], [6], [5],
takenTexts :[Sun], [Mon], [Tue],

Where()との違い

 さて、似たようなLINQの関数としてWhere()というものがあります。
 
public static IEnumerable<TSource> Where<TSource>( this IEnumerable<TSource> source, Func<TSource, bool> predicate );
Enumerable.Where(TSource) メソッド (IEnumerable(TSource), Func(TSource, Boolean)) (System.Linq)

 Where()は条件と同じ要素を取得する関数です。
 このWhere()TakeWhile()を比べてみましょう。

 以下の例では、指定した数値以下の要素を取得する条件を記述しています。
Program.cs

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

public static class Program
{
    static void Main( string[] args )
    {
        // 0 ~ 9 の"昇順"の数値データ
        int[]   numbers = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

        // 数値が5未満の要素"まで"取得する
        IEnumerable<int>    takenNumbers    = numbers.TakeWhile( value => value < 5 );
        // 数値が5未満の要素"を"取得する
        IEnumerable<int>    whereNumbers    = numbers.Where( value => value < 5 );

        // 結果発表
        System.Console.WriteLine( "takenNumbers:{0}", takenNumbers.Text() );
        System.Console.WriteLine( "whereNumbers:{0}", whereNumbers.Text() );
        // 入力待ち用
        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

takenNumbers:[0], [1], [2], [3], [4],
whereNumbers:[0], [1], [2], [3], [4],

 同じ結果になりました。
 先ほどはデータとなる数値の並びが昇順に並んでいる、整理されたデータを使っていました。
 ではこのデータの並びをばらばらにして、再度検証してみましょう。

Program.cs

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

public static class Program
{
    static void Main( string[] args )
    {
        // 0 ~ 9 の"バラバラ"の数値データ
        int[]   numbers = new int[] { 0, 3, 6, 1, 4, 5, 8, 7, 2, 9 };

        // 数値が5未満の要素"まで"取得する
        IEnumerable<int>    takenNumbers    = numbers.TakeWhile( value => value < 5 );
        // 数値が5未満の要素"を"取得する
        IEnumerable<int>    whereNumbers    = numbers.Where( value => value < 5 );

        // 結果発表
        System.Console.WriteLine( "takenNumbers:{0}", takenNumbers.Text() );
        System.Console.WriteLine( "whereNumbers:{0}", whereNumbers.Text() );
        // 入力待ち用
        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

takenNumbers:[0], [3],
whereNumbers:[0], [3], [1], [4], [2],

 結果が変わりました。

 Where()では、全ての要素に対して条件にあっているかを判定し、条件を満たしている要素を返しています。
 SkipWhile()では、先頭から条件をあっていない要素があった場合は、それ以降の要素は結果に反映されません。

 そのため、Where()の結果では5未満の全ての要素を含んでいます。
 ですが、SkipWhile()の結果では、5未満の全ての要素は取得できていません。
 途中で5以上の要素が見つかった場合は、その時点で結果を返してしまうからです。

 このあたりに気をつけて、Take()TakeWhile()を七兆回ほど使ってみてください。

LINQのリンク