突然ですが、次の要件を満たすコードを考えてみてください。
FizzBuzz
・int型の変数を一つ引数に取るメソッド
FizzBuzz
を作成する
・受け取った数が3で割り切れる時はFizz
とデバッグする。
・受け取った数が5で割り切れる時はBuzz
とデバッグする。
・受け取った数が3と5の両方で割り切れる時はFizzBuzz
とだけデバッグする。
・それ以外の場合は受け取った数をデバッグする。
皆さんご存知の通り、なんの変哲もないただのFizzBuzzです。Unityで書く場合はこうなるでしょう。
using UnityEngine;
public class Test : MonoBehaviour
{
private void FizzBuzz(int number)
{
if (number % 15 == 0) // 3でも5でも割り切れたら
{
Debug.Log("FizzBuzz");
}
else if (number % 3 == 0) // 3で割り切れたら
{
Debug.Log("Fizz");
}
else if (number % 5 == 0) // 5で割り切れたら
{
Debug.Log("Buzz");
}
else // それ以外の場合
{
Debug.Log(number);
}
}
}
しかし、私から言わせてもらうと、このコードには多くの「無駄」が含まれています。どういうことでしょうか?
一つの「洗練」された例をお見せしましょう。
class Test : MonoBehaviour
{
void FizzBuzz(int number) => Log($"{(number % 15 == 0 ? "FizzBuzz" : number % 3 == 0 ? "Fizz" : number % 5 == 0 ? "Buzz" : number)}");
}
global using static UnityEngine.Debug;
global using UnityEngine;
別のスクリプトでglobal using
を利用したり、ラムダ式や三項条件演算子を使うことで非常にスッキリしましたね!!!
.........はい、こんなものを現場で見せたら即座にぶん殴られますね。
global using
はUnityが対応していないC# 10.0
以降でしか利用できず、三項条件演算子が多すぎて慣れていない人にはきっと何かの呪文に見えるでしょう。
今回はこのような、 無駄なく洗練された完全に無駄な省略記法 について触れていきたいと思います。
記事の最後には、これを応用した神コードを用意しているので、そちらも見ていただけたらと思います。
using staticでコードを省略 - 便利度: 4 可読性: 3
先ほど見せたように、Debug.Log();
をLog();
のみで記述できるようになります。例を見てみましょう。
using static UnityEngine.KeyCode; // "KeyCode."と書く必要がなくなった
using static UnityEngine.Debug; // "Debug."と書く必要がなくなった
using static UnityEngine.Input; // "Input."と書く必要がなくなった
using UnityEngine;
class UsingStatic : MonoBehaviour
{
void Start()
{
if (Input.GetKey(KeyCode.A)) Debug.Log("ほげ"); // 長い
if (GetKey(A)) Log("ふが"); // 全く同じように使える
}
}
正直、普通に便利ですね。
命名が被ってしまった時は、以前と同様に書くことで回避できます。
global usingでusingを省略 - 便利度: 5 可読性: 4
Unityが対応していないC# 10.0
以降で利用できる機能です。
先ほど見せたように、一度任意のスクリプトにglobal using
/global using static
と書くことで、他の全てのスクリプトでその表記を省略できます。例を見ましょう。
global using static UnityEngine.KeyCode; // using staticでも同様に利用可能
global using static UnityEngine.Debug;
global using static UnityEngine.Input;
global using UnityEngine;
class Test : MonoBehaviour
{
void Start()
{
if (GetKey(A)) Log("ほげ"); // 別のスクリプトでも同じように使える
}
}
C# 10.0
以上の導入が少し大変ですが、使えるようになると非常に便利です。共同開発で勝手に使い始めたらきっと平手打ちされます。
並列で別々に宣言・代入する - 便利度: 2 可読性: -1
他の言語と同じように、同じ型を宣言したい時は、次のように一行で宣言することができましたね。
void Start()
{
int hoge = 0, fuga = 0;
}
実は、タプルを利用することで、別の変数型であっても同じ行で宣言することが可能です。
void Start()
{
(int hoge, float fuga, Vector2 piyo) = (0, 0f, new Vector2(0f, 0f));
}
勘のいい人は気づいたかもしれません。これは通常の代入や等値演算子でも利用可能です。
void Start()
{
int hoge; // 普通の宣言
(float fuga, Vector2 piyo) = (0f, Vector2.zero); // タプルで宣言
(hoge, fuga, piyo) = (2, 1f, Vector2.one); // タプルで代入
hoge = 1; // 普通に代入
if ((fuga, piyo) == (1f, Vector2.one)) Log("一致しています!!!"); // タプルで等値演算子を使う
}
タプルの機能を利用しているので、幾つでも並列で同じ処理を行うことができます。やばいですね。こんなコードが動いてしまうのです。
こんなコードを提出したら除霊されること間違いなしです!
条件式で代入 - 便利度: 3 可読性: 1
条件式はif, while, for, switch
などを使う時や、bool型の変数に代入する時に使いますね。
実は、条件式の中で変数に値を代入することが可能です。例を見ていきましょう。
void Start()
{
Transform hoge;
GameObject fuga = new GameObject("Fuga"); // Fugaという名前のGameObjectを作成
Transform parent = new GameObject("Parent").transform; // Parentという名前のGameObjectを作成し、そのTransformを取得
(hoge = Instantiate(fuga).transform).parent = parent; // hogeをInstantiateしたFugaのTransformに設定し、Parentの子として設定
}
新たにオブジェクトを作り、そのTransform
を代入するのと同時に、親オブジェクトを設定するコードです。
何が何だかというところですが、Instantiate(...).transform
で生成したオブジェクトのTransform
を取得して代入し、その親オブジェクトをparent
に設定しています。
Unity・C#でこの書き方をしている人はあまり見かけませんが、C言語のような構造化プログラミングのパラダイムでは割とよくみる表現法です。
三項条件演算子で代入 - 便利度: 2 可読性: -10
条件式での代入は三項条件演算子の中でも利用できます。例を見てみましょう。
using UnityEngine.UI;
using UnityEngine;
class Test : MonoBehaviour
{
[SerializeField] Slider hpSlider; // インスペクターからスライダーを設定する
float hp = 1f; // HPの値
void HpUpdate(float number)
{
hpSlider.value = (hp + number) > 1f ? hp = 1f : (hp + number) < 0f ? hp = 0f : hp += number;
}
}
HPバーであるhpSlider
の値をnumber
の分だけ増やすのと同時にhp
の値を変更するコードです。number
がマイナスの場合は、その分減少することになります。
三項条件演算子を使うことで、0から1の範囲に収まるようにしています。
ラムダ式 - 便利度: 4 可読性: 4
メソッドを一行で書ける有名な記述法です。例を見てみましょう。
using static UnityEngine.Random;
using UnityEngine.TMPro;
using UnityEngine;
class Test : MonoBehaviour
{
[SerializeField] TextMeshProUGUI scoreText; // エディタからテキストを設定する
int score = 0; // スコア
bool RandomPercent(float percent) => percent > Range(0f, 1f); // 入力した確率に基づいてboolを返すメソッド
void ScoreUpdate(int _score) => scoreText.text = $"スコア: {(score += _score)}"; // 戻り値がvoidでもラムダ式は使える
}
テキストであるscoreText
の値を_score
の分だけ増やすのと同時にscore
の値を変更するコードです。文字列補間の中で三項条件演算子を使っています。文字列補間の中で:
を直接書くとエラーが出るので()
で囲んであげましょう。
残念ながら、if文のように戻り値を持たない制御文は書けません。しかし、例のように三項条件演算子を使って代替できることもあります。
使用例
global using UnityEngine;
global using static UnityEngine.Quaternion;
class Test : MonoBehaviour
{
[SerializeField] Transform parentTransform;
[SerializeField] GameObject prefab;
(Transform transform, MeshRenderer childRenderer)[] array;
int count = 10;
Vector2 卍(int i) => new Vector2(0, i - (count - 1) / 2f); // マジ卍
void Start()
{
array = new (Transform, MeshRenderer)[count];
for (int i = 0; i < count; i++) ((array[i].transform = Instantiate(prefab, 卍(i), identity).transform).parent, array[i].childRenderer) = (parentTransform, array[i].transform.GetChild(0).GetComponent<MeshRenderer>());
}
}
この前書いたコードの一部を切り取って改変したり簡単にしたりしたものです。解説はしません。頑張って読んでください。
おまけ(追記)
Switch式でパターンマッチング - 便利度: 4 可読性: 5
switch
式でパターンマッチングを利用すると、次のようにFizzBuzz
を記述することができます。
void FizzBuzz(int number) => Log((number % 3, number % 5) switch
{
(0, 0) => "FizzBuzz",
(0, _) => "Fizz",
(_, 0) => "Buzz",
_ => number.ToString()
});
FizzBuzz
メソッドはDebug.Log();
の引数の中でパターンマッチングを利用しており、単一の行としてみなされるため、ラムダ式で{}
を省略することができます。
先に紹介したmと比べると可読性が高いので、おまけとして追記しました。