LoginSignup
7

posted at

updated at

【Unity】逆コンパイルから学ぶC#の内側

RiderのIL Viewer(Low-Level C#)からコンパイル後どういう実装が行われているかを知る事でC#を更に理解していきたいと思います。

Low-Level C#とは?

簡単に説明すると、コンパイル時に生成されるILコードが分からなくてもILコードをC#に変換すれば理解しやすくて便利だよね。という機能です。

調査環境

  • Unity : 2021.3.15f1 (Sillicon)
  • パフォーマンスの確認 : Unity Profilerを使いEditor上で確認
  • .NET Framework v4.7.1
  • C# 9.0

文字列

+演算子

内部ではstring.Concatに変換されていました。

C#
public sealed class TestClass : MonoBehaviour
{
    // [元のコード] 
    void Start()
    {
        var num = 1;
        var msg = "Number is " + num;
    }
    
    // [変換後] Low-Level C#
    private void Start()
    {
        string.Concat("Number is ", 1.ToString());
    }
}
コード全文
C#
public sealed class TestClass : MonoBehaviour
{
    void Start()
    {
        var num = 1;
        var msg = "Number is " + num;
    }
}
Low-Level C#
public sealed class TestClass : MonoBehaviour
{
  private void Start()
  {
    string.Concat("Number is ", 1.ToString());
  }

  public TestClass()
  {
    base..ctor();
  }
}
IL
.class public sealed auto ansi beforefieldinit
  TestClass
    extends [UnityEngine]UnityEngine.MonoBehaviour
{

  .method private hidebysig instance void
    Start() cil managed
  {
    .maxstack 2
    .locals init (
      [0] int32 num
    )

    // [13 9 - 13 21]
    IL_0000: ldc.i4.1
    IL_0001: stloc.0      // num

    // [14 9 - 14 38]
    IL_0002: ldstr        "Number is "
    IL_0007: ldloca.s     num
    IL_0009: call         instance string [mscorlib]System.Int32::ToString()
    IL_000e: call         string [mscorlib]System.String::Concat(string, string)
    IL_0013: pop

    // [15 5 - 15 6]
    IL_0014: ret

  } // end of method TestClass::Start

  .method public hidebysig specialname rtspecialname instance void
    .ctor() cil managed
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: call         instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
    IL_0006: ret

  } // end of method TestClass::.ctor
} // end of class TestClass

$ 文字列補間

内部ではstring.Formatに変換されていました。

C#
public sealed class TestClass : MonoBehaviour
{
    // [元のコード] 
    void Start()
    {
        var num = 1;
        var msg = $"Number is {num}";
    }
    
    // [変換後] Low-Level C#
    private void Start()
    {
        string.Format("Number is {0}", (object) 1);
    }
}
コード全文
C#
public sealed class TestClass : MonoBehaviour
{
    void Start()
    {
        var num = 1;
        var msg = $"Number is {num}";
    }
}
Low-Level C#
public sealed class TestClass : MonoBehaviour
{
  private void Start()
  {
    string.Format("Number is {0}", (object) 1);
  }

  public TestClass()
  {
    base..ctor();
  }
}
IL
.class public sealed auto ansi beforefieldinit
  TestClass
    extends [UnityEngine]UnityEngine.MonoBehaviour
{

  .method private hidebysig instance void
    Start() cil managed
  {
    .maxstack 2
    .locals init (
      [0] int32 num
    )

    // [13 9 - 13 21]
    IL_0000: ldc.i4.1
    IL_0001: stloc.0      // num

    // [14 9 - 14 38]
    IL_0002: ldstr        "Number is {0}"
    IL_0007: ldloc.0      // num
    IL_0008: box          [mscorlib]System.Int32
    IL_000d: call         string [mscorlib]System.String::Format(string, object)
    IL_0012: pop

    // [15 5 - 15 6]
    IL_0013: ret

  } // end of method TestClass::Start

  .method public hidebysig specialname rtspecialname instance void
    .ctor() cil managed
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: call         instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
    IL_0006: ret

  } // end of method TestClass::.ctor
} // end of class TestClass

気になったのでパフォーマンスチェック

+での連結と$での文字列補間で内部の実装が違うので、変数の埋め込みと文字列の連結をテストしてみました。

テストコード
C#
public sealed class StringTest : MonoBehaviour
{
    void Update()
    {
        Profiler.BeginSample("string.Format");
        for (var num = 1; num <= 100; num++)
        {
            string.Format("Number is {0}", num);
        }
        Profiler.EndSample();
        
        Profiler.BeginSample("string.Concat");
        for (var num = 1; num <= 100; num++)
        {
            string.Concat("Number is", num.ToString());
        }
        Profiler.EndSample();
        
        Profiler.BeginSample("[coupling] string.Format");
        {
            var stg = string.Empty;
            for (var i = 0; i < 100; i++)
            {
                stg = string.Format("{0}{1}", stg, "Number is 1");
            }
        }
        Profiler.EndSample();
        
        Profiler.BeginSample("[coupling] string.Concat");
        {
            var stg = string.Empty;
            for (var i = 0; i < 100; i++)
            {
                stg = string.Concat(stg, "Number is 1");
            }
        }
        Profiler.EndSample();
    }
}

テスト結果

変数の埋め込みテスト

GC Alloc Time ms
string.Format 6.5KB 0.28ms
string.Concat 6.8KB 0.19ms

文字列の連結テスト

GC Alloc Time ms
string.Format 340.7KB 0.73ms
string.Concat 110.6KB 0.13ms

結論

変数の埋め込み

パフォーマンス的にはstring.Concatが若干早いです。テスト結果から逸れますが、$ での文字列補間は、フォーマットが間違っていた場合コンパイルエラーとして検出してくれるのでフォーマットを使う場合は優先したいです。

文字列の連結

パフォーマンス的に、+で繋げた方が良さそうです。

文字列の参考

// これはダメ
var stg = string.Empty;
for (var i = 0; i < 100; i++)
{
    stg = $"{stg} Message";
}

// こっちの方が良い
var stg = string.Empty;
for (var i = 0; i < 100; i++)
{
    stg += " Message";
}

// フォーマットを使うなら安全性を優先してこんな感じで書きたい
var stg = string.Empty;
for (var num = 1; num <= 1000; num++)
{
    stg += $" Number is {num:#,0}";
}

// フォーマットを使わないならこれが最速
var stg = string.Empty;
for (var num = 1; num <= 1000; num++)
{
    stg += "Number is " + num;
}

今回は、2つの方法を比較したかったので除外しましたが、頻繁に文字列の連結が必要ならStringBuilderを検討した方が良いです。

暗黙的変換

Vector2の変数にVector3を代入するとVector2に自動的に変換してくれます。
内部では、Vector2に定義されたimplicit operatorを呼んでいるようです。

C#
public sealed class TestClass : MonoBehaviour
{
    Vector2 _pos;
    
    // [元のコード]
    void Start()
    {
        _pos = transform.position; // Vector3 to Vector2
    }
    
    // [変換後] Low-Level C#
    void Start()
    {
        this._pos = Vector2.op_Implicit(this.transform.position);
    }
}
コード全文
C#
public sealed class TestClass : MonoBehaviour
{
    Vector2 _pos;
    
    void Start()
    {
        _pos = transform.position;
    }
}
Low-Level C#
public sealed class TestClass : MonoBehaviour
{
  private Vector2 _pos;

  private void Start()
  {
    this._pos = Vector2.op_Implicit(this.transform.position);
  }

  public TestClass()
  {
    base..ctor();
  }
}
IL
.class public sealed auto ansi beforefieldinit
  TestClass
    extends [UnityEngine]UnityEngine.MonoBehaviour
{

  .field private valuetype [UnityEngine]UnityEngine.Vector2 _pos

  .method private hidebysig instance void
    Start() cil managed
  {
    .maxstack 8

    // [15 9 - 15 35]
    IL_0000: ldarg.0      // this
    IL_0001: ldarg.0      // this
    IL_0002: call         instance class [UnityEngine]UnityEngine.Transform [UnityEngine]UnityEngine.Component::get_transform()
    IL_0007: callvirt     instance valuetype [UnityEngine]UnityEngine.Vector3 [UnityEngine]UnityEngine.Transform::get_position()
    IL_000c: call         valuetype [UnityEngine]UnityEngine.Vector2 [UnityEngine]UnityEngine.Vector2::op_Implicit(valuetype [UnityEngine]UnityEngine.Vector3)
    IL_0011: stfld        valuetype [UnityEngine]UnityEngine.Vector2 TestClass::_pos

    // [16 5 - 16 6]
    IL_0016: ret

  } // end of method TestClass::Start

  .method public hidebysig specialname rtspecialname instance void
    .ctor() cil managed
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: call         instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
    IL_0006: ret

  } // end of method TestClass::.ctor
} // end of class TestClass

Vector2.op_Implicitの中身

implicit operatorのようです。内部を見てみるとnew Vector2を実行している事が分かりました。
newする度にほんの僅かですがコストがかかるので、例えば2Dゲームでもtransform.positionへの代入はVector3をメインに使った方が良さそうです。

C#
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static implicit operator Vector2(Vector3 v) => new Vector2(v.x, v.y);

Coroutine

Coroutineを使うと内部で自動生成されたIEnumeratorを実装したクラスを使用している事が分かります。これによりCoroutineを使う度にGC Allocが発生します。

C#
public sealed class TestClass : MonoBehaviour
{
    // [元のコード]
    void Start()
    {
        StartCoroutine(Process(1));
    }

    IEnumerator Process(float seconds)
    {
        yield return new WaitForSeconds(seconds);
        Debug.Log("Completed");
    }
    
    // [変換後] Low-Level C#
    [IteratorStateMachine(typeof (TestClass.<Process>d__1))]
    private IEnumerator Process(float seconds)
    {
        // 自動生成されたクラスをインスタンス化している
        TestClass.<Process>d__1 processD1 = new TestClass.<Process>d__1(0);
        processD1.seconds = seconds;
        return (IEnumerator) processD1;
    }
}
コード全文
C#
public sealed class TestClass : MonoBehaviour
{
    void Start()
    {
        StartCoroutine(Process(1));
    }

    IEnumerator Process(float seconds)
    {
        yield return new WaitForSeconds(seconds);
        Debug.Log("Completed");
    }
}
Low-Level C#
public sealed class TestClass : MonoBehaviour
{
  private void Start()
  {
    this.StartCoroutine(this.Process(1f));
  }

  [IteratorStateMachine(typeof (TestClass.<Process>d__1))]
  private IEnumerator Process(float seconds)
  {
    TestClass.<Process>d__1 processD1 = new TestClass.<Process>d__1(0);
    processD1.seconds = seconds;
    return (IEnumerator) processD1;
  }

  public TestClass()
  {
    base..ctor();
  }

  [CompilerGenerated]
  private sealed class <Process>d__1 : IEnumerator<object>, IEnumerator, IDisposable
  {
    private int <>1__state;
    private object <>2__current;
    public float seconds;

    [DebuggerHidden]
    public <Process>d__1(int _param1)
    {
      base..ctor();
      this.<>1__state = _param1;
    }

    [DebuggerHidden]
    void IDisposable.Dispose()
    {
    }

    bool IEnumerator.MoveNext()
    {
      int num = this.<>1__state;
      if (num != 0)
      {
        if (num != 1)
          return false;
        this.<>1__state = -1;
        Debug.Log((object) "Completed");
        return false;
      }
      this.<>1__state = -1;
      this.<>2__current = (object) new WaitForSeconds(this.seconds);
      this.<>1__state = 1;
      return true;
    }

    object IEnumerator<object>.Current
    {
      [DebuggerHidden] get
      {
        return this.<>2__current;
      }
    }

    [DebuggerHidden]
    void IEnumerator.Reset()
    {
      throw new NotSupportedException();
    }

    object IEnumerator.Current
    {
      [DebuggerHidden] get
      {
        return this.<>2__current;
      }
    }
  }
}
IL
.class public sealed auto ansi beforefieldinit
  TestClass
    extends [UnityEngine]UnityEngine.MonoBehaviour
{

  .class nested private sealed auto ansi beforefieldinit
    '<Process>d__1'
      extends [mscorlib]System.Object
      implements
        class [mscorlib]System.Collections.Generic.IEnumerator`1<object>,
        [mscorlib]System.Collections.IEnumerator,
        [mscorlib]System.IDisposable
  {
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )

    .field private int32 '<>1__state'

    .field private object '<>2__current'

    .field public float32 seconds

    .method public hidebysig specialname rtspecialname instance void
      .ctor(
        int32 '<>1__state'
      ) cil managed
    {
      .custom instance void [mscorlib]System.Diagnostics.DebuggerHiddenAttribute::.ctor()
        = (01 00 00 00 )
      .maxstack 8

      IL_0000: ldarg.0      // this
      IL_0001: call         instance void [mscorlib]System.Object::.ctor()
      IL_0006: ldarg.0      // this
      IL_0007: ldarg.1      // '<>1__state'
      IL_0008: stfld        int32 TestClass/'<Process>d__1'::'<>1__state'
      IL_000d: ret

    } // end of method '<Process>d__1'::.ctor

    .method private final hidebysig virtual newslot instance void
      System.IDisposable.Dispose() cil managed
    {
      .custom instance void [mscorlib]System.Diagnostics.DebuggerHiddenAttribute::.ctor()
        = (01 00 00 00 )
      .override method instance void [mscorlib]System.IDisposable::Dispose()
      .maxstack 8

      IL_0000: ret

    } // end of method '<Process>d__1'::System.IDisposable.Dispose

    .method private final hidebysig virtual newslot instance bool
      MoveNext() cil managed
    {
      .override method instance bool [mscorlib]System.Collections.IEnumerator::MoveNext()
      .maxstack 2
      .locals init (
        [0] int32 V_0
      )

      IL_0000: ldarg.0      // this
      IL_0001: ldfld        int32 TestClass/'<Process>d__1'::'<>1__state'
      IL_0006: stloc.0      // V_0
      IL_0007: ldloc.0      // V_0
      IL_0008: brfalse.s    IL_0010
      IL_000a: ldloc.0      // V_0
      IL_000b: ldc.i4.1
      IL_000c: beq.s        IL_0031
      IL_000e: ldc.i4.0
      IL_000f: ret
      IL_0010: ldarg.0      // this
      IL_0011: ldc.i4.m1
      IL_0012: stfld        int32 TestClass/'<Process>d__1'::'<>1__state'

      // [18 9 - 18 50]
      IL_0017: ldarg.0      // this
      IL_0018: ldarg.0      // this
      IL_0019: ldfld        float32 TestClass/'<Process>d__1'::seconds
      IL_001e: newobj       instance void [UnityEngine]UnityEngine.WaitForSeconds::.ctor(float32)
      IL_0023: stfld        object TestClass/'<Process>d__1'::'<>2__current'
      IL_0028: ldarg.0      // this
      IL_0029: ldc.i4.1
      IL_002a: stfld        int32 TestClass/'<Process>d__1'::'<>1__state'
      IL_002f: ldc.i4.1
      IL_0030: ret

      IL_0031: ldarg.0      // this
      IL_0032: ldc.i4.m1
      IL_0033: stfld        int32 TestClass/'<Process>d__1'::'<>1__state'

      // [19 9 - 19 32]
      IL_0038: ldstr        "Completed"
      IL_003d: call         void [UnityEngine]UnityEngine.Debug::Log(object)

      // [20 5 - 20 6]
      IL_0042: ldc.i4.0
      IL_0043: ret

    } // end of method '<Process>d__1'::MoveNext

    .method private final hidebysig virtual newslot specialname instance object
      'System.Collections.Generic.IEnumerator<System.Object>.get_Current'() cil managed
    {
      .custom instance void [mscorlib]System.Diagnostics.DebuggerHiddenAttribute::.ctor()
        = (01 00 00 00 )
      .override method instance !0/*T*/ class [mscorlib]System.Collections.Generic.IEnumerator`1<object>::get_Current()
      .maxstack 8

      IL_0000: ldarg.0      // this
      IL_0001: ldfld        object TestClass/'<Process>d__1'::'<>2__current'
      IL_0006: ret

    } // end of method '<Process>d__1'::'System.Collections.Generic.IEnumerator<System.Object>.get_Current'

    .method private final hidebysig virtual newslot instance void
      System.Collections.IEnumerator.Reset() cil managed
    {
      .custom instance void [mscorlib]System.Diagnostics.DebuggerHiddenAttribute::.ctor()
        = (01 00 00 00 )
      .override method instance void [mscorlib]System.Collections.IEnumerator::Reset()
      .maxstack 8

      IL_0000: newobj       instance void [mscorlib]System.NotSupportedException::.ctor()
      IL_0005: throw

    } // end of method '<Process>d__1'::System.Collections.IEnumerator.Reset

    .method private final hidebysig virtual newslot specialname instance object
      System.Collections.IEnumerator.get_Current() cil managed
    {
      .custom instance void [mscorlib]System.Diagnostics.DebuggerHiddenAttribute::.ctor()
        = (01 00 00 00 )
      .override method instance object [mscorlib]System.Collections.IEnumerator::get_Current()
      .maxstack 8

      IL_0000: ldarg.0      // this
      IL_0001: ldfld        object TestClass/'<Process>d__1'::'<>2__current'
      IL_0006: ret

    } // end of method '<Process>d__1'::System.Collections.IEnumerator.get_Current

    .property instance object 'System.Collections.Generic.IEnumerator<System.Object>.Current'()
    {
      .get instance object TestClass/'<Process>d__1'::'System.Collections.Generic.IEnumerator<System.Object>.get_Current'()
    } // end of property '<Process>d__1'::'System.Collections.Generic.IEnumerator<System.Object>.Current'

    .property instance object 'System.Collections.IEnumerator.Current'()
    {
      .get instance object TestClass/'<Process>d__1'::System.Collections.IEnumerator.get_Current()
    } // end of property '<Process>d__1'::System.Collections.IEnumerator.Current
  } // end of class '<Process>d__1'

  .method private hidebysig instance void
    Start() cil managed
  {
    .maxstack 8

    // [13 9 - 13 36]
    IL_0000: ldarg.0      // this
    IL_0001: ldarg.0      // this
    IL_0002: ldc.r4       1
    IL_0007: call         instance class [mscorlib]System.Collections.IEnumerator TestClass::Process(float32)
    IL_000c: call         instance class [UnityEngine]UnityEngine.Coroutine [UnityEngine]UnityEngine.MonoBehaviour::StartCoroutine(class [mscorlib]System.Collections.IEnumerator)
    IL_0011: pop

    // [14 5 - 14 6]
    IL_0012: ret

  } // end of method TestClass::Start

  .method private hidebysig instance class [mscorlib]System.Collections.IEnumerator
    Process(
      float32 seconds
    ) cil managed
  {
    .custom instance void [mscorlib]System.Runtime.CompilerServices.IteratorStateMachineAttribute::.ctor(class [mscorlib]System.Type)
      = (
        01 00 17 54 65 73 74 43 6c 61 73 73 2b 3c 50 72 // ...TestClass+<Pr
        6f 63 65 73 73 3e 64 5f 5f 31 00 00             // ocess>d__1..
      )
      // type(class TestClass/'<Process>d__1')
    .maxstack 8

    IL_0000: ldc.i4.0
    IL_0001: newobj       instance void TestClass/'<Process>d__1'::.ctor(int32)
    IL_0006: dup
    IL_0007: ldarg.1      // seconds
    IL_0008: stfld        float32 TestClass/'<Process>d__1'::seconds
    IL_000d: ret

  } // end of method TestClass::Process

  .method public hidebysig specialname rtspecialname instance void
    .ctor() cil managed
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: call         instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
    IL_0006: ret

  } // end of method TestClass::.ctor
} // end of class TestClass

CoroutineのGC最適化

GC Allocの発生を抑えるコードを書いてみましたが、実用的では無いですし0 Allocで書けなかったので残念です。掲載するか迷いましたが、今後の自分の為に掲載します。

テスト結果

※ 下記の結果は2回目以降の数値です。

GC Alloc Time ms
IEnumerator 84KB 0.01ms
WaitForSecondsEnumerator 24KB 0.00ms
コードを見る
C#
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;

public sealed class StringTest : MonoBehaviour
{
    WaitForSecondsEnumerator _waitForseconds = new WaitForSecondsEnumerator();
    
    void Update()
    {
        Profiler.BeginSample("Process");
        StartCoroutine(Process(1));
        Profiler.EndSample();
        
        Profiler.BeginSample("Process fast");
        StartCoroutine(Process2(1));
        Profiler.EndSample();
    }
    
    IEnumerator Process(float seconds)
    {
        yield return new WaitForSeconds(seconds);
        Debug.Log("Completed");
    }
    
    IEnumerator Process2(float seconds)
    {
        return _waitForseconds.Start(seconds, () =>
        {
            Debug.Log("Completed");
        });
    }

    class WaitForSecondsEnumerator : IEnumerator<object>
    {
        readonly IEnumerator _enumerator;
        int _state;
        float _seconds;
        object _waitForSeconds;
        Action _completedAction;

        public WaitForSecondsEnumerator() => _enumerator = this;

        public IEnumerator Start(float seconds, Action completedAction)
        {
            _completedAction = completedAction;
            _state = 0;
            if (Math.Abs(_seconds - seconds) > 0)
            {
                _seconds = seconds;
                _waitForSeconds = new WaitForSeconds(seconds);
            }
            return _enumerator;
        }

        bool IEnumerator.MoveNext()
        {
            if (_state != 0)
            {
                if (_state != 1)
                {
                    return false;
                }
                _state = -1;
                _completedAction?.Invoke();
                return false;
            }
            _state = 1;
            return true;
        }
        
        object IEnumerator<object>.Current => _waitForSeconds;
        object IEnumerator.Current => _waitForSeconds;
        void IEnumerator.Reset() => throw new NotSupportedException();
        void IDisposable.Dispose() {}
    }
}

ボックス化

値型を参照型であるobject型にキャストすると値型から参照型へ変換するボックス化が発生します。
身近なところで言うとDebug.Logに数値などの値型を入れるとボックス化が発生します。それだけでは無く重い処理の為なるべくDebug.Logはコードに残さない方が良いです。

C#
public sealed class TestClass : MonoBehaviour
{
    // [元のコード]
    void Start()
    {
        Debug.Log(1);
    }
    
    // [変換後] Low-Level C#
    void Start()
    {
        // object型にキャストされている
        Debug.Log((object) 1);
    }
}
コード全文
C#
public sealed class TestClass : MonoBehaviour
{
    void Start()
    {
        Debug.Log(1);
    }
}
Low-Level C#
public sealed class TestClass : MonoBehaviour
{
  private void Start()
  {
    Debug.Log((object) 1);
  }

  public TestClass()
  {
    base..ctor();
  }
}
IL
.class public sealed auto ansi beforefieldinit
  TestClass
    extends [UnityEngine]UnityEngine.MonoBehaviour
{

  .method private hidebysig instance void
    Start() cil managed
  {
    .maxstack 8

    // [13 9 - 13 22]
    IL_0000: ldc.i4.1
    IL_0001: box          [mscorlib]System.Int32
    IL_0006: call         void [UnityEngine]UnityEngine.Debug::Log(object)

    // [14 5 - 14 6]
    IL_000b: ret

  } // end of method TestClass::Start

  .method public hidebysig specialname rtspecialname instance void
    .ctor() cil managed
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: call         instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
    IL_0006: ret

  } // end of method TestClass::.ctor
} // end of class TestClass

Debug.Logを残したい

値の確認やエラーチェックなど、何らかの理由でDebug.Logを残したい事もあると思います。その場合、Development Buildのみ有効にするのをおすすめします。下記はすべてDevelopment Buildのみで機能します。

C#
using System.Diagnostics;
using UnityEngine;
using UnityEngine.Assertions;

public sealed class TestClass : MonoBehaviour
{
    [SerializeField] GameObject _obj;
    
    void Start()
    {
        // 1) 問題があればメッセージを表示したい場合は、Assertを使う
        // _objがnullの場合、エラーメッセージ表示
        Assert.IsNotNull(_obj, "InspectorからObjが設定されていません");
        
        // 2) Conditional属性を付与したメソッドを自作する
        PrintLog("Message");

        // 3) ディレクティブを使う
#if DEBUG
        Debug.Log("Message");
#endif
    }
    
    [Conditional("DEBUG")]
    void PrintLog(string message) => Debug.Log(message);
}

ラムダ式

ローカル変数をラムダ式内で使うと、GCゴミが発生します。
理由は内部でクラスが自動生成されていて、そのクラスを利用する為に発生します。

C#
public sealed class TestClass : MonoBehaviour
{
    // [元のコード]
    void Start()
    {
        var msg = "Completed";
        Do(result =>
        {
            Debug.Log(result + " " +  msg);
        });
    }

    // [変換後] Low-Level C#
    private void Start()
      {
        // 自動実装されたクラスがインスタンス化されている
        TestClass.<>c__DisplayClass0_0 cDisplayClass00 = new TestClass.<>c__DisplayClass0_0();
        cDisplayClass00.msg = "Completed";
        this.Do(new Action<string>((object) cDisplayClass00, __methodptr(<Start>b__0)));
      }

    void Do(Action<string> completedAction)
    {
        completedAction("Result");
    }
}
コード全文
C#
public sealed class TestClass : MonoBehaviour
{
    void Start()
    {
        var msg = "Completed";
        Do(result =>
        {
            Debug.Log(result + " " +  msg);
        });
    }

    void Do(Action<string> completedAction)
    {
        completedAction("Result");
    }
}
Low-Level C#
public sealed class TestClass : MonoBehaviour
{
  private void Start()
  {
    TestClass.<>c__DisplayClass0_0 cDisplayClass00 = new TestClass.<>c__DisplayClass0_0();
    cDisplayClass00.msg = "Completed";
    this.Do(new Action<string>((object) cDisplayClass00, __methodptr(<Start>b__0)));
  }

  private void Do(Action<string> completedAction)
  {
    completedAction("Result");
  }

  public TestClass()
  {
    base..ctor();
  }

  [CompilerGenerated]
  private sealed class <>c__DisplayClass0_0
  {
    public string msg;

    public <>c__DisplayClass0_0()
    {
      base..ctor();
    }

    internal void <Start>b__0(string result)
    {
      Debug.Log((object) string.Concat(result, " ", this.msg));
    }
  }
}
IL
.class public sealed auto ansi beforefieldinit
  TestClass
    extends [UnityEngine]UnityEngine.MonoBehaviour
{

  .class nested private sealed auto ansi beforefieldinit
    '<>c__DisplayClass0_0'
      extends [mscorlib]System.Object
  {
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )

    .field public string msg

    .method public hidebysig specialname rtspecialname instance void
      .ctor() cil managed
    {
      .maxstack 8

      IL_0000: ldarg.0      // this
      IL_0001: call         instance void [mscorlib]System.Object::.ctor()
      IL_0006: ret

    } // end of method '<>c__DisplayClass0_0'::.ctor

    .method assembly hidebysig instance void
      '<Start>b__0'(
        string result
      ) cil managed
    {
      .maxstack 8

      // [16 13 - 16 44]
      IL_0000: ldarg.1      // result
      IL_0001: ldstr        " "
      IL_0006: ldarg.0      // this
      IL_0007: ldfld        string TestClass/'<>c__DisplayClass0_0'::msg
      IL_000c: call         string [mscorlib]System.String::Concat(string, string, string)
      IL_0011: call         void [UnityEngine]UnityEngine.Debug::Log(object)

      // [17 9 - 17 10]
      IL_0016: ret

    } // end of method '<>c__DisplayClass0_0'::'<Start>b__0'
  } // end of class '<>c__DisplayClass0_0'

  .method private hidebysig instance void
    Start() cil managed
  {
    .maxstack 3
    .locals init (
      [0] class TestClass/'<>c__DisplayClass0_0' 'CS$<>8__locals0'
    )

    IL_0000: newobj       instance void TestClass/'<>c__DisplayClass0_0'::.ctor()
    IL_0005: stloc.0      // 'CS$<>8__locals0'

    // [13 9 - 13 31]
    IL_0006: ldloc.0      // 'CS$<>8__locals0'
    IL_0007: ldstr        "Completed"
    IL_000c: stfld        string TestClass/'<>c__DisplayClass0_0'::msg

    // [14 9 - 17 12]
    IL_0011: ldarg.0      // this
    IL_0012: ldloc.0      // 'CS$<>8__locals0'
    IL_0013: ldftn        instance void TestClass/'<>c__DisplayClass0_0'::'<Start>b__0'(string)
    IL_0019: newobj       instance void class [mscorlib]System.Action`1<string>::.ctor(object, native int)
    IL_001e: call         instance void TestClass::Do(class [mscorlib]System.Action`1<string>)

    // [18 5 - 18 6]
    IL_0023: ret

  } // end of method TestClass::Start

  .method private hidebysig instance void
    Do(
      class [mscorlib]System.Action`1<string> completedAction
    ) cil managed
  {
    .maxstack 8

    // [22 9 - 22 35]
    IL_0000: ldarg.1      // completedAction
    IL_0001: ldstr        "Result"
    IL_0006: callvirt     instance void class [mscorlib]System.Action`1<string>::Invoke(!0/*string*/)

    // [23 5 - 23 6]
    IL_000b: ret

  } // end of method TestClass::Do

  .method public hidebysig specialname rtspecialname instance void
    .ctor() cil managed
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: call         instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
    IL_0006: ret

  } // end of method TestClass::.ctor
} // end of class TestClass

ラムダ式内にローカル変数を移動できるのであれば、自動生成されたクラスがキャッシュされるので2回目以降使い回してくれます。

C#
public sealed class TestClass : MonoBehaviour
{
    // [元のコード]
    void Start()
    {
        Do(result =>
        {
            var msg = "Completed";
            Debug.Log($"{result} {msg}");
        });
    }

    // [変換後] Low-Level C#
    private void Start()
    {
        // TestClass.<>c.<>9__0_0変数にキャッシュされる
        this.Do(TestClass.<>c.<>9__0_0 ?? (TestClass.<>c.<>9__0_0 = new Action<string>((object) TestClass.<>c.<>9, __methodptr(<Start>b__0_0))));
    }

    void Do(Action<string> completedAction)
    {
        completedAction("Result");
    }
}
コード全文
C#
public sealed class TestClass : MonoBehaviour
{
    void Start()
    {
        Do(result =>
        {
            var msg = "Completed";
            Debug.Log($"{result} {msg}");
        });
    }
    
    void Do(Action<string> completedAction)
    {
        completedAction("Result");
    }
}
Low-Level C#
public sealed class TestClass : MonoBehaviour
{
  private void Start()
  {
    this.Do(TestClass.<>c.<>9__0_0 ?? (TestClass.<>c.<>9__0_0 = new Action<string>((object) TestClass.<>c.<>9, __methodptr(<Start>b__0_0))));
  }

  private void Do(Action<string> completedAction)
  {
    completedAction("Result");
  }

  public TestClass()
  {
    base..ctor();
  }

  [CompilerGenerated]
  [Serializable]
  private sealed class <>c
  {
    public static readonly TestClass.<>c <>9;
    public static Action<string> <>9__0_0;

    static <>c()
    {
      TestClass.<>c.<>9 = new TestClass.<>c();
    }

    public <>c()
    {
      base..ctor();
    }

    internal void <Start>b__0_0(string result)
    {
      string str2 = "Completed";
      Debug.Log((object) string.Concat(result, " ", str2));
    }
  }
}
IL
.class public sealed auto ansi beforefieldinit
  TestClass
    extends [UnityEngine]UnityEngine.MonoBehaviour
{

  .class nested private sealed auto ansi serializable beforefieldinit
    '<>c'
      extends [mscorlib]System.Object
  {
    .custom instance void [mscorlib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
      = (01 00 00 00 )

    .field public static initonly class TestClass/'<>c' '<>9'

    .field public static class [mscorlib]System.Action`1<string> '<>9__0_0'

    .method private hidebysig static specialname rtspecialname void
      .cctor() cil managed
    {
      .maxstack 8

      IL_0000: newobj       instance void TestClass/'<>c'::.ctor()
      IL_0005: stsfld       class TestClass/'<>c' TestClass/'<>c'::'<>9'
      IL_000a: ret

    } // end of method '<>c'::.cctor

    .method public hidebysig specialname rtspecialname instance void
      .ctor() cil managed
    {
      .maxstack 8

      IL_0000: ldarg.0      // this
      IL_0001: call         instance void [mscorlib]System.Object::.ctor()
      IL_0006: ret

    } // end of method '<>c'::.ctor

    .method assembly hidebysig instance void
      '<Start>b__0_0'(
        string result
      ) cil managed
    {
      .maxstack 3
      .locals init (
        [0] string msg
      )

      // [15 13 - 15 35]
      IL_0000: ldstr        "Completed"
      IL_0005: stloc.0      // msg

      // [16 13 - 16 42]
      IL_0006: ldarg.1      // result
      IL_0007: ldstr        " "
      IL_000c: ldloc.0      // msg
      IL_000d: call         string [mscorlib]System.String::Concat(string, string, string)
      IL_0012: call         void [UnityEngine]UnityEngine.Debug::Log(object)

      // [17 9 - 17 10]
      IL_0017: ret

    } // end of method '<>c'::'<Start>b__0_0'
  } // end of class '<>c'

  .method private hidebysig instance void
    Start() cil managed
  {
    .maxstack 8

    // [13 9 - 17 12]
    IL_0000: ldarg.0      // this
    IL_0001: ldsfld       class [mscorlib]System.Action`1<string> TestClass/'<>c'::'<>9__0_0'
    IL_0006: dup
    IL_0007: brtrue.s     IL_0020
    IL_0009: pop
    IL_000a: ldsfld       class TestClass/'<>c' TestClass/'<>c'::'<>9'
    IL_000f: ldftn        instance void TestClass/'<>c'::'<Start>b__0_0'(string)
    IL_0015: newobj       instance void class [mscorlib]System.Action`1<string>::.ctor(object, native int)
    IL_001a: dup
    IL_001b: stsfld       class [mscorlib]System.Action`1<string> TestClass/'<>c'::'<>9__0_0'
    IL_0020: call         instance void TestClass::Do(class [mscorlib]System.Action`1<string>)

    // [18 5 - 18 6]
    IL_0025: ret

  } // end of method TestClass::Start

  .method private hidebysig instance void
    Do(
      class [mscorlib]System.Action`1<string> completedAction
    ) cil managed
  {
    .maxstack 8

    // [22 9 - 22 35]
    IL_0000: ldarg.1      // completedAction
    IL_0001: ldstr        "Result"
    IL_0006: callvirt     instance void class [mscorlib]System.Action`1<string>::Invoke(!0/*string*/)

    // [23 5 - 23 6]
    IL_000b: ret

  } // end of method TestClass::Do

  .method public hidebysig specialname rtspecialname instance void
    .ctor() cil managed
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: call         instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
    IL_0006: ret

  } // end of method TestClass::.ctor
} // end of class TestClass

結論

ローカル変数はできる限りラムダ式内に書くのがパフォーマンスが良い。

C#
void Start()
{
    var msg = "Completed"; // x
    Do(result =>
    {
        // ○ ラムダ式内に書いた方がパフォーマンスが良い
        var msg = "Completed";
        Debug.Log($"{result} {msg}");
    });
}

タプル

タプルは内部でValueTupleに変換してくれるようです。
引数が少ないメソッドやシグネイチャの変更が発生しても問題無い場合は、積極的に使っていきたいです。ValueTupleは値型なのでモバイルでパフォーマンスを優先したい箇所でも使えそうです。

C#
public sealed class TestClass : MonoBehaviour
{
    void Start()
    {
        var a = Get();
    }

    // [元のコード]
    (int Number, string String) Get()
    {
        return (1, "1");
    }
    
    // [変換後] Low-Level C#
    [return: TupleElementNames(new string[] {"Number", "String"})]
  private ValueTuple<int, string> Get()
    {
        return new ValueTuple<int, string>(1, "1");
    }
}
コード全文
C#
public sealed class TestClass : MonoBehaviour
{
    void Start()
    {
        var a = Get();
    }

    (int Number, string String) Get()
    {
        return (1, "1");
    }
}
Low-Level C#
public sealed class TestClass : MonoBehaviour
{
  private void Start()
  {
    this.Get();
  }

  [return: TupleElementNames(new string[] {"Number", "String"})]
  private ValueTuple<int, string> Get()
  {
    return new ValueTuple<int, string>(1, "1");
  }

  public TestClass()
  {
    base..ctor();
  }
}
IL
.class public sealed auto ansi beforefieldinit
  TestClass
    extends [UnityEngine]UnityEngine.MonoBehaviour
{

  .method private hidebysig instance void
    Start() cil managed
  {
    .maxstack 8

    // [13 9 - 13 23]
    IL_0000: ldarg.0      // this
    IL_0001: call         instance valuetype [mscorlib]System.ValueTuple`2<int32, string> TestClass::Get()
    IL_0006: pop

    // [14 5 - 14 6]
    IL_0007: ret

  } // end of method TestClass::Start

  .method private hidebysig instance valuetype [mscorlib]System.ValueTuple`2<int32, string>
    Get() cil managed
  {
    .param [0]
      .custom instance void [mscorlib]System.Runtime.CompilerServices.TupleElementNamesAttribute::.ctor(string[])
        = (
          01 00 02 00 00 00 06 4e 75 6d 62 65 72 06 53 74 // .......Number.St
          72 69 6e 67 00 00                               // ring..
        )
        // string[2]
          /*( string('Number') string('String') )*/
    .maxstack 8

    // [18 9 - 18 25]
    IL_0000: ldc.i4.1
    IL_0001: ldstr        "1"
    IL_0006: newobj       instance void valuetype [mscorlib]System.ValueTuple`2<int32, string>::.ctor(!0/*int32*/, !1/*string*/)
    IL_000b: ret

  } // end of method TestClass::Get

  .method public hidebysig specialname rtspecialname instance void
    .ctor() cil managed
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: call         instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
    IL_0006: ret

  } // end of method TestClass::.ctor
} // end of class TestClass

Enum

Enum++でインクリメントできます。中身を確認するとintに置き換わっている事が分かりました。

C#
public sealed class TestClass : MonoBehaviour
{
    enum Enum {E1, E2, E3}
    
    // [元のコード]
    void Start()
    {
        var key = Enum.E1;
        key++;
    }
    
    // [変換後] Low-Level C#
    private void Start()
    {
        // ただのintの足し算に変換されている
        int num = 0 + 1;
    }
}

そして、数値が振られていない場合、自動的に0から始まる連番で初期化されるようです。
下記のようなイメージです。

C#
// 元のコード
enum Enum {E1, E2, E3}
// ↓ コンパイル後、自動的に連番が振られる
enum Enum {E1 = 0, E2 = 1, E3 = 2}

内部を知れば下記の意味が理解できます。

"列挙型" は、基になる整数値型の一連の名前付き定数によって定義された値の型です。
(Microsoft リファレンスより)

コード全文
C#
public sealed class TestClass : MonoBehaviour
{
    enum Enum {E1, E2, E3}
    
    void Start()
    {
        var key = Enum.E1;
        key++;
    }
}

※ 自動的に整数が振られるのはLow-Level C#では確認出来なかったのでILコードを確認ください。

Low-Level C#
public sealed class TestClass : MonoBehaviour
{
  private void Start()
  {
    int num = 0 + 1;
  }

  public TestClass()
  {
    base..ctor();
  }

  private enum Enum {E1, E2, E3}
}
IL
.class public sealed auto ansi beforefieldinit
  TestClass
    extends [UnityEngine]UnityEngine.MonoBehaviour
{

  .class nested private sealed auto ansi
    Enum
      extends [mscorlib]System.Enum
  {

    .field public specialname rtspecialname int32 value__

    .field public static literal valuetype TestClass/Enum E1 = int32(0) // 0x00000000

    .field public static literal valuetype TestClass/Enum E2 = int32(1) // 0x00000001

    .field public static literal valuetype TestClass/Enum E3 = int32(2) // 0x00000002
  } // end of class Enum

  .method private hidebysig instance void
    Start() cil managed
  {
    .maxstack 8

    // [15 9 - 15 27]
    IL_0000: ldc.i4.0

    // [16 9 - 16 15]
    IL_0001: ldc.i4.1
    IL_0002: add
    IL_0003: pop

    // [17 5 - 17 6]
    IL_0004: ret

  } // end of method TestClass::Start

  .method public hidebysig specialname rtspecialname instance void
    .ctor() cil managed
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: call         instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
    IL_0006: ret

  } // end of method TestClass::.ctor
} // end of class TestClass

Enumをシリアライズして使う場合、正しく値を変更しないとおかしくなります。

変更前

public sealed class EnumTest : MonoBehaviour
{
    [SerializeField] Enum _enum;
    
    public enum Enum {E1, E2, E3}
}

変更後

E1の前にE4を追加すると、選択されている値がE4に変わってしまいます。シリアライズするとenumの名称は無視され値の整数しか見ない為にこの問題が発生します。

public sealed class EnumTest : MonoBehaviour
{
    [SerializeField] Enum _enum;
    
    public enum Enum {E4, E1, E2, E3}
}

どうしたら良いか?

下記のように定義するのがおすすめです。自動で振られる連番をあえて自分で振って、強い意志をアピールするのが効果的です。

/// <summary>
/// アイテムの状態
/// ※ 数値の変更禁止
/// </summary>
public enum ItemState
{
    Unknown = 0,
    Locked = 1,
    Unlocked = 2
}
  • セーブデータとしてenumを保存する際も必ずシリアライズが必要になり同じ問題が発生するので注意が必要です。
  • 個人的には外部に公開する場合や、インスペクタに表示する場合は整数を振るようにしてます。

GameObjectnullチェック

Unityの==演算子は特殊なので内部でどうなっているのか調べてみました。

==でnullチェック

内部的には、Object.op_Equalityで比較している事が分かりました。

C#
public sealed class TestClass : MonoBehaviour
{
    GameObject _obj;
    
    // [元のコード]
    void Start()
    {
        if (_obj == null)
        {
            _obj = GetComponentInChildren<Transform>().gameObject;
        }
    }
    
    // [変換後] Low-Level C#
    void Start()
    {
        if (!Object.op_Equality((Object) this._obj, (Object) null))
          return;
        this._obj = this.GetComponentInChildren<Transform>().gameObject;
    }
}
コード全文
C#
public sealed class TestClass : MonoBehaviour
{
    GameObject _obj;
    
    void Start()
    {
        if (_obj == null)
        {
            _obj = GetComponentInChildren<Transform>().gameObject;
        }
    }
}
Low-Level C#
public sealed class TestClass : MonoBehaviour
{
  private GameObject _obj;

  private void Start()
  {
    if (!Object.op_Equality((Object) this._obj, (Object) null))
      return;
    this._obj = this.GetComponentInChildren<Transform>().gameObject;
  }

  public TestClass()
  {
    base..ctor();
  }
}
IL
.class public sealed auto ansi beforefieldinit
  TestClass
    extends [UnityEngine]UnityEngine.MonoBehaviour
{

  .field private class [UnityEngine]UnityEngine.GameObject _obj

  .method private hidebysig instance void
    Start() cil managed
  {
    .maxstack 8

    // [15 9 - 15 26]
    IL_0000: ldarg.0      // this
    IL_0001: ldfld        class [UnityEngine]UnityEngine.GameObject TestClass::_obj
    IL_0006: ldnull
    IL_0007: call         bool [UnityEngine]UnityEngine.Object::op_Equality(class [UnityEngine]UnityEngine.Object, class [UnityEngine]UnityEngine.Object)
    IL_000c: brfalse.s    IL_001f

    // [17 13 - 17 67]
    IL_000e: ldarg.0      // this
    IL_000f: ldarg.0      // this
    IL_0010: call         instance !!0/*class [UnityEngine]UnityEngine.Transform*/ [UnityEngine]UnityEngine.Component::GetComponentInChildren<class [UnityEngine]UnityEngine.Transform>()
    IL_0015: callvirt     instance class [UnityEngine]UnityEngine.GameObject [UnityEngine]UnityEngine.Component::get_gameObject()
    IL_001a: stfld        class [UnityEngine]UnityEngine.GameObject TestClass::_obj

    // [19 5 - 19 6]
    IL_001f: ret

  } // end of method TestClass::Start

  .method public hidebysig specialname rtspecialname instance void
    .ctor() cil managed
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: call         instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
    IL_0006: ret

  } // end of method TestClass::.ctor
} // end of class TestClass

Object.op_Equalityの中身

operator ==のようです。 内部では、Object.IsNativeObjectAliveGameObjectがシーン上に存在するか確認する処理が実行されています。

C#
// Object.op_Equality
public static bool operator ==(Object x, Object y) => Object.CompareBaseObjects(x, y);

private static bool CompareBaseObjects(Object lhs, Object rhs)
{
      bool flag1 = (object) lhs == null;
      bool flag2 = (object) rhs == null;
      if (flag2 & flag1)
        return true;
      if (flag2)
        return !Object.IsNativeObjectAlive(lhs);
      return flag1 ? !Object.IsNativeObjectAlive(rhs) : lhs.m_InstanceID == rhs.m_InstanceID;
}

null合体演算子での確認

null合体演算子でチェックした場合、先ほどのObject.op_Equalityが使われていません。

C#
public sealed class TestClass : MonoBehaviour
{
    GameObject _obj;
    
    void Start()
    {
        _obj = _obj ?? GetComponentInChildren<Transform>().gameObject;
    }
    
    // [変換後] Low-Level C#
    void Start()
    {
        GameObject gameObject = this._obj;
        if (gameObject == null) // Object.op_Equalityで確認していない。
          gameObject = this.GetComponentInChildren<Transform>().gameObject;
        this._obj = gameObject;
    }
}
コード全文
C#
public sealed class TestClass : MonoBehaviour
{
    GameObject _obj;
    
    void Start()
    {
        _obj = _obj ?? GetComponentInChildren<Transform>().gameObject;
    }
}
Low-Level C#
public sealed class TestClass : MonoBehaviour
{
  private GameObject _obj;

  private void Start()
  {
    GameObject gameObject = this._obj;
    if (gameObject == null)
      gameObject = this.GetComponentInChildren<Transform>().gameObject;
    this._obj = gameObject;
  }

  public TestClass()
  {
    base..ctor();
  }
}
IL
.class public sealed auto ansi beforefieldinit
  TestClass
    extends [UnityEngine]UnityEngine.MonoBehaviour
{

  .field private class [UnityEngine]UnityEngine.GameObject _obj

  .method private hidebysig instance void
    Start() cil managed
  {
    .maxstack 8

    // [15 9 - 15 71]
    IL_0000: ldarg.0      // this
    IL_0001: ldarg.0      // this
    IL_0002: ldfld        class [UnityEngine]UnityEngine.GameObject TestClass::_obj
    IL_0007: dup
    IL_0008: brtrue.s     IL_0016
    IL_000a: pop
    IL_000b: ldarg.0      // this
    IL_000c: call         instance !!0/*class [UnityEngine]UnityEngine.Transform*/ [UnityEngine]UnityEngine.Component::GetComponentInChildren<class [UnityEngine]UnityEngine.Transform>()
    IL_0011: callvirt     instance class [UnityEngine]UnityEngine.GameObject [UnityEngine]UnityEngine.Component::get_gameObject()
    IL_0016: stfld        class [UnityEngine]UnityEngine.GameObject TestClass::_obj

    // [16 5 - 16 6]
    IL_001b: ret

  } // end of method TestClass::Start

  .method public hidebysig specialname rtspecialname instance void
    .ctor() cil managed
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: call         instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
    IL_0006: ret

  } // end of method TestClass::.ctor
} // end of class TestClass

結論

null合体演算子は、GameObjectがシーン上に存在するか確認していない と言う事です。オブジェクトが削除されていても結果がtrueになる可能性があります。UnityのGameObjectは必ず==nullチェックする必要があります。Null条件演算子も同じです。

Null条件演算子

C#
public sealed class TestClass : MonoBehaviour
{
    MyScript _obj; // MyScriptにはMonoBehaviourが継承されています。
    
    // [元のコード]
    void Start()
    {
        var child = _obj?.GetChild();
    }
    
    // [変換後] Low-Level C#
    private void Start()
    {
        MyScript myScript = this._obj;
        if (myScript == null) // Object.op_Equalityで確認していない。
          return;
        myScript.GetChild();
    }
}
コード全文
C#
public sealed class TestClass : MonoBehaviour
{
    MyScript _obj; // MyScriptにはMonoBehaviourが継承されています。
    
    void Start()
    {
        var child = _obj?.GetChild();
    }
}
Low-Level C#
public sealed class TestClass : MonoBehaviour
{
  private MyScript _obj;

  private void Start()
  {
    MyScript myScript = this._obj;
    if (myScript == null)
      return;
    myScript.GetChild();
  }

  public TestClass()
  {
    base..ctor();
  }
}
IL
.class public sealed auto ansi beforefieldinit
  TestClass
    extends [UnityEngine]UnityEngine.MonoBehaviour
{

  .field private class MyScript _obj

  .method private hidebysig instance void
    Start() cil managed
  {
    .maxstack 8

    // [15 9 - 15 38]
    IL_0000: ldarg.0      // this
    IL_0001: ldfld        class MyScript TestClass::_obj
    IL_0006: dup
    IL_0007: brtrue.s     IL_000b
    IL_0009: pop
    IL_000a: ret
    IL_000b: call         instance class [UnityEngine]UnityEngine.GameObject MyScript::GetChild()
    IL_0010: pop

    // [16 5 - 16 6]
    IL_0011: ret

  } // end of method TestClass::Start

  .method public hidebysig specialname rtspecialname instance void
    .ctor() cil managed
  {
    .maxstack 8

    IL_0000: ldarg.0      // this
    IL_0001: call         instance void [UnityEngine]UnityEngine.MonoBehaviour::.ctor()
    IL_0006: ret

  } // end of method TestClass::.ctor
} // end of class TestClass

分かり易いように馴染み深いGameObjectで話を進めましたが、UnityEngine.Objectを継承するComponentクラスはすべて==nullチェックする必要があります。

あとがき

ILコードを読むのはしんどいですが、Low-Level C#であればいつも通りに読めるので解析しやすかったです。あと、今まで知っていた事でも読めるコードにすると理解しやすくて良いですね。今後もパフォーマンスを追い込む必要がある箇所や効率の良い書き方の発掘などで使っていきたいと思います。

最後まで読んでいただきありがとうございました。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
7