複数の値から最小値や最大値を求める簡単な方法はないの?
Mathf.Min/Maxメソッドを使えば良いわ。
複数の値の集合から最大値・最小値を求める方法の解説です。
Unity APIが使える環境なら、Mathf.Min、Mathf.Maxメソッドからそれぞれ最小値、最大値を求めることができます。
// 最小値を求める
var min = Mathf.Min(1, 2);
// 最大値を求める
var max = Mathf.Max(3, 4, 5);
引数には複数個の値を指定できますが、3個以上の値を引数として指定するとGC.Allocが発生します。 [1] パフォーマンス低下の要因となるため、注意が必要です。 [2]
この問題は、自作のラッパーメソッドなどを用いれば簡単に解決できます。 [3]
また、次のようにListなどのコレクションから最小値と最大値を求めることも可能です。
// int型のコレクション
List<int> inputValues;
・・・(中略)・・・
// Linqを用いて最小値・最大値を求める
var min = inputValues.Min();
var max = inputValues.Max();
このように、最小値・最大値を求める方法は複数存在し、状況に応じて使い分けることができます。
本記事では、複数個の値から最小値や最大値を求める方法をいくつか紹介します。
- Unity 2022.1.0f1
最小値を求める
Mathf.Minメソッドを使います。複数のfloat型またはint型の値から最小値を求めることができます。
メソッドの形式は次の通りです。
float型の場合
public static float Min(float a, float b);
public static float Min(params float[] values);
int型の場合
public static int Min(int a, int b);
public static int Min(params int[] values);
引数が2つとそれ以外、int型とfloat型でメソッドがオーバーロードされています。
3個以上の引数を渡す場合、可変長引数となり、配列として渡す形になります。
最大値を求める
Mathf.Maxメソッドを使います。引数の形式などはMathf.Minと一緒です。複数のfloat型またはint型の値から最大値を求めます。
float型の場合
public static float Max(float a, float b);
public static float Max(params float[] values);
int型の場合
public static int Max(int a, int b);
public static int Max(params int[] values);
サンプルコード
複数の指定された値から最大値、最小値を求めるサンプルスクリプトです。
using UnityEngine;
public class MinMaxExample : MonoBehaviour
{
// 入力値の一覧
[SerializeField] private int[] _inputValues;
private void Start()
{
// 最小値・最大値を求める
var min = Mathf.Min(_inputValues);
var max = Mathf.Max(_inputValues);
// 結果出力
Debug.Log($"最小値 = {min}, 最大値 = {max}");
}
}
このスクリプトをMinMaxExample.csという名前でUnityプロジェクトに保存し、適当なオブジェクトにアタッチすると機能するようになります。
インスペクターよりInput Valuesに入力値を複数指定できます。
実行結果
入力値とその結果です。
入力値
出力結果
3個以上の引数を渡す場合の注意点
Mathf.Min/Maxメソッドは、複数個の値を引数として渡せること解説しました。
しかし、ここに一つ落とし穴があります。それは、3個以上の値を引数として渡すと、メモリ領域の確保(AC.Alloc)が必ず発生してしまう点です。
次にその検証用コードと結果を示します。
検証用コード
3つの値から最大値を毎フレーム求めるだけのサンプルスクリプトです。
using UnityEngine;
public class ThreeValuesTest : MonoBehaviour
{
private void Update()
{
// 3つの値から最大値を求める
var max = Mathf.Max(1, 2, 3);
}
}
プロファイラでの検証結果
プロファイラを見ると、上記スクリプトで毎フレーム44ByteのGC.Allocが発生しています。
これは、複数の引数を渡す際に新しい配列を確保しているためです。
意図せずメモリ確保が行われ続けた場合、処理によってはメモリの断片化と共にマネージヒープが拡張され続ける可能性があります。詳細については必ず公式リファレンス等の一次情報をご確認の上判断してください。
3引数以上でもGC.Allocを発生させないようにする
GC.Allocを回避する手段はいくつか存在しますが、あまり多くない個数であれば、独自のラッパーを作れば簡単に解消できます。
ラッパーの例
3引数から最小値・最大値を求めるメソッドを実装したラッパーの例です。
public static struct MathUtil
{
// 3引数から最小値を求める
public static int Min(int v1, int v2, int v3)
{
var minValue = v1;
if (v2 < minValue) minValue = v2;
if (v3 < minValue) minValue = v3;
return minValue;
}
// 3引数から最大値を求める
public static int Max(int v1, int v2, int v3)
{
var maxValue = v1;
if (v2 > maxValue) maxValue = v2;
if (v3 > maxValue) maxValue = v3;
return maxValue;
}
}
例ではfloat型や3引数以外のものは割愛していますが、同じ要領で実装していけば良いです。
使用例
呼び出し側では、Mathf構造体をMathUtil構造体 [4] に置き換えるだけで使えるようになります。
// 最小値・最大値を求める
var min = MathUtil.Min(1, 2, 3);
var max = MathUtil.Max(4, 5, 6);
配列を直接指定する場合
引数に配列をそのまま渡してもGC.Allocを抑えられます。
using UnityEngine;
public class ArrayMaxTest : MonoBehaviour
{
private int[] _inputValues = new int[] {1, 2, 3};
private void Update()
{
// 3つの値から最大値を求める
var max = Mathf.Max(_inputValues);
}
}
修正後の結果
GC.Allocがなくなっていることが確認できました。
Listなど配列以外のコレクションの最小値・最大値を求める
List<T>型や、IEnumerable<T>型など、配列(Array<T>型)以外のコレクションから最小値、最大値を求めたい場合は、Mathf.Min/Maxメソッドが使えません。
これを可能にするため、本記事では以下の2つの方法を紹介します。
- Linqを使う(ただしGC.Allocが発生する)
- 独自実装する(List型など一部の型ではGC.Allocの発生を抑えられる)
前者のLinqを使う方法は手軽な分、算出処理を行う度に必ずGC.Allocが発生してしまいます。しかしながら、簡潔なコードになるのが大きな魅力です。
後者の独自実装する方法は、手間はかかりますがGC.Allocを抑えられる場合があります。しかし、IEnumerable<T>などインタフェース化された型に対してはGC.Allocを抑えることができません。
Linqを使った例
Linqを使って最小値・最大値を求めるサンプルスクリプトです。
using System.Collections.Generic;
using System.Linq; // Linqを使うために必要
using UnityEngine;
public class LinqMinMaxExample : MonoBehaviour
{
// List型の入力値
[SerializeField] private List<int> _inputValues;
private void Start()
{
// Linqを用いて最小値・最大値を求める
var min = _inputValues.Min();
var max = _inputValues.Max();
// 結果出力
Debug.Log($"最小値 = {min}, 最大値 = {max}");
}
}
出力結果は1つ目のスクリプトと一緒のため、割愛いたします。
次のように、Min/Max拡張メソッド呼び出しで1行で結果を得られます。
// Linqを用いて最小値・最大値を求める
var min = _inputValues.Min();
var max = _inputValues.Max();
GC.Allocが必ず発生するため、注意しておくと良いでしょう。 [5]
独自実装する例
余計なメモリ確保を解消するために、最小値、最大値を独自実装したサンプルスクリプトです。
using System.Collections.Generic;
// リストから最小値・最大値を求める拡張
// 今回はint型のみ対応している
public static class MinMaxExtensions
{
// int型のリストから最小値を求める
public static int Min(this IList<int> inputValues)
{
var count = inputValues.Count;
if (count == 0)
return default;
var minValue = inputValues[0];
for (var i = 1; i < count; ++i)
{
var value = inputValues[i];
if (value < minValue)
minValue = value;
}
return minValue;
}
// int型のリストから最大値を求める
public static int Max(this IList<int> inputValues)
{
var count = inputValues.Count;
if (count == 0)
return default;
var maxValue = inputValues[0];
for (var i = 1; i < count; ++i)
{
var value = inputValues[i];
if (value > maxValue)
maxValue = value;
}
return maxValue;
}
}
using System.Collections.Generic;
using UnityEngine;
public class ListMinMaxExample : MonoBehaviour
{
// List型の入力値
[SerializeField] private List<int> _inputValues;
private void Start()
{
// Linqを用いて最小値・最大値を求める
var min = _inputValues.Min();
var max = _inputValues.Max();
// 結果出力
Debug.Log($"最小値 = {min}, 最大値 = {max}");
}
}
1つ目のスクリプトはIList<T>型のコレクションから最小値・最大値を求める拡張メソッドの定義です。今回はint型のみの例を示しましたが、float型などその他の型も同じ要領で実装できます。
2つ目のスクリプトは1つめのスクリプトの拡張メソッドを使用した例です。
こちらも出力結果を割愛しますが、GC.Allocの発生が解消されています。
さいごに
複数の値の最小値・最大値はMathf.Min/Maxメソッドで求められます。
List型など配列以外のコレクションに対しての最小値・最大値を求める方法も紹介しました。
Mathf.Min/Maxメソッドは、3引数以上の場合は可変長引数扱いとなりGC.Allocが発生するため、パフォーマンスを求める場面では注意が必要です。
ラッパークラスを活用すれば、パフォーマンスと可読性両方の問題を一度に解決でき、実装も難しくありません。
どの方法が適切かは場面によって変わってくるため、それぞれの特徴を理解した上で使い分けてください。