Dot-Net

.NET 結構中的成員相等性測試使用的算法是什麼?

  • February 23, 2011

.NET 結構中的成員相等性測試使用的算法是什麼?我想知道這一點,以便我可以將其用作我自己算法的基礎。

我正在嘗試為任意對象(在 C# 中)編寫遞歸成員相等性測試,以測試 DTO 的邏輯相等性。如果 DTO 是結構,這會容易得多(因為 ValueType.Equals 主要做正確的事情),但這並不總是合適的。我還想覆蓋任何 IEnumerable 對象(但不是字元串!)的比較,以便比較它們的內容而不是它們的屬性。

事實證明,這比我預期的要難。任何提示將不勝感激。我會接受證明最有用的答案或提供指向最有用資訊的連結。

謝謝。

這是ValueType.Equals來自共享源公共語言基礎結構(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。然而,我已經明確地找出了我想知道的(而且我很懶),所以這是另一天的工作。

沒有預設的成員相等性,但對於基值類型(float,bytedecimal),語言規範要求按位比較。JIT 優化器將其優化為正確的彙編指令,但從技術上講,這種行為等同於 Cmemcmp函式。

一些 BCL 範例

  • DateTime只是比較它的內部InternalTicks成員欄位,它是一個long;
  • 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是這個列表中最奇怪的:它有自己的短路長列表 if 語句比較每個內部欄位(_ato _k,所有 int)是否不相等,不相等時返回 false。如果所有不相等,則返回 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不應被視為覆蓋,已修復。

引用自:https://stackoverflow.com/questions/1680602