[Unity/C#]何故か一度に361個以上Instantiateすると固まる[解決済み]

通常ありえないところで一日無駄にしたので備忘録。
実際、発生した問題とUnityとはあまり関係がありませんが、ゲームの開発中に同じ問題に直面した人のために残しておきます。~~恐らく私以外にはいないことを祈る。~~

2019/09/29追記：コメントにてすごく尤もな指摘を受けたので少し加筆修正。あと記事内で「イテレータ」という言葉がありますが、用いられる意味としてはどちらかというと「カウンタ変数」が近いです。

# 事の発端(本題とは関係ない)
弾幕シューティング作ろう！
環境はPC想定でまあとにかく作るか。

基本中の基本のプログラムは作ったぞ……性能にもこだわりたいから、いろいろ調べた結果Unity ECSとC# Job Systemsを使おう！

……と、その前に**性能比較用に弾数をだんだん増やすスクリプト**を作った！　早速計測だ！(Ctrl+P)

**　突　然　の　死**

Ctrl+Shift+ESCでタスクマネージャで見てみるとエディタが応答なし。そして時間経過でメモリ使用量が増えていく。このままではメモリ圧迫してろくでもないことになるのは明白。慌てず騒がず落ち着いてタスクを終了。

# 経過
Unity関連でかなり色々と調べはしたものの問題は判明せず。これはもう無視してECSとか導入した方が良いのでは？　いやしかし問題を放置して新しい技術の勉強はちょっと気が引ける。どうしよう。

と、あるものを見つけた。
フリーズした画面。エディタ内のInspectorに弾の発射数を眺めるためにSerializeFieldにしていた変数が、フリーズするときは必ず「361」の数値になっているじゃ、あーりませんか！

しかし、「361」の数値は何を意味しているのか……。

ん、待てよ？　361ということは、360度、つまり**弾の数に合わせた発射角の計算をイテレータ(i)に反映しているコード**に何か問題が！

# 原因
というわけで調べました。

問題のコードがこちら

```c#:敵クラス.cs
// メンバ変数
[SerializeField] private int bullet = 1;
/* ～～～～発射メソッド内～～～～ */
for (int i = 0; i < 360; i += (360 / bullet)) 
{
    // Instantiate
}
bullet++;
```

C#プログラミング上級者の方に見せたら「そんなことでか」と言われそうなミスであります。

この中のfor文
`for (int i = 0; i < 360; i += (360 / bullet))`
に問題があります。

最初は複合代入演算子`+=`か、イテレータの型`int i`か、floatとintの比較`<`に問題があるのかと考えていました。今思えば、3つ目に関しては二重に勘違いしていますね。

ということで順に勘違いを正していきましょう。

ちなみに`360`のハードコーディングはテスト用のプラグラムなので問題ありません。

# 常識的正しい知識
この問題を詳しく調査して得た成果物。勘違いを粉微塵にする正しい知識集です。問題の解決法は[こちら](#解説)に。

## 整数同士の除算で得られる数値は必ず整数
`3/2`、`6/5`、`3/4`はそれぞれ`1`、`1`、`0`となります。

仮に`float f = 3 / 2;`であっても、**整数同士の除算による計算結果がすべて整数**になるので、代入しようがしまいが`f`の中身は`1`になります。

そしてこの整数同士の除算の商は必ず**小数点以下切り捨て**となります。これはマイナスの数値であっても同様です。

ちなみにこれが私の知らなかった一番の事実です。これを知らずに間違った知識で致命的なバグを引き起こしました。

## floatとintの比較は特に問題ない
intはfloatに対して暗黙的型変換が可能なので、内部的にfloat同士で比較することが可能というわけです。
というかintとfloatの入り混じった**式**は特に問題ありません。
ただし、floatの値は必ずしも正確ではないことに注意。

## floatとintの相互暗黙的型変換は成り立たない
一つ上の段落でintはfloatに暗黙的型変換が可能と述べましたが、対するfloatはdecimalに対してのみ暗黙的型変換が許されているのみで、float to intの変換は明示的、つまりキャストが必要です。

代入でいえば

```cs:代入.cs
float f = 3f / 2;    // 1.5 (内部的にfloat同士で計算している)
int i = 3/2;         // 1

f = i;               // OK
i = f;               // Error
i = (int)f;          // OK
i = 3f / 2;          // Error
```

メソッドでいえば

```cs:メソッド.cs
void Start()
{
    A(2.4f);    // Error
    B(1);       // OK
    B(3/2);     // OK
}

void A(int i) {}

void B(float f) {}
```

このような感じです。

まあこのあたりは直面した問題とさして関係ありません。

# 解説
改めてこのコードの何が悪いのかを見ていきます。

```cs:敵クラスの発射メソッド.cs
for(int i = 0; i < 360; i += (360 / bullet)) {}
```

まず、`i += (360 / bullet)`の部分。
`bullet`が**361**以上になると、整数同士の除算の法則に基づき`0`が導き出され`i`に加算されます。
必然と`0`を足し続けることになるので、このfor文は**無限ループ**と化します。
……だから応答なしになるし、内部的には際限なくInstantiateしていることになるのでメモリ使用量も増えるばかりなのです。

これを回避するためには、単に整数同士でなくすればよいだけです。ただし`bullet`変数はintである必要があるので、360をfloat型にしましょう。

するとint型の`i`にはfloat型を格納することになるので、`i`もfloat型に変更~~しなければなりません。~~**ぁちょっと待ったァ！**

for文の初期化式は基本的にint型`int i = 0;`でfloat型は使うべきではないと、わりと昔から言われているようです。現に私は少しだけ既視感を感じています。

for文にfloat型は望ましくないというなら、じゃあどうするのか教えてくれよキ○ベツ太郎と。
というかそもfor文の更新式で角度の計算も兼ねるとか横着すんじゃねえよと。
普通にfor文の基本形`for(int i = 0; i < something; i++)`に沿えばいいだろう、というわけで。

```cs:敵クラスの発射メソッド.cs
for(int i = 0; i < bullet; i++)
{
    float degree = 360f / i;
    /* 角度をベクトルに変換する処理とかInstantiateとか */
}
```

このように変更することで無限ループを回避しました。

ちなみに`360f`のハードコーディングはテスト用のプログラムなので問題ありません。(2回目)

~~……最初から横着しなければよかっただろうという至極尤もな意見は受け付けません。あしからず。~~

## 問題解決前の私の考えと結論
- 複合代入演算子に問題が
    - ないです
- イテレータがint型なのが問題~~
    - ~~そうだよfloatに変えろ~~
    - **ないです**
        - for文のカウンタ変数はint型(整数型)であるべし
    - ~~イテレータを検索すると抽象化がどうのと出てくる……あれ？意味違う？C#のリファレンスには反復子とはあるけど、主にforeach……あっカウンタ変数……きちんと名称は調べるべきだな……ままええわ~~
- floatとintの比較に問題
    - ないです
    - もっと言えばfloatとintの比較ですらない
    - 型変換たって宣言した変数の型そのものが変わるわけないだろ~~このアホンダラ~~

## そもそも論

### ~~for文内で除算をするのにイテレータにfloat型を使わないのはおかしい~~
~~そも~~for文内に`i += (360 / bullet)`という処理があり、角度の計算も兼ねるのでもちろんのこと整数以上の精度が必要~~なのにもかかわらず、int型のまま放置しているという開発者にはありえないミス~~とはいえfor文の初期化式にfloat型の変数は使うべきではない。**float型の計算はあまり精度良くないってわりとどこでも言っているだろう！**
そも特別な理由がない限り**for文の()内の処理で別の処理を兼ねたりする横着はしない**ようにする。(戒め)
いやまあ発射角度が360度以下のならまあ想定通りにはなるか？
その場合はまた別の実装をしないと無限ループは避けられないな。~~for文の`()`内で余計なこと(横着)をしないとか。~~(余計なことしてました(小声))

# 参考
[算術演算子 - C# リファレンス | Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/operators/arithmetic-operators)