Edited at

LINQを覚えたてのUnity芸人がハマった罠


LINQとは?

コレクションの操作をオブジェクト指向っぽく記述できる便利機能です(だと思っています。)。

WhereとかSelectとか出てくるのでSQLとは別物ですが、SQLを少しでもかじっていると取っつきやすいと思います。

例えば、以下の作業をするとします。



  • List<int>型の_listTestの各要素の値を2倍にしたList<int>型のデータを新しく作る

LINQを使わないと以下のようなコードになると思います。


NoUsingLinq.cs

List<int> numTwiceList;

foreach(int num in _listTest){
numTwiceList.Add(num*2);
}

しかしっ!以上のコードはLINQを使えば以下のように書き換えられます!


UsingLinq.cs

List<int> numTwiceList = _listTest.Select(num => num*2).ToList<int>();


なんとっ!一行で書ける!しかも読みやすい!

(今回はあえて型を明示しています。)


Unityで活用しようと思った経緯

開発する際に、よく以下の手順を踏むことがある。

- ゲームのオブジェクトをあらかじめコレクションに格納しておく

- ボタンを押した際などのイベント時に、上記のコレクションの要素で条件にあてはまるものだけActiveにしたり、色を変えたりする

☆今回実装したメニュー画面

ダウンロード.gif

以上のようなことをする際に、

「LINQを使えば爆速で開発できるし、行数減らせるじゃんっ!」

と、思い立って書いたコードがこちら。


Menu.cs

using System.Collections;

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

public class Menu : MonoBehaviour
{
[SerializeField] private List<GameObject> _slideBarButtons;
[SerializeField] private List<GameObject> _mainPanelMenus;

[SerializeField] private Color _selectedColor;
[SerializeField] private Color _nonSelectedColor;

/// <summary>
/// ボタンを押して、セレクトボタンの色を変更します。
/// </summary>
/// <param name="name"></param>
public void SelectMenuButtonByName(string name)
{
_slideBarButtons.Where(b => b.name.Equals(name))
.ToList<GameObject>().ForEach(b => b.GetComponent<Image>().color = _selectedColor);
_slideBarButtons.Where(b => !b.name.Equals(name))
.ToList<GameObject>().ForEach(b => b.GetComponent<Image>().color = _nonSelectedColor);
}

/// <summary>
/// セレクトボタンを押して、メインメニューの表示を切り替えます。
/// </summary>
/// <param name="name"></param>
public void SelectMainMenuByName(string name)
{
_mainPanelMenus.Where(b => b.name.Equals(name)).ToList<GameObject>().ForEach(b => b.SetActive(true));
_mainPanelMenus.Where(b => !b.name.Equals(name)).ToList<GameObject>().ForEach(b => b.SetActive(false));
}
}


上記のコードのメソッド2つは、Unityのボタンイベントです。引数にUnityのHierarchy上の名前を代入して実行させる前提です。

__slideBarButtonsと_mainPanelMenusにはあらかじめUnityエディタ上から要素を追加しておきます。(そのための[SerializeField]です。)


書いてから気がついた、「全然良いコードになってない!

コインの裏と表のような条件分岐(例えば、Where(b => b.name.Equals(name))Where(b => !b.name.Equals(name)))を一つのメソッドでWhere文の中に二度も書いている・・・し、たいして可読性も上がっていない。

LINQで書き換えるものは、以下のような構造のものだと分かりました・・・。

- foreach文の中で条件分岐が1つしかない

- 条件分岐にelseがない


LinqIsUseful.cs

var collection = ~;

//LINQに書き換えたほうが良い構造
foreach(x in collection){
if(/*条件*/){
/*collectionの各要素に行う処理*/
}
}

//LINQに書き換える
collection.Where(/*条件*/).ForEach(/*collectionの各要素に行う処理*/);



こっちのほうが読みやすい

普通にforeach文で書き換えました。


Menu.cs

using System.Collections;

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

public class Menu : MonoBehaviour
{
[SerializeField] private List<GameObject> _slideBarButtons;
[SerializeField] private List<GameObject> _mainPanelMenus;

[SerializeField] private Color _selectedColor;
[SerializeField] private Color _nonSelectedColor;

/// <summary>
/// ボタンを押して、セレクトボタンの色を変更します。
/// </summary>
/// <param name="name"></param>
public void SelectMenuButtonByName(string name)
{
foreach (GameObject g in _slideBarButtons)
{
if (g.name.Equals(name))
{
g.GetComponent<Image>().color = _selectedColor;
}
else
{
g.GetComponent<Image>().color = _nonSelectedColor;
}
}
}

/// <summary>
/// セレクトボタンを押して、メインメニューの表示を切り替えます。
/// </summary>
/// <param name="name"></param>
public void SelectMainMenuByName(string name)
{
foreach(GameObject g in _mainPanelMenus)
{
if (g.name.Equals(name))
{
g.SetActive(true);
}
else
{
g.SetActive(false);
}
}
}
}


※if文の前にComponentの取り出しをやれよっ!


最後に

LINQ初心者がハマった罠について記述しました。

複数のコレクションの論理演算とか、その他LINQには大変便利な機能があることを存じていますので、今後使える場面があれば正しく使いたいと考えています。

以上っ!!