Vergleichen von Objekten in C # .NET

C # .NET bietet viele Möglichkeiten zum Vergleichen von Objekten, sowohl von Instanzen von Klassen als auch von Strukturen. Es gibt so viele Möglichkeiten, dass sich Brei unvermeidlich im Kopf bildet, ohne diese Methoden zu rationalisieren und ihre kompetente Verwendung und Implementierung zu verstehen (wenn die Möglichkeit einer Neudefinition besteht).

Daher bietet die System.Object-Klasse die folgenden Methoden:
  • public static bool ReferenceEquals(object objA, object objB)
    {
        return objA == objB;
    }
             

  • public static bool Equals(object objA, object objB)
    {
        return objA == objB || (objA != null && objB != null && objA.Equals(objB));
    }
             

  • public virtual bool Equals(object obj)
    {
        return RuntimeHelpers.Equals(this, obj);
    }
             


Na und natürlich:
public static bool operator == (Foo left, Foo right);

Es ist auch möglich, IEquatable, IStructuralEquatable zu erben.

ReferenceEquals

Die ReferenceEquals-Methode vergleicht zwei Links. Wenn die Verweise auf die Objekte identisch sind, wird true zurückgegeben. Dies bedeutet, dass diese Methode Instanzen nicht auf Gleichheit, sondern auf Identität überprüft. Wenn Sie Instanzen eines signifikanten Typs an diese Methode übergeben (auch wenn Sie dieselbe Instanz übergeben), wird immer false zurückgegeben. Dies geschieht, weil während des Transfers Verpackungen von bedeutendem Typ vorhanden sind und die Verknüpfungen zu diesen unterschiedlich sind.
An dieser Stelle möchte ich auch den Vergleich zweier Zeilen nach dieser Methode erwähnen. Zum Beispiel:
class Program
    {
        static void Main(string[] args)
        {
            string a = "Hello";
            string b = "Hello";
            if(object.ReferenceEquals(a,b))
                Console.WriteLine("Same objects");
            else
                Console.WriteLine("Not the same objects");
            Console.ReadLine();
        }
    }

Ein solches Programm kann problemlos "Gleiche Objekte" ausgeben. Mach dir keine Sorgen, das liegt an der Saiteninternierung. Aber das ist eine ganz andere Geschichte und darüber wird nicht gesprochen.

public static bool Equals (Objekt objA, Objekt objB)

Diese Methode prüft zunächst die Instanzen auf Identität. Wenn die Objekte nicht identisch sind, prüft sie auf Null und delegiert die Verantwortung für den Vergleich der neu definierten Instanzmethode Equals.

public virtual bool Equals (Objekt obj)

Standardmäßig verhält sich diese Methode genau wie ReferenceEquals. Für aussagekräftige Typen wird es jedoch in System.ValueType wie folgt überschrieben:
public override bool Equals(object obj)
		{
			if (obj == null)
			{
				return false;
			}
			RuntimeType runtimeType = (RuntimeType)base.GetType();
			RuntimeType left = (RuntimeType)obj.GetType();
			if (left != runtimeType)
			{
				return false;
			}
			if (ValueType.CanCompareBits(this))
			{
				return ValueType.FastEqualsCheck(this, obj);
			}
			FieldInfo[] fields = runtimeType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
			for (int i = 0; i < fields.Length; i++)
			{
				object obj2 = ((RtFieldInfo)fields[i]).InternalGetValue(this, false);
				object obj3 = ((RtFieldInfo)fields[i]).InternalGetValue(obj, false);
				if (obj2 == null)
				{
					if (obj3 != null)
					{
						return false;
					}
				}
				else
				{
					if (!obj2.Equals(obj3))
					{
						return false;
					}
				}
			}
			return true;
		}

Gott verbiete jedem, eine solche Implementierung für große Mengen zu verwenden. BCL-Entwickler können nicht wissen, welche wichtigen Typen wir definieren und Instanzen von wichtigen Typen anhand ihrer Felder mithilfe von Reflektion vergleichen, ohne vorher etwas über diese Felder zu wissen. Dies ist natürlich kein sehr produktiver Vergleich. Wenn Sie also wichtige Typen verwenden, die in der Kompilierungsphase bekannt sind, müssen Sie diese Methode außer Kraft setzen, denn wer kann die beiden von Ihnen entwickelten Objekte besser vergleichen als Sie? Bei Referenztypen ist das Überschreiben dieser Methode optional, ohne dass zwei Instanzen in der Art von signifikanten Typen verglichen werden müssen.
Schauen wir uns ein Beispiel für eine kompetente Neudefinition dieser Methode an und implementieren IEquatable sofort:
class Vehicle:IEquatable
    {
        protected int speed;
        public int Speed
        {
            get { return this.speed; }
            set { this.speed = value; }
        }
        protected string name;
        public string Name
        {
            get { return this.name; }
            set { this.name = value; }
        }
        public Vehicle(){}
        public Vehicle(int speed, string name)
        {
            this.speed = speed;
            this.name = name;
        }
        public override bool Equals(object other)
        {
            //Последовательность проверки должна быть именно такой.
            //Если не проверить на null объект other, то other.GetType() может выбросить //NullReferenceException.            
            if (other == null)
                return false;
            //Если ссылки указывают на один и тот же адрес, то их идентичность гарантирована.
            if (object.ReferenceEquals(this, other))
                return true;
            //Если класс находится на вершине иерархии или просто не имеет наследников, то можно просто
            //сделать Vehicle tmp = other as Vehicle; if(tmp==null) return false; 
            //Затем вызвать экземплярный метод, сразу передав ему объект tmp.
            if (this.GetType() != other.GetType()) 
                return false;                                                                        
            return this.Equals(other as Vehicle);
        }
        public bool Equals(Vehicle other)
        {
            if (other == null)
                return false;
            //Здесь сравнение по ссылкам необязательно.
            //Если вы уверены, что многие проверки на идентичность будут отсекаться на проверке по ссылке - //можно имплементировать.
            if (object.ReferenceEquals(this, other))
                return true;
            //Если по логике проверки, экземпляры родительского класса и класса потомка могут считаться равными,
            //то проверять на идентичность необязательно и можно переходить сразу к сравниванию полей.
            if (this.GetType() != other.GetType())
                return false;
            if (string.Compare(this.Name, other.Name, StringComparison.CurrentCulture) == 0 && this.speed.Equals(other.speed))
                return true;
            else
                return false;
        }        
    }

Der Kommentar zur Spitze der Hierarchie bei der Neudefinition der virtuellen Methode erfolgt aus einem bestimmten Grund. Wenn Sie einen Erben der Vehicle-Klasse erstellen (z. B. Bike), der auch eine überschriebene virtuelle Equals-Methode enthält, bei der GetType keinen Typvergleich durchführt und versucht, eine Konvertierung durchzuführen, Bike tmp = other as Bike; if(tmp!=null) this.Equals(tmp);kann der folgende Code in diesem Fall Probleme verursachen:
Vehicle vehicle = new Vehicle();
Bike bike = new Bike();
object vehicleObj = vehicle;
object bikeObject = bike;
bike.Equals(vehicleObj); //Базовый тип не сможет привестись к наследнику. Таким образом, может быть //нарушено свойство симметричности сравнения объектов


public static bool operator == (Foo left, Foo right)

Für aussagekräftige Typen sollten Sie diese wie bei virtual Equals () immer neu definieren. Bei Referenztypen ist es besser, sie nicht zu überschreiben, da bei Referenztypen von == standardmäßig das Verhalten wie bei der ReferenceEquals () -Methode erwartet wird. Hier ist also alles einfach.

IStructuralEquatable

IStructuralEquatable geht Hand in Hand mit der IEqualityComparer-Schnittstelle. Die IStructuralEquatable-Schnittstelle wird von Klassen wie System.Array oder System.Tuple implementiert. Laut Bill Wagner erklärt IStructuralEquality, dass ein Typ größere Objekte darstellen kann, die die Semantik bedeutungsvoller Typen implementieren, und es ist unwahrscheinlich, dass wir ihn jemals selbst implementieren müssen. Was ist jedoch schwierig in der Umsetzung? Schauen Sie sich die Implementierung in System.Array an:
bool IStructuralEquatable.Equals(object other, IEqualityComparer comparer)
		{
			if (other == null)
			{
				return false;
			}
			if (object.ReferenceEquals(this, other))
			{
				return true;
			}
			Array array = other as Array;
			if (array == null || array.Length != this.Length)
			{
				return false;
			}
			for (int i = 0; i < array.Length; i++)
			{
				object value = this.GetValue(i);
				object value2 = array.GetValue(i);
				if (!comparer.Equals(value, value2))
				{
					return false;
				}
			}
			return true;
		}

Tatsächlich wird zuerst die Identität der Objekte überprüft, dann werden sie auf denselben Typ umgewandelt und in der Länge verglichen. Wenn die Länge gleich ist, delegiert der elementweise Vergleich die Verantwortung für diesen Vergleich an die Equals-Schnittstellenmethode (IEqualityComparer).

Das ist im Wesentlichen alles, was zum Vergleichen von Objekten in C # .NET gesagt werden kann, aber es bleibt noch ein kleines, aber wichtiges Detail: die GetHashCode () -Methode.

public virtual int GetHashCode ()

Im Allgemeinen verhält sich die Standardimplementierung dieser Methode wie ein Generator für eindeutige Bezeichner. Der Nachteil dieses Ansatzes besteht darin, dass dieselben semantischen Objekte unterschiedliche Hashwerte zurückgeben können. Richter beklagt, dass die Standardimplementierung ebenfalls schlecht abschneidet. Eine kompetente Umsetzung dieser Methode ist sehr problematisch. Es ist notwendig, Hash schnell zu berechnen und eine große Streuung zu haben, damit es bei ausreichend großen Mengen zu keinen Wiederholungen kommt. Tatsächlich ist die Implementierung von GetHashCode () in den meisten Fällen sehr einfach. Überall werden Verschiebungen vorgenommen, "bitweise oder" oder "exklusiv oder". Richter selbst gibt ein Beispiel mit einer Struktur an, die zwei Felder vom Typ int hat. GetHashCode () schlägt vor, so etwas zu implementieren:
internal sealed class Point
    {
        private int a;
        private int b;
        public override int  GetHashCode()
        {
            return a ^ b;
        }
    }

Und so wird GetHasCode () in System.Char neu definiert:
public override int GetHashCode()
		{
			return (int)this | (int)this << 16;
		}

Es können viele Beispiele angeführt werden, und heuristische Indikatoren werden fast überall für Schichten verwendet, die keine oder nur wenige Schichten enthalten.

Beim Schreiben des Artikels wurden bekannte Quellen verwendet:
J.Richter, CLR über C #
B. Wagner Effective C #
Auch ich habe meine Erfahrungen und Quellen im Internet genutzt, was nicht viel Sinn macht.


Übersetzung hier

Jetzt auch beliebt: