C# のジェネリック引数の Null またはデフォルトの比較

StackOverflow https://stackoverflow.com/questions/65351

  •  09-06-2019
  •  | 
  •  

質問

次のように定義された汎用メソッドがあります。

public void MyMethod<T>(T myArgument)

まず最初に、myArgument の値がその型のデフォルト値であるかどうかを確認します。次のようになります。

if (myArgument == default(T))

しかし、T が == 演算子を実装することを保証していないため、これはコンパイルできません。そこで、コードを次のように切り替えました。

if (myArgument.Equals(default(T)))

これでコンパイルは完了しますが、myArgument が null の場合は失敗します。これはテスト対象の一部です。次のように明示的な null チェックを追加できます。

if (myArgument == null || myArgument.Equals(default(T)))

今、これは私には冗長に感じられます。ReSharper は、myArgument == null 部分を myArgument ==default(T) に変更することを提案しています。これが私が始めたところです。この問題を解決するより良い方法はありますか?

サポートする必要があります 両方 参照型と値型。

役に立ちましたか?

解決

ボックス化を避けるために、ジェネリックを同等かどうか比較する最良の方法は次のとおりです。 EqualityComparer<T>.Default. 。これは尊重します IEquatable<T> (ボクシングなし) object.Equals, 、すべてを処理します Nullable<T> 「持ち上げられた」というニュアンス。したがって、次のようになります。

if(EqualityComparer<T>.Default.Equals(obj, default(T))) {
    return obj;
}

これは以下と一致します:

  • クラスの場合は null
  • null (空) Nullable<T>
  • 他の構造体の場合は zero/false/etc

他のヒント

これはどう:

if (object.Equals(myArgument, default(T)))
{
    //...
}

の使用 static object.Equals() このメソッドを使用すると、次のことを行う必要がなくなります。 null 自分自身で調べて。呼び出しを明示的に修飾する object. コンテキストによってはおそらく必要ありませんが、通常は接頭辞を付けます static コードをよりわかりやすくするためだけに、型名を使用して呼び出します。

を見つけることができました Microsoft Connect の記事 この問題について詳しく説明しています。

残念ながら、この動作は仕様によるものであり、値の型を含む可能性のある型パラメーターでの使用を有効にする簡単な解決策はありません。

型が参照型であることがわかっている場合、型が独自のカスタム オーバーロードを指定する場合がありますが、オブジェクトで定義された既定のオーバーロードは参照の等価性について変数をテストします。コンパイラは、変数の静的型に基づいて、どのオーバーロードを使用するかを決定します (決定は多態的ではありません)。したがって、ジェネリック型パラメーター T を非シール参照型 (例外など) に制限するように例を変更すると、コンパイラーは使用する特定のオーバーロードを決定でき、次のコードがコンパイルされます。

public class Test<T> where T : Exception

型が値型であることがわかっている場合は、使用されている正確な型に基づいて特定の値の等価性テストを実行します。参照比較は値の型に対して意味がなく、コンパイラはどの特定の値比較を出力するかを認識できないため、ここでは適切な「デフォルト」比較はありません。コンパイラは ValueType.Equals(Object) への呼び出しを発行できますが、このメソッドはリフレクションを使用するため、特定の値の比較に比べて非常に非効率的です。したがって、T に値型制約を指定したとしても、コンパイラーがここで生成する合理的なものは何もありません。

public class Test<T> where T : struct

あなたが提示したケースでは、コンパイラーは T が値であるか参照型であるかさえ知りません。同様に、すべての可能な型に対して有効となる生成するものは何もありません。参照比較は値型に対しては無効であり、オーバーロードしない参照型に対してはある種の値比較は予期されません。

ここでできることは...

これらのメソッドは両方とも、参照型と値型の一般的な比較に機能することを検証しました。

object.Equals(param, default(T))

または

EqualityComparer<T>.Default.Equals(param, default(T))

「==」演算子を使用して比較を行うには、次のいずれかの方法を使用する必要があります。

T のすべてのケースが既知の基本クラスから派生している場合は、ジェネリック型の制限を使用してコンパイラーに知らせることができます。

public void MyMethod<T>(T myArgument) where T : MyBase

その後、コンパイラは操作を実行する方法を認識します。 MyBase また、現在表示されている「演算子 '==' は型 'T' および 'T' のオペランドに適用できません」というエラーはスローされません。

別のオプションは、T を実装する型に制限することです。 IComparable.

public void MyMethod<T>(T myArgument) where T : IComparable

そして、 CompareTo によって定義されたメソッド I同等のインターフェース.

これを試して:

if (EqualityComparer<T>.Default.Equals(myArgument, default(T)))

それはコンパイルされ、あなたが望むことを行うはずです。

(編集済み)

Marc Gravell が最良の答えを持っていますが、私はそれを実証するために考え出した簡単なコード スニペットを投稿したいと思いました。これを単純な C# コンソール アプリで実行するだけです。

public static class TypeHelper<T>
{
    public static bool IsDefault(T val)
    {
         return EqualityComparer<T>.Default.Equals(obj,default(T));
    }
}

static void Main(string[] args)
{
    // value type
    Console.WriteLine(TypeHelper<int>.IsDefault(1)); //False
    Console.WriteLine(TypeHelper<int>.IsDefault(0)); // True

    // reference type
    Console.WriteLine(TypeHelper<string>.IsDefault("test")); //False
    Console.WriteLine(TypeHelper<string>.IsDefault(null)); //True //True

    Console.ReadKey();
}

もう一つ:VS2008 を使用している人はこれを拡張メソッドとして試すことができますか?ここでは 2005 年にこだわっていますが、それが許可されるかどうか知りたいと思っています。


編集: これを拡張メソッドとして機能させる方法は次のとおりです。

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // value type
        Console.WriteLine(1.IsDefault());
        Console.WriteLine(0.IsDefault());

        // reference type
        Console.WriteLine("test".IsDefault());
        // null must be cast to a type
        Console.WriteLine(((String)null).IsDefault());
    }
}

// The type cannot be generic
public static class TypeHelper
{
    // I made the method generic instead
    public static bool IsDefault<T>(this T val)
    {
        return EqualityComparer<T>.Default.Equals(val, default(T));
    }
}

T がプリミティブ型である場合を含め、すべての型の T を処理するには、両方の比較方法でコンパイルする必要があります。

    T Get<T>(Func<T> createObject)
    {
        T obj = createObject();
        if (obj == null || obj.Equals(default(T)))
            return obj;

        // .. do a bunch of stuff
        return obj;
    }

ここで問題が発生します -

これを任意の型で機能させる場合、default(T) は参照型では常に null になり、値型では 0 (または 0 でいっぱいの構造体) になります。

ただし、これはおそらくあなたが望んでいる動作ではありません。これを一般的な方法で機能させたい場合は、おそらくリフレクションを使用して T の型をチェックし、参照型とは異なる値の型を処理する必要があります。

あるいは、これにインターフェース制約を設定することもでき、インターフェースはクラス/構造体のデフォルトと照合する方法を提供できます。

おそらくこのロジックを 2 つの部分に分割し、最初に null をチェックする必要があると思います。

public static bool IsNullOrEmpty<T>(T value)
{
    if (IsNull(value))
    {
        return true;
    }
    if (value is string)
    {
        return string.IsNullOrEmpty(value as string);
    }
    return value.Equals(default(T));
}

public static bool IsNull<T>(T value)
{
    if (value is ValueType)
    {
        return false;
    }
    return null == (object)value;
}

IsNull メソッドでは、ValueType オブジェクトは定義上 null にならないという事実に依存しているため、value がたまたま ValueType から派生したクラスである場合、それが null ではないことがすでにわかっています。一方、値型でない場合は、オブジェクトにキャストされた値を null と比較するだけです。オブジェクトへのキャストに直接進むことで、ValueType に対するチェックを回避することもできますが、それは値型がボックス化されることを意味します。これはヒープ上に新しいオブジェクトが作成されることを意味するため、おそらく避けたいことです。

IsNullOrEmpty メソッドでは、文字列の特殊なケースをチェックしています。他のすべての型については、値を比較しています (値はすでにわかっています) ない null) のデフォルト値に対して、すべての参照型は null であり、値型は通常何らかの形式のゼロ (整数の場合) です。

これらのメソッドを使用すると、次のコードは期待どおりに動作します。

class Program
{
    public class MyClass
    {
        public string MyString { get; set; }
    }

    static void Main()
    {
        int  i1 = 1;    Test("i1", i1); // False
        int  i2 = 0;    Test("i2", i2); // True
        int? i3 = 2;    Test("i3", i3); // False
        int? i4 = null; Test("i4", i4); // True

        Console.WriteLine();

        string s1 = "hello";      Test("s1", s1); // False
        string s2 = null;         Test("s2", s2); // True
        string s3 = string.Empty; Test("s3", s3); // True
        string s4 = "";           Test("s4", s4); // True

        Console.WriteLine();

        MyClass mc1 = new MyClass(); Test("mc1", mc1); // False
        MyClass mc2 = null;          Test("mc2", mc2); // True
    }

    public static void Test<T>(string fieldName, T field)
    {
        Console.WriteLine(fieldName + ": " + IsNullOrEmpty(field));
    }

    // public static bool IsNullOrEmpty<T>(T value) ...

    // public static bool IsNull<T>(T value) ...
}

私が使う:

public class MyClass<T>
{
  private bool IsNull() 
  {
    var nullable = Nullable.GetUnderlyingType(typeof(T)) != null;
    return nullable ? EqualityComparer<T>.Default.Equals(Value, default(T)) : false;
  }
}

これが要件で機能するかどうかはわかりませんが、次のように、T を IComparable などのインターフェイスを実装する Type に制約し、そのインターフェイス (IIRC が null をサポート/処理する) から ComparesTo() メソッドを使用することもできます。 :

public void MyMethod<T>(T myArgument) where T : IComparable
...
if (0 == myArgument.ComparesTo(default(T)))

おそらく、IEquitable などと同様に使用できる他のインターフェイスがあるでしょう。

@ilitirit:

public class Class<T> where T : IComparable
{
    public T Value { get; set; }
    public void MyMethod(T val)
    {
        if (Value == val)
            return;
    }
}

演算子「==」はタイプ「T」および「T」のオペランドには適用できません

上記で提案したように、明示的な null テストの後に Equals メソッドまたは object.Equals を呼び出すことなしにこれを行う方法は思いつきません。

System.Comparison を使用して解決策を考案することもできますが、実際にはコード行数が大幅に増加し、複雑さが大幅に増加します。

近くにいたと思います。

if (myArgument.Equals(default(T)))

これでコンパイルは完了しますが、次の場合は失敗します。 myArgument は null ですが、これはテスト対象の一部です。次のように明示的な null チェックを追加できます。

エレガントな Null-safe アプローチを実現するには、equals が呼び出されるオブジェクトを反転するだけです。

default(T).Equals(myArgument);
ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top