はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【Unity】LINQについてかるくまとめてみた

この記事でのバージョン
Unity2018.3.9f1

はじめに

今回はLINQのかるい説明から実際に様々なメソッドを使ってみるという記事になります!

LINQと聞いて「ゼルダの伝説」を思い出す人がいるのではないでしょうか?

私は最初それしか思いつかなかったのですが、どうやら調べてみるとゼルダの伝説に登場するリンクは英語で「Link」と書くようです。

少し話が脱線してしまいましたがこのLINQはC#をある程度触ったことがある人なら聞いたことがあるかもしれません。

C#なので当然Unityでも使えるのですが、昔はスマホ(IPhone)で動かない可能性がありました。

しかし、今はどうやら大丈夫になったみたいです。
Unity+iOSでエラーになるLINQのまとめ - Qiita

心配なく使うことができるようになった訳なので、一度チャレンジしてみてはどうでしょうか。

ただIL2CPPを忘れずにしましょう!
【Unity】IL2CPPで高速化?してみた - はなちるのマイノート

LINQを使うための準備

LINQを使うにはusing ディレクティブを用いて名前空間System.Linqを指定しましょう。

using System.Linq;

なぜLINQを使うべきなのか

LINQを用いるにあたってインターフェイスの知識が多少必要です。

hanaachiru.hatenablog.com

重要な性質はリストや配列といったIEnumerable<T>インターフェイスを実装している型ならば、どんなものであっても実行することができるということです。

実際にどれくらいのクラスがIEnumerable<T>インターフェイスを実装しているか気になると思いますが、これだけあります。

f:id:hanaaaaaachiru:20190322220935g:plain

https://docs.microsoft.com/ja-jp/dotnet/api/system.collections.generic.ienumerable-1?view=netframework-4.7.2


加えてLINQは「foreachのパワーアップ版」と言われることがありますが、「foreachはIEnumerable<T>インターフェイスと深い関係」にあります。
その一つとしてforeachはIEnumerableインターフェースを実装しているクラスしか処理することができません(厳密には異なります。以下の記事を参照)。

これは結構衝撃を覚えるかたもいるかもしれませんが、IEnumerableとIEnumerator,イテレーターはLINQの正しい理解をするのに大切なようです。

http://garicchi.hatenablog.jp/entry/2014/09/12/200000garicchi.hatenablog.jp

foreachについて本気出して調べてみた - Qiita

さらにもう一つ重要な性質があり、メソッドの返り値がIEnumerable<T>であるということです。
これは実際に見てもらうと分かりやすいと思います。

using System.Linq;

public class Hoge : MonoBehaviour
{

    void Start()
    {
        var list = new List<int> { 1, 2, 3, 4, 5 };

        IEnumerable<string> query = list.Where(s => s <= 3)
            .Select(s => s.ToString());

        foreach (string num in query) Debug.Log(num);     //1 2 3

    }

}

これはメソッドチェーンと呼ばれるもので、メソッドをチェーンのようにつなげていくことができます

またLINQを用いることでコードの量が減るだけでなく、何をやるのかが読み取りやすくなります

以上の理由より、LINQを使える場面では積極的に用いるべきでしょう。

説明の前に

LINQにはコレクションやシーケンスといった用語が頻出です。
自信がない方は今一度基本用語をチェックしてみてください!
hanaachiru.hatenablog.com

LINQで使えるクエリ演算子

上のコードでも用いた.Whereや.SelectのようなLINQメソッドはたくさんあります。
これは厳密にはIEnumerable<T>インターフェイスの拡張メソッドです。

どれくらいあるかこれも気になると思うので、GIFを載せておきます。
f:id:hanaaaaaachiru:20190322225831g:plain

https://docs.microsoft.com/ja-jp/dotnet/api/system.collections.generic.ienumerable-1?view=netframework-4.7.2

Whereメソッド

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

public class Hoge: MonoBehaviour
{
    private void Start()
    {
        IEnumerable<int> numbers = Enumerable.Range(1, 5)
            .Where(s => s <= 3);

        foreach(int num in numbers)
        {
            Debug.Log(num);     //1 2 3
        }
    }
}

Whereメソッド引数で指定した条件に一致する要素を検索し、見つかった要素を全て返します

Selectメソッド

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

public class Hoge: MonoBehaviour
{
    private void Start()
    {
        IEnumerable<int> numbers = Enumerable.Range(1, 5)
            .Select(n => n * n);

        foreach(int num in numbers)
        {
            Debug.Log(num);     //1 4 9 16 25
        }
    }
}

Selectメソッドコレクションのすべての要素に対して何らかの操作をし、新しいコレクションを作ります

Distinctメソッド

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

public class Hoge: MonoBehaviour
{
    private void Start()
    {
        var numbers = new List<int> { 1, 2, 3, 4, 5, 4, 3, 2, 1 };

        IEnumerable<int> results = numbers.Distinct();

        foreach (int num in results) Debug.Log(num);    //1 2 3 4 5
    }
}

Distinctメソッドコレクションから重複を排除することができます

OrderByメソッド

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

public class Enemy
{
    public string Name { get; set; }
    public int Hp { get; set; }
    public int Exp { get; set; }
}

public class Hoge: MonoBehaviour
{
    private void Start()
    {
        var numbers = new List<int> { 1, 2, 3, 4, 5, 4, 3, 2, 1 };

        IEnumerable<int> sortedNumbers = numbers.OrderBy(x => x);

        foreach (int num in sortedNumbers) Debug.Log(num);    //1 1 2 3 3 4 4 5

        var enemies = new List<Enemy>
        {
            new Enemy{ Name = "ももんじゃ" , Hp = 20,Exp = 10 },
            new Enemy{ Name = "ドラキー" , Hp = 20,Exp = 20 },
            new Enemy{ Name = "スライム" , Hp = 10,Exp = 3 },
            new Enemy{ Name = "メタルスライム" , Hp = 5,Exp = 100 },
        };

        IEnumerable<Enemy> sortedEnemy = enemies.OrderBy(x => x.Exp);

        foreach (Enemy enemy in sortedEnemy) Debug.Log(enemy.Name);     //スライム ももんじゃ ドラキー メタルスライム

    }
}

OrderByメソッドコレクションを昇順にソートします
また同様に降順にするOrderByDescendingメソッドも存在します。

Concatメソッド

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

public class Hoge: MonoBehaviour
{
    private void Start()
    {
        var numbers1 = new List<int> { 1, 2, 3, 4, 5 };

        var numbers2 = new List<int> { 4, 3, 2, 1 };

        IEnumerable<int> concatedNumber = numbers1.Concat(numbers2);

        foreach (int num in concatedNumber) Debug.Log(num);    //1 2 3 4 5 4 3 2 1
    }
}

Concatメソッド2つのコレクションを連結することができます

Enumerable.Repeatメソッド

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

public class Hoge: MonoBehaviour
{
    private void Start()
    {
        IEnumerable<int> numbers = Enumerable.Repeat(-1, 5);

        foreach(int num in numbers)
        {
            Debug.Log(num);    //-1, -1, -1, -1, -1
        }
    }
}

Enumerable.Repeatメソッド同じ値を繰り返すシーケンス(IEnumerableインターフェイス を実装するオブジェクト)を作ります

Enumerable.Rangeメソッド

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

public class Hoge: MonoBehaviour
{
    private void Start()
    {
        IEnumerable<int> numbers = Enumerable.Range(1, 5);

        foreach(int num in numbers)
        {
            Debug.Log(num);     //1 2 3 4 5
        }
    }
}

Enumerable.Rangeメソッド連続した数字を生成します。

ToListメソッド・ToArrayメソッド

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

public class Hoge: MonoBehaviour
{
    private void Start()
    {
        List<int> numbers = Enumerable.Repeat(-1, 20)
            .ToList();

        int[] numbers2 = Enumerable.Repeat(-1, 20)
            .ToArray();
    }
}

ToListメソッドシーケンスをリストに変換します。
同様にToArrayメソッドシーケンスを配列に変換します。

FirstOrDefaultメソッド

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

public class Enemy
{
    public string Name { get; set; }
    public int Hp { get; set; }
    public int Exp { get; set; }
}

public class Hoge: MonoBehaviour
{
    private void Start()
    {
        IEnumerable<int> numbers = Enumerable.Range(1, 5);

        int num = numbers.FirstOrDefault(n => n >= 3);

        Debug.Log(num);   //3

        var enemies = new List<Enemy>
        {
            new Enemy{ Name = "スライム" , Hp = 10,Exp = 3 },
            new Enemy{ Name = "ももんじゃ" , Hp = 20,Exp = 10 },
            new Enemy{ Name = "ドラキー" , Hp = 20,Exp = 20 },
            new Enemy{ Name = "メタルスライム" , Hp = 5,Exp = 100 },
        };

        Enemy enemy = enemies.FirstOrDefault(n => n.Exp >= 10);

        Debug.Log(enemy.Name);   //ももんじゃ

    }
}

FirstOrDefaultメソッドコレクションの中で条件に一致する最初の要素を取得します
また同様に最後の要素を取得するLastOrDefaultメソッドも存在します。

Takeメソッド

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

public class Hoge: MonoBehaviour
{
    private void Start()
    {
        IEnumerable<int> numbers = Enumerable.Range(1, 20);

        var results = numbers.Where(n => n > 10)
            .Take(5);

        foreach(int num in results)
        {
            Debug.Log(num);     //11 12 13 14 15
        }
    }
}

Takeメソッド指定した個数分の要素を取得します
もし指定した個数分がないときは取得できるところまで取得します。

TakeWhileメソッド

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

public class Hoge: MonoBehaviour
{
    private void Start()
    {
        IEnumerable<int> selected = Enumerable.Range(1, 20)
            .TakeWhile(x => x < 10);

        foreach(int num in selected)
        {
            Debug.Log(num);     //1 2 3 4 5 6 7 8 9
        }
    }
}

TakeWhileメソッド指定した条件を満たしている間だけ要素を取得し、条件を満たさない要素を見つけた時点で終了します

SkipWhileメソッド

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

public class Hoge: MonoBehaviour
{
    private void Start()
    {
        IEnumerable<int> selected = Enumerable.Range(1, 20)
            .SkipWhile(n => n <= 10);

        foreach(int num in selected)
        {
            Debug.Log(num);     //11 12 13 14 15 16 17 18 19 20
        }
    }
}

SkipWhileメソッド条件を満たしている間は要素を読み飛ばし、それ以降の要素を取り出します

Anyメソッド

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

public class Enemy
{
    public string Name { get; set; }
    public int Hp { get; set; }
    public int Exp { get; set; }
}

public class Hoge: MonoBehaviour
{
    private void Start()
    {
        IEnumerable<int> numbers = Enumerable.Range(1, 5);

        bool exist = numbers.Any(n => n % 5 == 0);

        Debug.Log(exist);   //true

        var enemies = new List<Enemy>
        {
            new Enemy{ Name = "スライム" , Hp = 10,Exp = 3 },
            new Enemy{ Name = "ももんじゃ" , Hp = 20,Exp = 10 },
            new Enemy{ Name = "ドラキー" , Hp = 20,Exp = 20 },
            new Enemy{ Name = "メタルスライム" , Hp = 5,Exp = 100 },
        };

        bool exist2 = enemies.Any(n => n.Exp % 7 == 0);
        
        Debug.Log(exist2);  //false

    }
}

Anyメソッド条件に一致する要素がコレクション内に一つでも存在するかどうか調べられます

Allメソッド

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

public class Enemy
{
    public string Name { get; set; }
    public int Hp { get; set; }
    public int Exp { get; set; }
}

public class Hoge: MonoBehaviour
{
    private void Start()
    {
        IEnumerable<int> numbers = Enumerable.Range(1, 5);

        bool isAllPositive = numbers.All(n => n > 0);

        Debug.Log(isAllPositive);

        var enemies = new List<Enemy>
        {
            new Enemy{ Name = "スライム" , Hp = 10,Exp = 3 },
            new Enemy{ Name = "ももんじゃ" , Hp = 20,Exp = 10 },
            new Enemy{ Name = "ドラキー" , Hp = 20,Exp = 20 },
            new Enemy{ Name = "メタルスライム" , Hp = 5,Exp = 100 },
        };

        bool isAllPositive2 = enemies.All(n => n.Exp > 50);
        
        Debug.Log(isAllPositive2);  //false

    }
}

Allメソッドコレクション内のすべての要素が条件を満たしているかどうかを調べられます

SequenceEqualメソッド

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

public class Hoge: MonoBehaviour
{
    private void Start()
    {
        IEnumerable<int> numbers1 = Enumerable.Range(1, 5);
        IEnumerable<int> numbers2 = Enumerable.Range(1, 5);

        bool equal = numbers1.SequenceEqual(numbers2);

        Debug.Log(equal);   //true
    }
}

SequenceEqualメソッド2つのコレクションの要素が等しいか調べることができます
ただ同じオブジェクトを参照しているかどうかではないことに注意してください。

Countメソッド

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

public class Enemy
{
    public string Name { get; set; }
    public int Hp { get; set; }
    public int Exp { get; set; }
}

public class Hoge: MonoBehaviour
{
    private void Start()
    {
        IEnumerable<int> numbers = Enumerable.Range(1, 5);

        int count = numbers.Count(n => n <= 3);

        Debug.Log(count);   //3

        var enemies = new List<Enemy>
        {
            new Enemy{ Name = "スライム" , Hp = 10,Exp = 3 },
            new Enemy{ Name = "ももんじゃ" , Hp = 20,Exp = 10 },
            new Enemy{ Name = "ドラキー" , Hp = 20,Exp = 20 },
            new Enemy{ Name = "メタルスライム" , Hp = 5,Exp = 100 },
        };

        double count2 = enemies.Count(n => n.Exp <= 50);
        
        Debug.Log(count2);  //3

    }
}

Countメソッドコレクションの中に条件に一致する要素数を調べられます

Averageメソッド

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

public class Enemy
{
    public string Name { get; set; }
    public int Hp { get; set; }
    public int Exp { get; set; }
}

public class Hoge: MonoBehaviour
{
    private void Start()
    {
        IEnumerable<int> numbers = Enumerable.Range(1, 5);

        double average = numbers.Average();

        Debug.Log(average);     //3

        var enemies = new List<Enemy>
        {
            new Enemy{ Name = "スライム" , Hp = 10,Exp = 3 },
            new Enemy{ Name = "ももんじゃ" , Hp = 20,Exp = 10 },
            new Enemy{ Name = "ドラキー" , Hp = 20,Exp = 20 },
            new Enemy{ Name = "メタルスライム" , Hp = 5,Exp = 100 },
        };

        double averageExp = enemies.Average(x => x.Exp);
        
        Debug.Log(averageExp);  //33.25

    }
}

Averageメソッドコレクションの平均値を求めることができます

Sumメソッド

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

public class Enemy
{
    public string Name { get; set; }
    public int Hp { get; set; }
    public int Exp { get; set; }
}

public class Hoge: MonoBehaviour
{
    private void Start()
    {
        IEnumerable<int> numbers = Enumerable.Range(1, 5);

        int sum = numbers.Sum();

        Debug.Log(sum);     //15

        var enemies = new List<Enemy>
        {
            new Enemy{ Name = "スライム" , Hp = 10,Exp = 3 },
            new Enemy{ Name = "ももんじゃ" , Hp = 20,Exp = 10 },
            new Enemy{ Name = "ドラキー" , Hp = 20,Exp = 20 },
            new Enemy{ Name = "メタルスライム" , Hp = 5,Exp = 100 },
        };

        int sumExp = enemies.Sum(x => x.Exp);
        
        Debug.Log(sumExp);  //133

    }
}

Sumメソッドコレクションの合計を求めることができます

Maxメソッド・Minメソッド

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

public class Enemy
{
    public string Name { get; set; }
    public int Hp { get; set; }
    public int Exp { get; set; }
}

public class Hoge: MonoBehaviour
{
    private void Start()
    {
        IEnumerable<int> numbers = Enumerable.Range(1, 5);

        int max = numbers.Max();

        Debug.Log(max);     //5

        var enemies = new List<Enemy>
        {
            new Enemy{ Name = "スライム" , Hp = 10,Exp = 3 },
            new Enemy{ Name = "ももんじゃ" , Hp = 20,Exp = 10 },
            new Enemy{ Name = "ドラキー" , Hp = 20,Exp = 20 },
            new Enemy{ Name = "メタルスライム" , Hp = 5,Exp = 100 },
        };

        int minExp = enemies.Min(x => x.Exp);
        
        Debug.Log(minExp);  //3

    }
}

Maxメソッド・Minメソッドはそれぞれコレクションの最大値・最小値を求めます

+α

LINQには遅延実行,即時実行という性質があります。
ここでは紹介しませんが、是非調べてみてください。

さいごに

お疲れ様です。
想像以上にボリュームがすごくなってしまい、15000文字オーバーというヤバイ記事になってしまいました笑

ただLINQを使うことはたくさんのメリットがあるので、是非試してはどうでしょうか。