Not
Bu sayfaya erişim yetkilendirme gerektiriyor. Oturum açmayı veya dizinleri değiştirmeyi deneyebilirsiniz.
Bu sayfaya erişim yetkilendirme gerektiriyor. Dizinleri değiştirmeyi deneyebilirsiniz.
Tavsiye
Önce kayıtları kullanmayı düşünün. Kayıtlar en az kodla otomatik olarak değer eşitliği uygular ve bu da veri odaklı türlerin çoğu için önerilen yaklaşımdır. Özel değer eşitliği mantığına ihtiyacınız varsa veya kayıtları kullanamıyorsanız, aşağıdaki el ile uygulama adımlarıyla devam edin.
Bir sınıf veya yapı tanımladığınızda, tür için değer eşitliği (veya denkliği) için özel bir tanım oluşturmanın mantıklı olup olmadığına karar verirsiniz. Genellikle, bir koleksiyona türdeki nesneleri eklemeyi beklediğinizde veya birincil amacı bir alan veya özellik kümesi depolamak olduğunda değer eşitliği uygularsınız. Değer eşitliği tanımınızı türdeki tüm alanların ve özelliklerin karşılaştırmasını temel alabilir veya tanımı bir alt kümeye dayandırabilirsiniz.
Her iki durumda da ve hem sınıflarda hem de yapılarda uygulamanız beş denklik garantisine uymalıdır (aşağıdaki kurallar için , x ve y değerlerinin znull olmadığını varsayın):
Refleksif özellik:
x.Equals(x)truedöndürür.Simetrik özellik:
x.Equals(y)ile aynı değeriy.Equals(x)döndürür.Geçişli özellik: Eğer
(x.Equals(y) && y.Equals(z))truedöndürürse, o zamanx.Equals(z)truedöndürür.x ve y tarafından başvuruda bulunan nesneler değiştirilmediği sürece birbirini izleyen çağrılar
x.Equals(y)aynı değeri döndürür.Null olmayan değerler null değerine eşit değildir. Ancak null
x.Equals(y)olduğundaxbir özel durum oluşturur. Bu,Equalsbağımsız değişkenine bağlı olarak 1 veya 2. kuralı bozar.
Tanımladığınız herhangi bir yapı, System.ValueType yönteminin Object.Equals(Object) geçersiz kılınmasından devraldığı varsayılan değer eşitliği uygulamasına zaten sahiptir. Bu uygulama, türdeki tüm alanları ve özellikleri incelemek için yansıma kullanır. Bu uygulama doğru sonuçlar üretse de, türü için özel olarak yazdığınız özel bir uygulamayla karşılaştırıldığında nispeten yavaştır.
Değer eşitliği için uygulama ayrıntıları sınıflar ve yapılar için farklıdır. Ancak, hem sınıflar hem de yapılar eşitlik uygulamak için aynı temel adımları gerektirir:
SanalObject.Equals(Object) yöntemi geçersiz kılın. Bu, nesnelerinizin referans olarak
objectdeğerlendirildiğinde doğru şekilde karşılaştırılabilmesini sağlayan çok biçimli eşitlik özelliği sunar. Koleksiyonlarda ve çok biçimlilik kullanılırken doğru davranışı sağlar. Çoğu durumda,bool Equals( object obj )uygulamanız,Equalsarabiriminin bir uygulaması olan tür özel System.IEquatable<T> yöntemini çağırmalıdır. (Bkz. 2. adım.)Tipine özgü System.IEquatable<T> yöntem sağlayarak
Equalsarabirimini uygulayın. Bu, kutulama olmadan tür güvenli eşitlik denetimi sağlayarak daha iyi performans sağlar. Ayrıca gereksiz atamaları önler ve derleme zamanı türü denetimini etkinleştirir. Burada gerçek denklik karşılaştırması gerçekleştirilir. Örneğin, türünizdeki yalnızca bir veya iki alanı karşılaştırarak eşitlik tanımlamaya karar vekleyebilirsiniz.Equals'den özel durum fırlatmayın. Kalıtım yoluyla birbirine bağlı sınıflar için:Bu yöntem yalnızca sınıfında bildirilen alanları incelemelidir. Temel sınıftaki alanları incelemek için
base.Equalsçağrı yapmalıdır. (Doğrudanbase.Equalsöğesinden tür devralıyorsa Object çağırmayın, çünkü Object'nin Object.Equals(Object) üzerindeki uygulaması bir başvuru eşitliği denetimi yapar.)yalnızca karşılaştırılan değişkenlerin çalışma zamanı türleri aynıysa iki değişken eşit kabul edilmelidir. Ayrıca, bir değişkenin çalışma zamanı ve derleme zamanı türleri farklıysa,
IEquatableyöntemi için çalışma zamanı türünün uygulanmasınınEqualskullanıldığından emin olun. Çalışma zamanı türlerinin her zaman doğru karşılaştırıldığından emin olmak için bir strateji yalnızca sınıflardaIEquatableuygulamaktırsealed. Daha fazla bilgi için bu makalenin devamında yer alan sınıf örneğine bakın.
İsteğe bağlı ama önerilir: == ve != işleçlerini aşırı yükleyin. Bu, eşitlik karşılaştırmaları için yerleşik türlerden kullanıcı beklentilerini eşleştiren tutarlı ve sezgisel söz dizimi sağlar.
obj1 == obj2veobj1.Equals(obj2)'in aynı şekilde davranmasını sağlar.Object.GetHashCode'yi, değer açısından eşit olan iki nesnenin aynı karma kodu üretmesini sağlamak için geçersiz kılın. Bu,
Dictionary<TKey,TValue>veHashSet<T>gibi karma tabanlı koleksiyonlarda doğru davranış için gereklidir. Eşit olan nesnelerin karma kodları eşit olmalıdır, aksi durumda bu koleksiyonlar düzgün çalışmaz.İsteğe bağlı: "büyüktür" veya "küçüktür" tanımlarını desteklemek için, türünüz için IComparable<T> arabirimini uygulayın ve ayrıca <= ve >= işleçlerini aşırı yükleyin. Bu, sıralama işlemlerini etkinleştirir ve türünüz için tam bir sıralama ilişkisi sağlar; sıralanmış koleksiyonlara nesne eklerken veya dizileri veya listeleri sıralarken kullanışlıdır.
Kayıt örneği
Aşağıdaki örnekte kayıtların minimum kodla otomatik olarak değer eşitliğini nasıl uyguladığı gösterilmektedir. İlk kayıt TwoDPoint , değer eşitliğini otomatik olarak uygulayan basit bir kayıt türüdür. İkinci kayıt ThreeDPoint , kayıtların diğer kayıtlardan türetilebileceğini ve yine de uygun değer eşitliği davranışını koruyabileceğini gösterir:
namespace ValueEqualityRecord;
public record TwoDPoint(int X, int Y);
public record ThreeDPoint(int X, int Y, int Z) : TwoDPoint(X, Y);
class Program
{
static void Main(string[] args)
{
// Create some points
TwoDPoint pointA = new TwoDPoint(3, 4);
TwoDPoint pointB = new TwoDPoint(3, 4);
TwoDPoint pointC = new TwoDPoint(5, 6);
ThreeDPoint point3D_A = new ThreeDPoint(3, 4, 5);
ThreeDPoint point3D_B = new ThreeDPoint(3, 4, 5);
ThreeDPoint point3D_C = new ThreeDPoint(3, 4, 7);
Console.WriteLine("=== Value Equality with Records ===");
// Value equality works automatically
Console.WriteLine($"pointA.Equals(pointB) = {pointA.Equals(pointB)}"); // True
Console.WriteLine($"pointA == pointB = {pointA == pointB}"); // True
Console.WriteLine($"pointA.Equals(pointC) = {pointA.Equals(pointC)}"); // False
Console.WriteLine($"pointA == pointC = {pointA == pointC}"); // False
Console.WriteLine("\n=== Hash Codes ===");
// Equal objects have equal hash codes automatically
Console.WriteLine($"pointA.GetHashCode() = {pointA.GetHashCode()}");
Console.WriteLine($"pointB.GetHashCode() = {pointB.GetHashCode()}");
Console.WriteLine($"pointC.GetHashCode() = {pointC.GetHashCode()}");
Console.WriteLine("\n=== Inheritance with Records ===");
// Inheritance works correctly with value equality
Console.WriteLine($"point3D_A.Equals(point3D_B) = {point3D_A.Equals(point3D_B)}"); // True
Console.WriteLine($"point3D_A == point3D_B = {point3D_A == point3D_B}"); // True
Console.WriteLine($"point3D_A.Equals(point3D_C) = {point3D_A.Equals(point3D_C)}"); // False
// Different types are not equal (unlike problematic class example)
Console.WriteLine($"pointA.Equals(point3D_A) = {pointA.Equals(point3D_A)}"); // False
Console.WriteLine("\n=== Collections ===");
// Works seamlessly with collections
var pointSet = new HashSet<TwoDPoint> { pointA, pointB, pointC };
Console.WriteLine($"Set contains {pointSet.Count} unique points"); // 2 unique points
var pointDict = new Dictionary<TwoDPoint, string>
{
{ pointA, "First point" },
{ pointC, "Different point" }
};
// Demonstrate that equivalent points work as the same key
var duplicatePoint = new TwoDPoint(3, 4);
Console.WriteLine($"Dictionary contains key for {duplicatePoint}: {pointDict.ContainsKey(duplicatePoint)}"); // True
Console.WriteLine($"Dictionary contains {pointDict.Count} entries"); // 2 entries
Console.WriteLine("\n=== String Representation ===");
// Automatic ToString implementation
Console.WriteLine($"pointA.ToString() = {pointA}");
Console.WriteLine($"point3D_A.ToString() = {point3D_A}");
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Expected Output:
=== Value Equality with Records ===
pointA.Equals(pointB) = True
pointA == pointB = True
pointA.Equals(pointC) = False
pointA == pointC = False
=== Hash Codes ===
pointA.GetHashCode() = -1400834708
pointB.GetHashCode() = -1400834708
pointC.GetHashCode() = -148136000
=== Inheritance with Records ===
point3D_A.Equals(point3D_B) = True
point3D_A == point3D_B = True
point3D_A.Equals(point3D_C) = False
pointA.Equals(point3D_A) = False
=== Collections ===
Set contains 2 unique points
Dictionary contains key for TwoDPoint { X = 3, Y = 4 }: True
Dictionary contains 2 entries
=== String Representation ===
pointA.ToString() = TwoDPoint { X = 3, Y = 4 }
point3D_A.ToString() = ThreeDPoint { X = 3, Y = 4, Z = 5 }
*/
Kayıtlar, değer eşitliği için çeşitli avantajlar sağlar:
-
Otomatik uygulama: Kayıtlar System.IEquatable<T> ve Object.Equals, Object.GetHashCode ve
==/!=işleçlerini otomatik olarak uygular ve geçersiz kılar. -
Doğru devralma davranışı: Kayıtlar, devralma hiyerarşilerinde ve çok biçimli senaryolarda doğru davranışı sağlayarak her iki işlenenin çalışma zamanı türünü denetleen sanal yöntemler kullanılarak uygulanır
IEquatable<T>. - Varsayılan olarak değişmezlik: Kayıtlar, değer eşitliği semantiğiyle iyi çalışan sabit tasarımı teşvik eder.
- Kısa söz dizimi: Konumsal parametreler, veri türlerini tanımlamak için küçük bir yol sağlar.
- Daha iyi performans: Derleyici tarafından oluşturulan eşitlik uygulaması iyileştirilir ve varsayılan yapı uygulaması gibi yansıma kullanmaz.
Birincil hedefiniz verileri depolamak olduğunda ve değer eşitliği semantiğine ihtiyacınız olduğunda kayıtları kullanın.
Başvuru eşitliği kullanan üyeleri olan kayıtlar
Kayıtlar başvuru eşitliği kullanan üyeler içerdiğinde, kayıtların otomatik değer eşitliği davranışı beklendiği gibi çalışmaz. Bu, gibi System.Collections.Generic.List<T>koleksiyonlar için geçerlidir, diziler ve değer tabanlı eşitlik uygulamayan diğer başvuru türleri (değer eşitliği uygulayan önemli özel durumu System.Stringile).
Önemli
Kayıtlar temel veri türleri için mükemmel değer eşitliği sağlasa da, başvuru eşitliği kullanan üyeler için değer eşitliğini otomatik olarak çözmez. Bir kayıt, değer eşitliği uygulamayan bir System.Collections.Generic.List<T>, System.Arrayveya başka başvuru türleri içeriyorsa, üyeler başvuru eşitliğini kullandığından, bu üyelerde aynı içeriğe sahip iki kayıt örneği yine de eşit olmaz.
public record PersonWithHobbies(string Name, List<string> Hobbies);
var person1 = new PersonWithHobbies("Alice", new List<string> { "Reading", "Swimming" });
var person2 = new PersonWithHobbies("Alice", new List<string> { "Reading", "Swimming" });
Console.WriteLine(person1.Equals(person2)); // False - different List instances!
Bunun nedeni kayıtların her üyenin Object.Equals yöntemini kullanması ve koleksiyon türlerinin genellikle içeriklerini karşılaştırmak yerine başvuru eşitliğini kullanmasıdır.
Aşağıdaki sorun gösterilmektedir:
// Records with reference-equality members don't work as expected
public record PersonWithHobbies(string Name, List<string> Hobbies);
Kodu çalıştırdığınızda şu şekilde davranır:
Console.WriteLine("=== Records with Collections - The Problem ===");
// Problem: Records with mutable collections use reference equality for the collection
var person1 = new PersonWithHobbies("Alice", [ "Reading", "Swimming" ]);
var person2 = new PersonWithHobbies("Alice", [ "Reading", "Swimming" ]);
Console.WriteLine($"person1: {person1}");
Console.WriteLine($"person2: {person2}");
Console.WriteLine($"person1.Equals(person2): {person1.Equals(person2)}"); // False! Different List instances
Console.WriteLine($"Lists have same content: {person1.Hobbies.SequenceEqual(person2.Hobbies)}"); // True
Console.WriteLine();
Başvuru eşitliği üyelerine sahip kayıtlar için çözümler
Özel System.IEquatable<T> uygulama: Derleyici tarafından oluşturulan eşitliği, başvuru eşitliği üyeleri için içerik tabanlı karşılaştırma sağlayan el ile kodlanmış bir sürümle değiştirin. Koleksiyonlar için veya benzer yöntemler kullanarak Enumerable.SequenceEqual öğeye göre öğe karşılaştırması uygulayın.
Mümkün olduğunda değer türlerini kullanın: Verilerinizin değer türleriyle mi yoksa veya Planegibi doğal olarak System.Numerics.Vector<T> değer eşitliğini destekleyen sabit yapılarla mı temsil edilebileceğini düşünün.
Değer tabanlı eşitlikle türleri kullanma: Koleksiyonlar için, değer tabanlı eşitlik uygulayan türleri kullanmayı veya veya System.Collections.Immutable.ImmutableList<T>gibi System.Collections.Immutable.ImmutableArray<T> içerik tabanlı karşılaştırma sağlamak için geçersiz kılan Object.Equals özel koleksiyon türlerini uygulamayı göz önünde bulundurun.
Başvuru eşitliğini göz önünde bulundurarak tasarlama: Bazı üyelerin başvuru eşitliğini kullanacağını kabul edin ve uygulama mantığınızı uygun şekilde tasarlayarak eşitlik önemli olduğunda aynı örnekleri yeniden kullandığınızdan emin olun.
Koleksiyonları olan kayıtlar için özel eşitlik uygulama örneği aşağıda verilmiştir:
// A potential solution using IEquatable<T> with custom equality
public record PersonWithHobbiesFixed(string Name, List<string> Hobbies) : IEquatable<PersonWithHobbiesFixed>
{
public virtual bool Equals(PersonWithHobbiesFixed? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
// Use SequenceEqual for List comparison
return Name == other.Name && Hobbies.SequenceEqual(other.Hobbies);
}
public override int GetHashCode()
{
// Create hash based on content, not reference
var hashCode = new HashCode();
hashCode.Add(Name);
foreach (var hobby in Hobbies)
{
hashCode.Add(hobby);
}
return hashCode.ToHashCode();
}
}
Bu özel uygulama düzgün çalışır:
Console.WriteLine("=== Solution 1: Custom IEquatable Implementation ===");
var personFixed1 = new PersonWithHobbiesFixed("Bob", [ "Cooking", "Hiking" ]);
var personFixed2 = new PersonWithHobbiesFixed("Bob", [ "Cooking", "Hiking" ]);
Console.WriteLine($"personFixed1: {personFixed1}");
Console.WriteLine($"personFixed2: {personFixed2}");
Console.WriteLine($"personFixed1.Equals(personFixed2): {personFixed1.Equals(personFixed2)}"); // True! Custom equality
Console.WriteLine();
Aynı sorun dizileri ve diğer koleksiyon türlerini de etkiler:
// These also use reference equality - the issue persists
public record PersonWithHobbiesArray(string Name, string[] Hobbies);
public record PersonWithHobbiesImmutable(string Name, IReadOnlyList<string> Hobbies);
Diziler de başvuru eşitliğini kullanarak aynı beklenmeyen sonuçları üretir:
Console.WriteLine("=== Arrays Also Use Reference Equality ===");
var personArray1 = new PersonWithHobbiesArray("Charlie", ["Gaming", "Music" ]);
var personArray2 = new PersonWithHobbiesArray("Charlie", ["Gaming", "Music" ]);
Console.WriteLine($"personArray1: {personArray1}");
Console.WriteLine($"personArray2: {personArray2}");
Console.WriteLine($"personArray1.Equals(personArray2): {personArray1.Equals(personArray2)}"); // False! Arrays use reference equality too
Console.WriteLine($"Arrays have same content: {personArray1.Hobbies.SequenceEqual(personArray2.Hobbies)}"); // True
Console.WriteLine();
Salt okunur koleksiyonlar bile bu başvuru eşitliği davranışını sergiler:
Console.WriteLine("=== Same Issue with IReadOnlyList ===");
var personImmutable1 = new PersonWithHobbiesImmutable("Diana", [ "Art", "Travel" ]);
var personImmutable2 = new PersonWithHobbiesImmutable("Diana", [ "Art", "Travel" ]);
Console.WriteLine($"personImmutable1: {personImmutable1}");
Console.WriteLine($"personImmutable2: {personImmutable2}");
Console.WriteLine($"personImmutable1.Equals(personImmutable2): {personImmutable1.Equals(personImmutable2)}"); // False! Reference equality
Console.WriteLine($"Content is the same: {personImmutable1.Hobbies.SequenceEqual(personImmutable2.Hobbies)}"); // True
Console.WriteLine();
Önemli içgörü, kayıtların yapısal eşitlik sorununu çözmesi ancak içerdikleri türlerin anlamsal eşitlik davranışını değiştirmemeleridir.
Sınıf örneği
Aşağıdaki örnekte, bir sınıfta değer eşitliğinin nasıl uygulandığı gösterilmektedir (başvuru türü). Kayıtları kullanamıyorsanız veya özel eşitlik mantığına ihtiyaç duyduğunuzda bu el ile yaklaşım gereklidir:
namespace ValueEqualityClass;
class TwoDPoint : IEquatable<TwoDPoint>
{
public int X { get; private set; }
public int Y { get; private set; }
public TwoDPoint(int x, int y)
{
if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.X = x;
this.Y = y;
}
public override bool Equals(object obj) => this.Equals(obj as TwoDPoint);
public bool Equals(TwoDPoint p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// If run-time types are not exactly the same, return false.
if (this.GetType() != p.GetType())
{
return false;
}
// Return true if the fields match.
// Note that the base class is not invoked because it is
// System.Object, which defines Equals as reference equality.
return (X == p.X) && (Y == p.Y);
}
public override int GetHashCode() => (X, Y).GetHashCode();
public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
{
if (lhs is null)
{
if (rhs is null)
{
return true;
}
// Only the left side is null.
return false;
}
// Equals handles case of null on right side.
return lhs.Equals(rhs);
}
public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
}
// For the sake of simplicity, assume a ThreeDPoint IS a TwoDPoint.
class ThreeDPoint : TwoDPoint, IEquatable<ThreeDPoint>
{
public int Z { get; private set; }
public ThreeDPoint(int x, int y, int z)
: base(x, y)
{
if ((z < 1) || (z > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.Z = z;
}
public override bool Equals(object obj) => this.Equals(obj as ThreeDPoint);
public bool Equals(ThreeDPoint p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// Check properties that this class declares.
if (Z == p.Z)
{
// Let base class check its own fields
// and do the run-time type comparison.
return base.Equals((TwoDPoint)p);
}
else
{
return false;
}
}
public override int GetHashCode() => (X, Y, Z).GetHashCode();
public static bool operator ==(ThreeDPoint lhs, ThreeDPoint rhs)
{
if (lhs is null)
{
if (rhs is null)
{
// null == null = true.
return true;
}
// Only the left side is null.
return false;
}
// Equals handles the case of null on right side.
return lhs.Equals(rhs);
}
public static bool operator !=(ThreeDPoint lhs, ThreeDPoint rhs) => !(lhs == rhs);
}
class Program
{
static void Main(string[] args)
{
ThreeDPoint pointA = new ThreeDPoint(3, 4, 5);
ThreeDPoint pointB = new ThreeDPoint(3, 4, 5);
ThreeDPoint pointC = null;
int i = 5;
Console.WriteLine($"pointA.Equals(pointB) = {pointA.Equals(pointB)}");
Console.WriteLine($"pointA == pointB = {pointA == pointB}");
Console.WriteLine($"null comparison = {pointA.Equals(pointC)}");
Console.WriteLine($"Compare to some other type = {pointA.Equals(i)}");
TwoDPoint pointD = null;
TwoDPoint pointE = null;
Console.WriteLine($"Two null TwoDPoints are equal: {pointD == pointE}");
pointE = new TwoDPoint(3, 4);
Console.WriteLine($"(pointE == pointA) = {pointE == pointA}");
Console.WriteLine($"(pointA == pointE) = {pointA == pointE}");
Console.WriteLine($"(pointA != pointE) = {pointA != pointE}");
System.Collections.ArrayList list = new System.Collections.ArrayList();
list.Add(new ThreeDPoint(3, 4, 5));
Console.WriteLine($"pointE.Equals(list[0]): {pointE.Equals(list[0])}");
// Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
pointA.Equals(pointB) = True
pointA == pointB = True
null comparison = False
Compare to some other type = False
Two null TwoDPoints are equal: True
(pointE == pointA) = False
(pointA == pointE) = False
(pointA != pointE) = True
pointE.Equals(list[0]): False
*/
Sınıflarda (başvuru türleri), her iki Object.Equals(Object) yöntemin de varsayılan uygulaması, değer eşitliği kontrolü değil, başvuru eşitliği karşılaştırması gerçekleştirir. Bir uygulayıcı sanal yöntemi geçersiz kıldığında, amaç değer eşitliği semantiği vermektir.
== ve != işleçleri, sınıfı aşırı yüklemese bile sınıflarla birlikte kullanılabilir. Ancak, varsayılan davranış bir başvuru eşitliği denetimi gerçekleştirmektir. Bir sınıfta, eğer Equals yöntemini aşırı yüklerseniz, == ve != işleçlerini de aşırı yüklemelisiniz, ancak bu zorunlu değildir.
Önemli
Yukarıdaki örnek kod, her devralma senaryolarını beklediğiniz gibi işlemeyebilir. Aşağıdaki kodu inceleyin:
TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
Console.WriteLine(p1.Equals(p2)); // output: True
Bu kod, farklı p1 değerlerine rağmen p2'in z'e eşit olduğunu bildirir. Derleyici, derleme zamanı türüne göre TwoDPoint'in uygulamasını seçtiği için IEquatable farkı yoksayılır. Bu, devralma hiyerarşilerinde çok biçimli eşitlikle ilgili temel bir sorundur.
Çok biçimli eşitlik
Sınıflarla devralma hiyerarşilerinde değer eşitliği uygulanırken, sınıf örneğinde gösterilen standart yaklaşım, nesneler çok biçimli kullanıldığında yanlış davranışa yol açabilir. Bu sorun, uygulamalar çalışma zamanı türüne değil derleme zamanı türüne göre seçildiğinden oluşur System.IEquatable<T> .
Standart uygulamalarla ilgili sorun
Bu sorunlu senaryoya göz önünde bulundurun:
TwoDPoint p1 = new ThreeDPoint(1, 2, 3); // Declared as TwoDPoint
TwoDPoint p2 = new ThreeDPoint(1, 2, 4); // Declared as TwoDPoint
Console.WriteLine(p1.Equals(p2)); // True - but should be False!
Derleyici, koordinat farklarını TwoDPoint.Equals(TwoDPoint) yoksayarak bildirilen türe göre seçtiğinden Z karşılaştırma döndürülüyorTrue.
Polimorfik eşitliği düzeltmenin anahtarı, tüm eşitlik karşılaştırmalarının çalışma zamanı türlerini denetleyebilen ve devralmayı doğru şekilde işleyebilen sanal Object.Equals yöntemi kullanmasını sağlamaktır. Bu, sanal yönteme temsilciler için System.IEquatable<T> açık arabirim uygulaması kullanılarak gerçekleştirilebilir:
Temel sınıf, anahtar desenlerini gösterir:
// Safe polymorphic equality implementation using explicit interface implementation
class TwoDPoint : IEquatable<TwoDPoint>
{
public int X { get; private set; }
public int Y { get; private set; }
public TwoDPoint(int x, int y)
{
if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.X = x;
this.Y = y;
}
public override bool Equals(object? obj) => Equals(obj as TwoDPoint);
// Explicit interface implementation prevents compile-time type issues
bool IEquatable<TwoDPoint>.Equals(TwoDPoint? p) => Equals((object?)p);
protected virtual bool Equals(TwoDPoint? p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// If run-time types are not exactly the same, return false.
if (this.GetType() != p.GetType())
{
return false;
}
// Return true if the fields match.
// Note that the base class is not invoked because it is
// System.Object, which defines Equals as reference equality.
return (X == p.X) && (Y == p.Y);
}
public override int GetHashCode() => (X, Y).GetHashCode();
public static bool operator ==(TwoDPoint? lhs, TwoDPoint? rhs)
{
if (lhs is null)
{
if (rhs is null)
{
return true;
}
// Only the left side is null.
return false;
}
// Equals handles case of null on right side.
return lhs.Equals(rhs);
}
public static bool operator !=(TwoDPoint? lhs, TwoDPoint? rhs) => !(lhs == rhs);
}
Türetilen sınıf eşitlik mantığını doğru genişletir:
// For the sake of simplicity, assume a ThreeDPoint IS a TwoDPoint.
class ThreeDPoint : TwoDPoint, IEquatable<ThreeDPoint>
{
public int Z { get; private set; }
public ThreeDPoint(int x, int y, int z)
: base(x, y)
{
if ((z < 1) || (z > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.Z = z;
}
public override bool Equals(object? obj) => Equals(obj as ThreeDPoint);
// Explicit interface implementation prevents compile-time type issues
bool IEquatable<ThreeDPoint>.Equals(ThreeDPoint? p) => Equals((object?)p);
protected override bool Equals(TwoDPoint? p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// Runtime type check happens in the base method
if (p is ThreeDPoint threeD)
{
// Check properties that this class declares.
if (Z != threeD.Z)
{
return false;
}
return base.Equals(p);
}
return false;
}
public override int GetHashCode() => (X, Y, Z).GetHashCode();
public static bool operator ==(ThreeDPoint? lhs, ThreeDPoint? rhs)
{
if (lhs is null)
{
if (rhs is null)
{
// null == null = true.
return true;
}
// Only the left side is null.
return false;
}
// Equals handles the case of null on right side.
return lhs.Equals(rhs);
}
public static bool operator !=(ThreeDPoint? lhs, ThreeDPoint? rhs) => !(lhs == rhs);
}
Bu uygulama sorunlu polimorfik senaryoları şu şekilde işler:
Console.WriteLine("=== Safe Polymorphic Equality ===");
// Test polymorphic scenarios that were problematic before
TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
TwoDPoint p3 = new ThreeDPoint(1, 2, 3);
TwoDPoint p4 = new TwoDPoint(1, 2);
Console.WriteLine("Testing polymorphic equality (declared as TwoDPoint):");
Console.WriteLine($"p1 = ThreeDPoint(1, 2, 3) as TwoDPoint");
Console.WriteLine($"p2 = ThreeDPoint(1, 2, 4) as TwoDPoint");
Console.WriteLine($"p3 = ThreeDPoint(1, 2, 3) as TwoDPoint");
Console.WriteLine($"p4 = TwoDPoint(1, 2)");
Console.WriteLine();
Console.WriteLine($"p1.Equals(p2) = {p1.Equals(p2)}"); // False - different Z values
Console.WriteLine($"p1.Equals(p3) = {p1.Equals(p3)}"); // True - same values
Console.WriteLine($"p1.Equals(p4) = {p1.Equals(p4)}"); // False - different types
Console.WriteLine($"p4.Equals(p1) = {p4.Equals(p1)}"); // False - different types
Console.WriteLine();
Uygulama, doğrudan tür karşılaştırmalarını da doğru şekilde işler:
// Test direct type comparisons
var point3D_A = new ThreeDPoint(3, 4, 5);
var point3D_B = new ThreeDPoint(3, 4, 5);
var point3D_C = new ThreeDPoint(3, 4, 7);
var point2D_A = new TwoDPoint(3, 4);
Console.WriteLine("Testing direct type comparisons:");
Console.WriteLine($"point3D_A.Equals(point3D_B) = {point3D_A.Equals(point3D_B)}"); // True
Console.WriteLine($"point3D_A.Equals(point3D_C) = {point3D_A.Equals(point3D_C)}"); // False
Console.WriteLine($"point3D_A.Equals(point2D_A) = {point3D_A.Equals(point2D_A)}"); // False
Console.WriteLine($"point2D_A.Equals(point3D_A) = {point2D_A.Equals(point3D_A)}"); // False
Console.WriteLine();
Eşitlik uygulaması koleksiyonlarla da düzgün çalışır:
// Test with collections
Console.WriteLine("Testing with collections:");
var hashSet = new HashSet<TwoDPoint> { p1, p2, p3, p4 };
Console.WriteLine($"HashSet contains {hashSet.Count} unique points"); // Should be 3: one ThreeDPoint(1,2,3), one ThreeDPoint(1,2,4), one TwoDPoint(1,2)
var dictionary = new Dictionary<TwoDPoint, string>
{
{ p1, "First 3D point" },
{ p2, "Second 3D point" },
{ p4, "2D point" }
};
Console.WriteLine($"Dictionary contains {dictionary.Count} entries");
Console.WriteLine($"Dictionary lookup for equivalent point: {dictionary.ContainsKey(new ThreeDPoint(1, 2, 3))}"); // True
Yukarıdaki kod, değer tabanlı eşitlik uygulamak için temel öğeleri gösterir:
-
Sanal
Equals(object?)geçersiz kılma: Ana eşitlik mantığı, derleme zamanı türünden bağımsız olarak çağrılan sanal Object.Equals yöntemde gerçekleşir. -
Çalışma zamanı türü denetimi: Kullanmak
this.GetType() != p.GetType(), farklı türlerdeki nesnelerin hiçbir zaman eşit kabul edilmemesini sağlar. - Açık arabirim uygulaması: Uygulama System.IEquatable<T> , derleme zamanı türü seçimi sorunlarını önleyerek sanal yöntemi temsil eder.
-
Korumalı sanal yardımcı yöntemi: yöntemi türetilmiş
protected virtual Equals(TwoDPoint? p)sınıfların tür güvenliğini korurken eşitlik mantığını geçersiz kılmasına olanak tanır.
Bu düzeni aşağıdaki durumlarda kullanın:
- Değer eşitliğinin önemli olduğu devralma hiyerarşileriniz var
- Nesneler çok biçimli olarak kullanılabilir (temel tür olarak bildirilir, türetilmiş tür olarak örneği oluşturulur)
- Değer eşitliği semantiğine sahip başvuru türlerine ihtiyacınız var
Tercih edilen yaklaşım, değer tabanlı eşitlik uygulamak için türleri kullanmaktır record . Bu yaklaşım, standart yaklaşımdan daha karmaşık bir uygulama gerektirir ve doğruluğu sağlamak için çok biçimli senaryoların kapsamlı bir şekilde test edilmesini gerektirir.
Yapı örneği
Aşağıdaki örnek, bir yapıda değer eşitliğinin (değer türü) nasıl uygulanacaklarını gösterir. Yapılar varsayılan değer eşitliğine sahip olsa da, özel bir uygulama performansı geliştirebilir:
namespace ValueEqualityStruct
{
struct TwoDPoint : IEquatable<TwoDPoint>
{
public int X { get; private set; }
public int Y { get; private set; }
public TwoDPoint(int x, int y)
: this()
{
if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
X = x;
Y = y;
}
public override bool Equals(object? obj) => obj is TwoDPoint other && this.Equals(other);
public bool Equals(TwoDPoint p) => X == p.X && Y == p.Y;
public override int GetHashCode() => (X, Y).GetHashCode();
public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs) => lhs.Equals(rhs);
public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
}
class Program
{
static void Main(string[] args)
{
TwoDPoint pointA = new TwoDPoint(3, 4);
TwoDPoint pointB = new TwoDPoint(3, 4);
int i = 5;
// True:
Console.WriteLine($"pointA.Equals(pointB) = {pointA.Equals(pointB)}");
// True:
Console.WriteLine($"pointA == pointB = {pointA == pointB}");
// True:
Console.WriteLine($"object.Equals(pointA, pointB) = {object.Equals(pointA, pointB)}");
// False:
Console.WriteLine($"pointA.Equals(null) = {pointA.Equals(null)}");
// False:
Console.WriteLine($"(pointA == null) = {pointA == null}");
// True:
Console.WriteLine($"(pointA != null) = {pointA != null}");
// False:
Console.WriteLine($"pointA.Equals(i) = {pointA.Equals(i)}");
// CS0019:
// Console.WriteLine($"pointA == i = {pointA == i}");
// Compare unboxed to boxed.
System.Collections.ArrayList list = new System.Collections.ArrayList();
list.Add(new TwoDPoint(3, 4));
// True:
Console.WriteLine($"pointA.Equals(list[0]): {pointA.Equals(list[0])}");
// Compare nullable to nullable and to non-nullable.
TwoDPoint? pointC = null;
TwoDPoint? pointD = null;
// False:
Console.WriteLine($"pointA == (pointC = null) = {pointA == pointC}");
// True:
Console.WriteLine($"pointC == pointD = {pointC == pointD}");
TwoDPoint temp = new TwoDPoint(3, 4);
pointC = temp;
// True:
Console.WriteLine($"pointA == (pointC = 3,4) = {pointA == pointC}");
pointD = temp;
// True:
Console.WriteLine($"pointD == (pointC = 3,4) = {pointD == pointC}");
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
pointA.Equals(pointB) = True
pointA == pointB = True
Object.Equals(pointA, pointB) = True
pointA.Equals(null) = False
(pointA == null) = False
(pointA != null) = True
pointA.Equals(i) = False
pointE.Equals(list[0]): True
pointA == (pointC = null) = False
pointC == pointD = True
pointA == (pointC = 3,4) = True
pointD == (pointC = 3,4) = True
*/
}
Yapılar için, Object.Equals(Object)'ın varsayılan uygulaması (bu, System.ValueType içindeki geçersiz kılınan sürümdür), türdeki her alanın değerlerini yansımayı kullanarak karşılaştırarak bir değer eşitliği denetimi gerçekleştirir. Bu uygulama doğru sonuçlar üretse de, türü için özel olarak yazdığınız özel bir uygulamayla karşılaştırıldığında nispeten yavaştır.
Bir yapıda sanal Equals yöntemi geçersiz kıldığınızda, amaç değer eşitliği denetimini gerçekleştirmek için daha verimli bir yöntem sağlamak ve isteğe bağlı olarak karşılaştırmayı yapı alanlarının veya özelliklerinin bazı alt kümesine dayandırmaktır.
== ve != işleçleri, yapı açıkça aşırı yüklemediği sürece bir yapı üzerinde çalışamaz.