UnityのSerializeFieldのint型のRangeがうまく設定できない件

# 動作確認環境

Unity 2023.2.13f1 (※画像は一部で2020.3.20f1を使用しているものがあります)
VisualStudio 2017

# UnityのSerializeFieldで上限・下限を設定する

SerializeFieldで上限下限を設定する方法はおそらくご存知の方も多いはず。

```C#
[SerializeField]
[Range( 0, 100 )]
private int hoge = 0;
```

これは下限0, 上限100の設定です。
下限は0で上限は数値の型の範囲なんでもいいなんて場合もあると思います。
intの最大値はint.MaxValueだからこれを設定したら最大値はintの範囲内で好きなところにできるだろうと思い、設定してみました。

```C#
[SerializeField]
[Range( 0, int.MaxValue )]
private int hoge = 0;
```

上記を記述し、エディタを設定してみるとなぜか負の数になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/dcc0753c-7c06-57df-f3a6-933209dda1e5.png)

公式ドキュメントを見ても特に記載はありませんでした。
https://docs.unity3d.com/ja/2020.3/ScriptReference/RangeAttribute.html

エディタで切れている部分を実際に見てみると

```C#
[SerializeField]
[Range( 0, int.MaxValue )]
private int hoge = 0;

void Awake()
{
    Debug.Log( $"hoge={hoge}" );
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/496bdee5-1637-233c-a175-f42fbd7ae5e3.png)

となっており、「int.MaxValue=2,147,483,647」をオーバーフローしているようです。

int.MaxValueがバグってるのか？と思って確認してみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/cea40539-fa40-b0d1-1c77-0e73905b996c.png)

定義は問題なさそうなので、実際に表示してみました。

```C#
[SerializeField]
[Range( 0, int.MaxValue )]
private int hoge = 0;

void Awake()
{
    Debug.Log( $"hoge={hoge} int.MaxValue={int.MaxValue}" );
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/eda05281-d71b-806f-6832-86046f31b757.png)

int.MaxValueは正常なようです。
どうやらRangeを経由するとおかしくなるようです。

### どこまでなら正常な値が表示されるか調べてみた

下記のように記載して、地道に調べてみました。
```C#
[SerializeField]
[Range( 0, int.MaxValue - 10 )]
private int hoge = 0;

[SerializeField]
[Range( 0, int.MaxValue - 50 )]
private int fuga = 0;
```
その結果、正常に表示される範囲を特定しました。
```C#
[SerializeField]
[Range( 0, int.MaxValue - 63 )]
private int hoge = 0;

[SerializeField]
[Range( 0, int.MaxValue - 64 )]
private int fuga = 0;
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/974db122-22cd-76a5-fb88-e91f88b6a61b.png)

どうやら「in.MaxValue - 64」までが正常に出せる正常値のようです。

エディタ上で表示が切れていますが、値を確認してみると、

```
int.MaxValue - 64 = 2,147,483,520
```
となっていました。

int(符号付き32ビット整数)の範囲は -2,147,483,648 ～ 2,147,483,647 のはずなので、2,147,483,520 に 64 を足すと 2,147,483,584 となり、intの範囲内ですが、元の2,147,483,647には戻りません。

https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/integral-numeric-types

### どうしてこんな結果になったのか調べてみた

Rangeの実装を見たところ、floatにキャストされてました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/a0cb7a42-7192-314b-8475-75f55ffe7ecb.png)

つまり「int.MaxValueをfloatにキャストするとおかしくなるということでは？」と疑い、下記のコードで確認してみました。

```C#
[SerializeField]
[Range( 0, int.MaxValue )]
private int hoge = 0;

void Awake()
{
    Debug.Log( $"hoge={hoge} int.MaxValue={int.MaxValue} (float)int.MaxValue={(float)int.MaxValue}" );
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/0a19f743-0149-fdcb-2508-b9f1a997e583.png)

これだと下の桁が見えないので、桁数指定を入れてちょっと書き換えてみました。
```C#
[SerializeField]
[Range( 0, int.MaxValue )]
private int hoge = 0;

void Awake()
{
    Debug.Log( $"hoge={hoge} int.MaxValue={int.MaxValue} (float)int.MaxValue={(( float )int.MaxValue):0000000000}" );
}
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/a5840c8d-dc88-fb53-c589-278e266ed958.png)


この結果からキャストでオーバーフローしてることは確定みたいですね。

floatにキャストしたまま使うと思うので、大体の場合は問題ないのだと思いますが、Rangeはintにキャストし直すために問題が起きているのだと思われます。

### 63 と 64 の境界について調べてみた

int.MaxValue-63 と int.MaxValue-64をfloatにキャストした値をそれぞれ確認してみました。
こちらも確認しやすいように桁数指定を入れています。
```C#
void Awake()
{
    Debug.Log( $"(float)( int.MaxValue - 63 )={( float ) ( int.MaxValue - 63 ):0000000000} (float)( int.MaxValue - 64 )={( float ) ( int.MaxValue - 64 ):0000000000}" );
}
```
表示上は同じように見えます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/4db33ea2-e5fa-ea2b-906d-e434dec09c7c.png)

intにキャストし直す時に起きるようなので下記のように書き直してみました。

```C#
void Awake()
{
    Debug.Log( $"(int)(( float ) ( int.MaxValue - 63 ))={(int)(( float ) ( int.MaxValue - 63 ))} ( int ) (( float ) ( int.MaxValue - 64 ))={( int ) (( float ) ( int.MaxValue - 64 ))}" );
}
```
63の時はエラーが出てしまいました…
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/1ecae2ad-264d-0866-67fb-dc2aaa866445.png)


#### C++で試してみた

C++とC#でキャストの結果は同じだと思いますが、念のため確認。
お試しでこんな感じで書いてみました。

```C++
int main()
{
    printf( "INT_MAX(%d)をfloatキャスト(%10f)してintに戻す(%d)\n", INT_MAX, ( float ) INT_MAX, ( int )( ( float ) INT_MAX ) );
    printf( "64引いてみると…→%10f\n", ( ( float ) ( INT_MAX - 64 ) ) );
    printf( "63引いてみると…→%10f\n", ( ( float ) ( INT_MAX - 63 ) ) );
    printf( "64引いてみると…→%d\n", ( int ) ( ( float )( INT_MAX - 64 ) ) );
    printf( "63引いてみると…→%d\n", ( int ) ( ( float )( INT_MAX - 63 ) ) );

    return 0;
}
```

出力結果↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/6074867f-e6e3-0e56-a609-e3b8096c3fc6.png)


C++もC#もキャストの計算は一緒なので、同じ結果になると思っていましたが、予想通りの結果でした。

つまりRangeに限らず、int.MaxValueをfloatにキャストして再度intにキャストしようとしたときに必ず起きる問題です。

これはそれぞれの型の有効桁数がintは9～10桁(-2,147,483,647～2,147,483,647)、floatはC++の場合は6～7桁、C#の場合は6～9桁なので実はfloatの方が精度が低いから起きる現象です。
(下記参考リンク参照)
キャストについてはまた記事を書くとして、今回は他の型はちゃんと使えるのか調べてみたいと思います。

参考リンク
https://learn.microsoft.com/ja-jp/cpp/cpp/data-type-ranges?view=msvc-170
https://learn.microsoft.com/ja-jp/cpp/c-language/type-float?view=msvc-170
https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/floating-point-numeric-types

## 他の型も調べてみる
調べてみました。

### float
2種類の書き方を試してみました(変わらないと思うけど)

```C#
[SerializeField]
[Range( 0, float.MaxValue )]
private float hoge = 0;

[SerializeField]
[Range( 0, Mathf.Infinity )]
private float fuga = 0;
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/813a9c6a-804b-e175-e491-8826b9c454f4.png)

正しく表示されました。

### byte
```C#
[SerializeField]
[Range( 0, byte.MaxValue )]
private byte hoge = 0;
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/8eb34cbb-77a9-3800-4d5b-496a6aa46fd6.png)

正しく表示されました。

### uint

ここまで調べた感じだと int がダメダメっぽいので、それなら uint ならどうだ？と思い、確認しました。

```C#
[SerializeField]
[Range( 0, uint.MaxValue )]
private uint hoge = 0;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/6a42d48f-4f00-ccfa-0b15-3ddb6af4bd1e.png)

0から動かせなくなりました。

### 型違いはどうなるのか？

こんな感じで試してみました。
```C#
[SerializeField]
[Range( 0, int.MaxValue )]
private float hoge = 0;

[SerializeField]
[Range( 0, int.MaxValue )]
private double fuga = 0;

[SerializeField]
[Range( 0, byte.MaxValue )]
private float hogehoge = 0;

[SerializeField]
[Range( 0, byte.MaxValue )]
private double fugafuga = 0;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/95c25df6-c412-fef9-2451-65ef78e08321.png)

エディタの表示が切れてしまっていてint.MaxValueの方の値がおかしなようにみえますが、設定されている値は「2.147484e+09」なので問題なさそうです。
(ここまで調べた結果ではint.MaxValueにはなっていなさそうなので問題があるといえばありますが…)


## 負の数はどうなるのか？
負の数はどうなるか試してみました。

### int

```C#
[SerializeField]
[Range( int.MinValue, 0 )]
private int hoge = 0;
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/c80dd88d-67e5-9328-2d15-283dc20eabff.png)

正常に表示されました。
エディタの表示で見えない部分も-2,147,483,648と表示されていて正常です。

### float
```C#
[SerializeField]
[Range( float.MinValue, 0 )]
private float hoge = 0;

[SerializeField]
[Range( Mathf.NegativeInfinity, 0 )]
private float fuga = 0;
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/eef014f1-7606-d5cc-2424-b89c67ef3cd0.png)

正常に表示されました。
## int.MinValue vs int.MaxValue

int.MaxValueが負になるなら下限をint.MinValue、上限をint.MaxValueにしたらどうなるのか試してみました。

```C#
[SerializeField]
[Range( int.MinValue, int.MaxValue )]
private int hoge = 0;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2969992/379dd322-1bd2-a402-eb81-8e9d104e14cd.png)

ハンドルが消失しました。
動かすことすらできません。


# まとめ

キャストの影響でおかしくなるのはint.MaxValueだけなようです。
Rangeでint.MaxValueを使いたいのであれば-64して使うか、他の方法の検討が必要になりそうです。