これは ドリコム Advent Calendar 2019の24日目です。
23日目は 渡辺 祥二郎 さんによる、 アニメーターの基礎訓練&落とし穴 です。 こんにちは、ゲーム事業本部開発2部の青木です。
今年前半まではHTML5+JavaScriptでブラウザ向けゲーム開発をしていましたが
現在はCocos2d-xを使ったネイティブゲーム開発に携わっています。

今回の話題

さて、今回はC#でプログラムを書いている方々には既におなじみであろう
LINQについて筆を執りたいと思います。
LINQは便利で読みやすく、よく使われていますが、意外と詳しく知る機会はないなぁと
感じていたので、改めて調べてみました。
これを機に、今までLINQについて「なんとなーく便利そうだから使ってみた」という方も、
「使い倒してもう完璧!」という方も、よりLINQに興味を持っていただければ嬉しいです。
※筆者がゲーム開発に従事していることからサンプルコードはUnityで作成しております。
 予めご了承ください。

そもそもLINQとは何か

さて、ではそもそもLINQとは何でしょうか。
インターネットや書籍で調べると「統合言語クエリ」だそうです。
と、言われてもピンとこない方も多いと思いますので実際に例を見てみましょう。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
 
public class Sample1 : MonoBehaviour
{
  // Start is called before the first frame update
  void Start()
  {
    int[] list = new int[] { 1, 2, 3, 4, 5, 6, };
    IEnumerable<int> result = from x in list select x * x;
    // 1, 4, 9, 16, 25, 36が順に出力されます
    foreach (int item in result)
    {
      Debug.Log(item.ToString());
    }
  }
}

さて、見慣れない書き方だと思う方もいらっしゃるのではないでしょうか。
ここで統合言語クエリと呼ばれるのは
IEnumerable<int> result = from x in list select x * x;
の部分です。出力結果を見れば何をやっているかは一目瞭然、変数listに入っている
各要素を2乗しています。
LINQというのは、こういったリストのようなデータの集まりに対して何らかの処理をすること
であると思っていただければよいと思います。
では、上記の処理をもう少し馴染みの深い形に直してみましょう。

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

public class Sample2 : MonoBehaviour
{
  // Start is called before the first frame update
  void Start()
  {
    int[] list = new int[] { 1, 2, 3, 4, 5, 6, };
    IEnumerable<int> result = list.Select(x => x * x);
    foreach (int item in result)
    {
      Debug.Log(item.ToString());
    }
  }
}

書き方こそ違いますが、同じ結果になったと思います。
このようにSQLに似た形でもメソッドのような形でもLINQは表現できます。
ただし、SQLに似た形は使えない機能があったり、あまり使われないという観点から
以降はメソッド形式を中心にお話していきます。

LINQの特徴

LINQにはいくつか特徴があります。
この特徴を理解することが、LINQの奥深さにつながっていると思います。
それでは見ていきましょう。

処理をメソッドで指定できる

ほとんどのLINQでは引数に処理用のメソッドを取るので、柔軟に対応できます。
 例えば、以下のように複雑な変換もお手の物です。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;

public class Sample3 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        List<int> list = Enumerable.Range(1, 20).ToList(); // Rangeも実は便利
        IEnumerable<string> fizzBuzz = list.Select(i =>
        {
            if (i % 3 == 0)
            {
                if (i % 5 == 0)
                {
                    return "FizzBuzz";
                }
                return "Fizz";
            }
            if (i % 5 == 0)
            {
                return "Buzz";
            }
            return i.ToString();
        }
        );
        // 結果は実際にやってみてのお楽しみ
        foreach (string output in fizzBuzz)
        {
            Debug.Log(output);
        }
    }
}

IEnumerable型に実装されており、IEnumerable型を返す

SelectやWhereなど複数のデータを返すメソッドは必ずIEnumerable
もしくはそれを親に持つ型を返します。
これにより、以下のようにメソッドチェーンを使ってコードを書くことができます。

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

public class Sample4 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        List<int> list = new List<int> { 1, 5, -2, 3, -9, 6 };
        // メソッドチェーンで絞り込みと加工を同時に行う
        IEnumerable<int> result = list
            .Where(item => item % 2 == 0)
            .Select(item => item * item);
        // 4, 36の順に出力
        foreach(int item in result)
        {
            Debug.Log(item.ToString());
        }
    }
}

LINQの処理は遅延実行される

最後に「遅延実行」についてご紹介します。
LINQを使用したとき、実際に式が評価されるのはforeachなどで
要素にアクセスする時です。
イメージとしては、LINQのメソッドが実行された時に処理を予約しておいて、
実際に使う場面がきて初めて実行される感じとなります。
以下のコードを実行してみるとよくわかると思います。

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


public class Sample5 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        List<int> list = new List<int> { 6, -8, 1 };
        // 絶対値が小さい順に並び替え
        IEnumerable<int> result = list.OrderBy(item => Mathf.Abs(item));
        list.Add(5);
        // 1, 5, 6, -8の順に出力される
        foreach(int item in result)
        {
            Debug.Log(item.ToString());
        }
    }
}

OrderByメソッドを読んだのは「5」を追加する前ですが、きちんと順番に並び替えられています。
このように、呼び出した瞬間ではなく要素にアクセスした時に実行されます。

LINQのメリットとデメリット

それでは最後に、LINQで書くことによるメリットとデメリットを考えてみます。

メリット

読みやすい

最大のメリットは何と言っても読みやすさです。
やりたいこと1つにつき1つのメソッドを呼び出すので、
ぱっと見で何をしているのかがわかりやすくなります。
下記は、foreach文で書いたパターンとLINQで書いたパターンです。
これを比較してみても、LINQの方が簡潔に書けて読みやすいことがわかりますね。

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

public class Sample6 : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        // 4文字以上の文字列を探して字数のリストを作る
        List<string> source = new List<string>() { "Bird", "Dog", "Cat", "Fish", "Horse" };

        // -- foreach版 --
        List<int> forResult = new List<int>();
        foreach(string s in source)
        {
            int length = s.Length;
            if (length >= 4)
            {
                forResult.Add(length);
            }
        }
        foreach (int result in forResult)
        {
            Debug.Log(result.ToString());
        }

        // -- LINQ版 --
        IEnumerable<int> linqResult = source.Select(s => s.Length)
            .Where(l => l >= 4);
        foreach(int result in linqResult)
        {
            Debug.Log(result.ToString());
        }
    }
}

バグが発生しにくい

並び替え、絞り込み、変換などなど主要な機能は一通り網羅されているので
自分でロジックを書く必要が少なくなり、バグが入り込む余地が少なくなります。

データの集まりを表現する幅広い型に対応している

LINQのメソッドを持つIEnumerable型はDictionaryやSetなどList以外にも実装されているので、
汎用的に処理を書くことができます。
例えば、下記のようにDictionaryの特定のキーを持つものを取り出して加工するまでループ処理を書かずして実現できます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
 
public class Sample7 : MonoBehaviour
{
    void Start()
    {
        Dictionary<string, string> roles = new Dictionary<string, string>
        {
            { "Taro", "Programmer" },
            { "Hanako", "Designer" },
            { "Mike", "Planner" },
            { "Tatsuo", "Director" },
        };
 
        IEnumerable<string> results = roles
            .Where(person => person.Key.StartsWith("T"))
            .Select(person => person.Value.ToUpper());
        // PROGRAMMER, DIRECTORが出力される(Dictionaryなので順番は変わるかもしれない)
        foreach (string result in results)
        {
            Debug.Log(result);
        }
    }
}

デメリット

パフォーマンスの低下を招きやすい


最近のCPUであれば気になることはあまりないかもしれませんが
これはメソッドで呼び出すたびに、全要素にアクセスしていることを考えれば
容易に想像できるかと思います。
対策としては絞り込み(Where)を早い段階で使うなど要素へのアクセス数を
減らすことである程度軽減ができます。

まとめ


LINQについてまとめると、以下のようなことが言えます。
  • LINQはデータの集まりに対して処理をするもの
  • 3つの特徴がある
    • 処理をメソッドで指定できる
    • IEnumerable型に実装されており、IEnumerable型を返す
    • LINQの処理は遅延実行される
  • LINQのメリット
    • 読みやすい
    • バグが発生しにくい
    • データの集まりを表現する幅広い型に対応している
  • LINQのデメリット
    • パフォーマンスの低下を招きやすい

最後に


私がUnityに触れていた頃はAsParallelなど便利な機能が使えなかったのが
残念でしたが、最近ではこれらの機能も使えるようになりつつあり、
LINQの重要度も上がっているのではないかと思います。
この記事を読んで少しでもLINQについて興味を持ち、詳しく知りたいなと
思っていただけたなら嬉しいです。

明日は ナガノ さんの記事です。
ドリコムでは一緒に働くメンバーを募集しています!
募集一覧はコチラを御覧ください!