メソッドを呼び出すときに、nullチェックを実行することがよくあります。 誰かが別のメソッドでチェックアウトすると、コードはよりきれいに見え、次のようになります。
public void ThrowIfNull(object obj) { if(obj == null) { throw new ArgumentNullException(); } }
そして、このようなチェックで興味深いのは、ジェネリックを使用できるため、オブジェクト属性の大規模な使用が見られることです。 メソッドを汎用に置き換えて、パフォーマンスを比較してみましょう。
テストする前に、オブジェクト引数のもう1つの欠点があります。 値の型をnullにすることはできません(Nullable型はカウントしません)。 ThrowIfNull(5)などのメソッド呼び出しは無意味ですが、引数の型はオブジェクトであるため、コンパイラはメソッドの呼び出しを許可します。 私にとっては、これはコードの品質を低下させます。状況によっては、パフォーマンスよりもはるかに重要です。 この動作を取り除き、メソッドのシグネチャを改善するには、ジェネリックメソッドを2つに分割して、制約を示す必要があります。 問題は、Nullable制約を指定できないことですが、struct制約を使用してNullable引数を指定できます。
パフォーマンスのテストを開始し、 BenchmarkDotNetライブラリを使用します。 属性を添付して実行し、結果を確認します。
public class ObjectArgVsGenericArg { public string str = "some string"; public Nullable<int> num = 5; [MethodImpl(MethodImplOptions.NoInlining)] public void ThrowIfNullGenericArg<T>(T arg) where T : class { if (arg == null) { throw new ArgumentNullException(); } } [MethodImpl(MethodImplOptions.NoInlining)] public void ThrowIfNullGenericArg<T>(Nullable<T> arg) where T : struct { if(arg == null) { throw new ArgumentNullException(); } } [MethodImpl(MethodImplOptions.NoInlining)] public void ThrowIfNullObjectArg(object arg) { if(arg == null) { throw new ArgumentNullException(); } } [Benchmark] public void CallMethodWithObjArgString() { ThrowIfNullObjectArg(str); } [Benchmark] public void CallMethodWithObjArgNullableInt() { ThrowIfNullObjectArg(num); } [Benchmark] public void CallMethodWithGenericArgString() { ThrowIfNullGenericArg(str); } [Benchmark] public void CallMethodWithGenericArgNullableInt() { ThrowIfNullGenericArg(num); } } class Program { static void Main(string[] args) { var summary = BenchmarkRunner.Run<ObjectArgVsGenericArg>(); } }
方法 | 平均 | エラー | Stddev |
---|
CallMethodWithObjArgString | 1.784 ns | 0.0166 ns | 0.0138 ns |
CallMethodWithObjArgNullableInt | 124.335 ns | 0.2480 ns | 0.2320 ns |
CallMethodWithGenericArgString | 1.746 ns | 0.0290 ns | 0.0271 ns |
CallMethodWithGenericArgNullableInt | 2.158 ns | 0.0089 ns | 0.0083 ns |
私たちの一般的なnullable型は2000倍速く動作しました! そしてすべては、悪名高いパッケージング(ボクシング)によるものです。 CallMethodWithObjArgNullableIntを呼び出すと、nullable-intが「パック」されてヒープに配置されます。 パッケージングは非常に高価な操作であり、この方法ではパフォーマンスも低下します。 したがって、ジェネリックを使用すると、パッケージ化を回避できます。
したがって、次の理由により、ジェネリック引数はオブジェクトよりも優れています。
- パッケージングから節約
- 制限を使用するときにメソッドのシグネチャを改善できます
更新しました。 コメントを寄せてくれたzelyony habrayuzerに感謝します。 インラインのメソッドでは、より正確な測定のために、 MethodImpl(MethodImplOptions.NoInlining)属性が追加されました。