14

.NET 構造体のメンバーごとの等価性テストで使用されるアルゴリズムは何ですか? 独自のアルゴリズムの基礎として使用できるように、これを知りたいです。

DTO の論理的等価性をテストするために、(C# で) 任意のオブジェクトに対して再帰的なメンバーごとの等価性テストを作成しようとしています。これは、DTO が構造体である場合 (ValueType.Equals がほとんど正しいことを行うため) はかなり簡単ですが、常に適切であるとは限りません。また、プロパティではなく内容が比較されるように、IEnumerable オブジェクト (文字列ではありません) の比較をオーバーライドしたいと考えています。

これは、私が予想していたよりも難しいことが証明されています。どんなヒントでも大歓迎です。最も有用であることが証明された回答、または最も有用な情報へのリンクを提供する回答を受け入れます。

ありがとう。

4

5 に答える 5

15

デフォルトのメンバー単位の等価性はありませんが、基本値型 ( floatbyteなどdecimal) の場合、言語仕様ではビット単位の比較が要求されます。JIT オプティマイザは、これを適切なアセンブリ命令に最適化しますが、技術的には、この動作は Cmemcmp関数と同じです。

BCL の例

  • DateTimeInternalTickslong である内部メンバー フィールドを単純に比較します。
  • PointFのように X と Y を比較し(left.X == right.X) && (left.Y == right.Y)ます。
  • Decimal内部フィールドを比較しませんが、InternalImpl にフォールバックします。つまり、内部の表示できない .NET 部分にあります (ただし、SSCLI を確認できます)。
  • Rectangle各フィールド (x、y、幅、高さ) を明示的に比較します。
  • ModuleHandleそのEqualsオーバーライドを使用し、これを行うものは他にもたくさんあります。
  • SqlStringおよび他の SqlXXX 構造体はそのIComparable.Compare実装を使用します。
  • Guid_aは、このリストの中で最も奇妙なものです。これには、すべての内部フィールド ( to _k、すべての int ) が等しくないかどうかを比較し、等しくない場合に false を返す、if ステートメントの独自の短絡的な長いリストがあります。すべてが等しくない場合は、true を返します。

結論

このリストはかなり恣意的ですが、問題に光を当てることを願っています.利用可能なデフォルトのメソッドはなく、BCLでさえ、目的に応じて各構造体に異なるアプローチを使用しています. 肝心なのは、後の追加でEqualsオーバーライドまたはIcomparable.Compareを呼び出す頻度が高くなったようですが、それは問題を別のメソッドに移すだけです。

他の方法:

リフレクションを使用して各フィールドを通過できますが、これは非常に低速です。内部フィールドでビット単位の比較を行う単一の拡張メソッドまたは静的ヘルパーを作成することもできます。を使用StructLayout.Sequentialして、メモリ アドレスとサイズを取得し、メモリ ブロックを比較します。これにはアンセーフ コードが必要ですが、すばやく簡単に実行できます (そして少し汚れています)。

更新:言い換え、いくつかの実際の例を追加、新しい結論を追加


更新:メンバーごとの比較の実装

上記は質問の誤解のようですが、将来の訪問者にとって何らかの価値があると思うので、そのままにしておきます。これがより重要な答えです:

これは、オブジェクトと値の型のメンバーごとの比較の実装であり、すべてのプロパティ、フィールド、および列挙可能なコンテンツを、深さに関係なく再帰的に調べることができます。テストされていません。おそらくいくつかのタイプミスが含まれていますが、問題なくコンパイルされます。詳細については、コード内のコメントを参照してください。

public static bool MemberCompare(object left, object right)
{
    if (Object.ReferenceEquals(left, right))
        return true;

    if (left == null || right == null)
        return false;

    Type type = left.GetType();
    if (type != right.GetType())
        return false;

    if(left as ValueType != null)
    {
        // do a field comparison, or use the override if Equals is implemented:
        return left.Equals(right);
    }

    // check for override:
    if (type != typeof(object)
        && type == type.GetMethod("Equals").DeclaringType)
    {
        // the Equals method is overridden, use it:
        return left.Equals(right);
    }

    // all Arrays, Lists, IEnumerable<> etc implement IEnumerable
    if (left as IEnumerable != null)
    {
        IEnumerator rightEnumerator = (right as IEnumerable).GetEnumerator();
        rightEnumerator.Reset();
        foreach (object leftItem in left as IEnumerable)
        {
            // unequal amount of items
            if (!rightEnumerator.MoveNext())
                return false;
            else
            {
                if (!MemberCompare(leftItem, rightEnumerator.Current))
                    return false;
            }                    
        }
    }
    else
    {
        // compare each property
        foreach (PropertyInfo info in type.GetProperties(
            BindingFlags.Public | 
            BindingFlags.NonPublic | 
            BindingFlags.Instance | 
            BindingFlags.GetProperty))
        {
            // TODO: need to special-case indexable properties
            if (!MemberCompare(info.GetValue(left, null), info.GetValue(right, null)))
                return false;
        }

        // compare each field
        foreach (FieldInfo info in type.GetFields(
            BindingFlags.GetField |
            BindingFlags.NonPublic |
            BindingFlags.Public |
            BindingFlags.Instance))
        {
            if (!MemberCompare(info.GetValue(left), info.GetValue(right)))
                return false;
        }
    }
    return true;
}

更新:いくつかのエラーを修正し、Equals利用可能な場合のみオーバーライドの使用を追加しました。
更新: object.Equalsオーバーライドと見なすべきではありません。修正されました。

于 2009-11-05T13:36:42.397 に答える
5

ValueType.Equalsこれは、Shared Source Common Language Infrastructure (バージョン 2.0)の実装です。

public override bool Equals (Object obj) {
    BCLDebug.Perf(false, "ValueType::Equals is not fast.  "+
        this.GetType().FullName+" should override Equals(Object)");
    if (null==obj) {
        return false;
    }
    RuntimeType thisType = (RuntimeType)this.GetType();
    RuntimeType thatType = (RuntimeType)obj.GetType();

    if (thatType!=thisType) {
        return false;
    }

    Object thisObj = (Object)this;
    Object thisResult, thatResult;

    // if there are no GC references in this object we can avoid reflection 
    // and do a fast memcmp
    if (CanCompareBits(this))
        return FastEqualsCheck(thisObj, obj);

    FieldInfo[] thisFields = thisType.GetFields(
        BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);

    for (int i=0; i<thisFields.Length; i++) {
        thisResult = ((RtFieldInfo)thisFields[i])
            .InternalGetValue(thisObj, false);
        thatResult = ((RtFieldInfo)thisFields[i])
            .InternalGetValue(obj, false);

        if (thisResult == null) {
            if (thatResult != null)
                return false;
        }
        else
        if (!thisResult.Equals(thatResult)) {
            return false;
        }
    }

    return true;
}

これは、Reflector で示されているコードとほとんど同じであることに注意してください。SSCLI は単なるリファレンス実装であり、最終的なライブラリではないと思っていたので、これには驚きました。繰り返しになりますが、この比較的単純なアルゴリズムを実装する方法は限られていると思います。

もっと理解したかった部分は と の呼び出しCanCompareBitsですFastEqualsCheck。これらはどちらもネイティブ メソッドとして実装されていますが、そのコードも SSCLI に含まれています。以下の実装からわかるように、CLI は (メソッド テーブルを介して) オブジェクトのクラスの定義を調べて、参照型へのポインターが含まれているかどうか、およびオブジェクトのメモリがどのようにレイアウトされているかを確認します。参照がなく、オブジェクトが連続している場合、メモリは C 関数を使用して直接比較されますmemcmp

// Return true if the valuetype does not contain pointer and is tightly packed
FCIMPL1(FC_BOOL_RET, ValueTypeHelper::CanCompareBits, Object* obj)
{
    WRAPPER_CONTRACT;
    STATIC_CONTRACT_SO_TOLERANT;

    _ASSERTE(obj != NULL);
    MethodTable* mt = obj->GetMethodTable();
    FC_RETURN_BOOL(!mt->ContainsPointers() && !mt->IsNotTightlyPacked());
}
FCIMPLEND

FCIMPL2(FC_BOOL_RET, ValueTypeHelper::FastEqualsCheck, Object* obj1,
    Object* obj2)
{
    WRAPPER_CONTRACT;
    STATIC_CONTRACT_SO_TOLERANT;

    _ASSERTE(obj1 != NULL);
    _ASSERTE(obj2 != NULL);
    _ASSERTE(!obj1->GetMethodTable()->ContainsPointers());
    _ASSERTE(obj1->GetSize() == obj2->GetSize());

    TypeHandle pTh = obj1->GetTypeHandle();

    FC_RETURN_BOOL(memcmp(obj1->GetData(),obj2->GetData(),pTh.GetSize()) == 0);
}
FCIMPLEND

私がそれほど怠け者でなければ、ContainsPointersandの実装を検討するかもしれませんIsNotTightlyPacked。しかし、私は知りたかったことを確実に見つけたので (そして私怠け者です)、それは別の日の仕事です。

于 2010-04-29T23:35:11.150 に答える
2

これは見た目よりも複雑です。簡単な答えは次のとおりです。

public bool MyEquals(object obj1, object obj2)
{
  if(obj1==null || obj2==null)
    return obj1==obj2;
  else if(...)
    ...  // Your custom code here
  else if(obj1.GetType().IsValueType)
    return
      obj1.GetType()==obj2.GetType() &&
      !struct1.GetType().GetFields(ALL_FIELDS).Any(field =>
       !MyEquals(field.GetValue(struct1), field.GetValue(struct2)));
  else
    return object.Equals(obj1, obj2);
}

const BindingFlags ALL_FIELDS =
  BindingFlags.Instance |
  BindingFlags.Public |
  BindingFlags.NonPublic;

しかし、それだけではありません。詳細は次のとおりです。

構造体を宣言し、.Equals() をオーバーライドしない場合、NET Framework は、構造体に "単純な" 値の型しかないかどうかに応じて、2 つの異なる戦略のいずれかを使用します ("単純な" は以下で定義されています)。

構造体に「単純な」値型のみが含まれている場合、基本的にビットごとの比較が行われます。

strncmp((byte*)&struct1, (byte*)&struct2, Marshal.Sizeof(struct1));

構造体に参照または「単純」でない値の型が含まれている場合、宣言された各フィールドは object.Equals() と同様に比較されます。

struct1.GetType()==struct2.GetType() &&
!struct1.GetType().GetFields(ALL_FIELDS).Any(field =>
  !object.Equals(field.GetValue(struct1), field.GetValue(struct2)));

「単純な」タイプと見なされるのはどれですか? 私のテストから、基本的なスカラー型 (int、long、decimal、double など) に加えて、.Equals オーバーライドを持たず、「単純な」型のみを (再帰的に) 含む構造体のように見えます。

これには、いくつかの興味深い影響があります。たとえば、次のコードで:

struct DoubleStruct
{
  public double value;
}

public void TestDouble()
{
  var test1 = new DoubleStruct { value = 1 / double.PositiveInfinity };
  var test2 = new DoubleStruct { value = 1 / double.NegativeInfinity };

  bool valueEqual = test1.value.Equals(test2.value);
  bool structEqual = test1.Equals(test2);

  MessageBox.Show("valueEqual=" + valueEqual + ", structEqual=" + structEqual);
}

test1.value と test2.value に何が割り当てられていても、valueEqual は常に structEqual と同一であることが期待されます。これはそうではありません!

この驚くべき結果の理由は、 double.Equals() が、複数の NaN やゼロ表現など、IEEE 754 エンコーディングの複雑さの一部を考慮に入れているが、ビットごとの比較では考慮していないためです。「double」は単純な型と見なされるため、valueEqual が true を返す場合でも、ビットが異なる場合、structEqual は false を返します。

上記の例では代替のゼロ表現を使用しましたが、これは複数の NaN 値でも発生する可能性があります。

...
  var test1 = new DoubleStruct { value = CreateNaN(1) };
  var test2 = new DoubleStruct { value = CreateNaN(2) };
...
public unsafe double CreateNaN(byte lowByte)
{
  double result = double.NaN;
  ((byte*)&result)[0] = lowByte;
  return result;
}

ほとんどの通常の状況では、これで違いはありませんが、注意が必要です。

于 2009-11-05T14:57:02.203 に答える
2

これが、この問題に対する私自身の試みです。それは機能しますが、すべてのベースをカバーしたとは確信していません.

public class MemberwiseEqualityComparer : IEqualityComparer
{
    public bool Equals(object x, object y)
    {
        // ----------------------------------------------------------------
        // 1. If exactly one is null, return false.
        // 2. If they are the same reference, then they must be equal by
        //    definition.
        // 3. If the objects are both IEnumerable, return the result of
        //    comparing each item.
        // 4. If the objects are equatable, return the result of comparing
        //    them.
        // 5. If the objects are different types, return false.
        // 6. Iterate over the public properties and compare them. If there
        //    is a pair that are not equal, return false.
        // 7. Return true.
        // ----------------------------------------------------------------

        //
        // 1. If exactly one is null, return false.
        //
        if (null == x ^ null == y) return false;

        //
        // 2. If they are the same reference, then they must be equal by
        //    definition.
        //
        if (object.ReferenceEquals(x, y)) return true;

        //
        // 3. If the objects are both IEnumerable, return the result of
        //    comparing each item.
        // For collections, we want to compare the contents rather than
        // the properties of the collection itself so we check if the
        // classes are IEnumerable instances before we check to see that
        // they are the same type.
        //
        if (x is IEnumerable && y is IEnumerable && false == x is string)
        {
            return contentsAreEqual((IEnumerable)x, (IEnumerable)y);
        }

        //
        // 4. If the objects are equatable, return the result of comparing
        //    them.
        // We are assuming that the type of X implements IEquatable<> of itself
        // (see below) which is true for the numeric types and string.
        // e.g.: public class TypeOfX : IEquatable<TypeOfX> { ... }
        //
        var xType = x.GetType();
        var yType = y.GetType();
        var equatableType = typeof(IEquatable<>).MakeGenericType(xType);
        if (equatableType.IsAssignableFrom(xType)
            && xType.IsAssignableFrom(yType))
        {
            return equatablesAreEqual(equatableType, x, y);
        }

        //
        // 5. If the objects are different types, return false.
        //
        if (xType != yType) return false;

        //
        // 6. Iterate over the public properties and compare them. If there
        //    is a pair that are not equal, return false.
        //
        if (false == propertiesAndFieldsAreEqual(x, y)) return false;

        //
        // 7. Return true.
        //
        return true;
    }

    public int GetHashCode(object obj)
    {
        return null != obj ? obj.GetHashCode() : 0;
    }

    private bool contentsAreEqual(IEnumerable enumX, IEnumerable enumY)
    {
        var enumOfObjX = enumX.OfType<object>();
        var enumOfObjY = enumY.OfType<object>();

        if (enumOfObjX.Count() != enumOfObjY.Count()) return false;

        var contentsAreEqual = enumOfObjX
            .Zip(enumOfObjY) // Custom Zip extension which returns
                             // Pair<TFirst,TSecond>. Similar to .NET 4's Zip
                             // extension.
            .All(pair => Equals(pair.First, pair.Second))
            ;

        return contentsAreEqual;
    }

    private bool equatablesAreEqual(Type equatableType, object x, object y)
    {
        var equalsMethod = equatableType.GetMethod("Equals");
        var equal = (bool)equalsMethod.Invoke(x, new[] { y });
        return equal;
    }

    private bool propertiesAndFieldsAreEqual(object x, object y)
    {
        const BindingFlags bindingFlags
            = BindingFlags.Public | BindingFlags.Instance;

        var propertyValues = from pi in x.GetType()
                                         .GetProperties(bindingFlags)
                                         .AsQueryable()
                             where pi.CanRead
                             select new
                             {
                                 Name   = pi.Name,
                                 XValue = pi.GetValue(x, null),
                                 YValue = pi.GetValue(y, null),
                             };

        var fieldValues = from fi in x.GetType()
                                      .GetFields(bindingFlags)
                                      .AsQueryable()
                          select new
                          {
                              Name   = fi.Name,
                              XValue = fi.GetValue(x),
                              YValue = fi.GetValue(y),
                          };

        var propertiesAreEqual = propertyValues.Union(fieldValues)
            .All(v => Equals(v.XValue, v.YValue))
            ;

        return propertiesAreEqual;
    }
}
于 2009-11-06T16:17:53.627 に答える
0
public static bool CompareMembers<T>(this T source, T other, params Expression<Func<object>>[] propertiesToSkip)
{
    PropertyInfo[] sourceProperties = source.GetType().GetProperties();

    List<string> propertiesToSkipList = (from x in propertiesToSkip
                                         let a = x.Body as MemberExpression
                                         let b = x.Body as UnaryExpression
                                         select a == null ? ((MemberExpression)b.Operand).Member.Name : a.Member.Name).ToList();

    List<PropertyInfo> lstProperties = (
        from propertyToSkip in propertiesToSkipList
        from property in sourceProperties
        where property.Name != propertyToSkip
        select property).ToList();

    return (!(lstProperties.Any(property => !property.GetValue(source, null).Equals(property.GetValue(other, null)))));
}

使い方:

bool test = myObj1.MemberwiseEqual(myObj2,
        () => myObj.Id,
        () => myObj.Name);
于 2011-02-23T09:20:39.390 に答える