Reputation: 17
This might be a beginners question, but I have a problem for which I could not find an existing solution and was hoping you could help me here.
I would like to compare instances of a reference type object by value in without manually overwriting the .Equals()
method in this class and its children. That is because I not want to write all that code in the class and especially it's many children, as it would add a lot of complexity to an already huge codebase. Is there a way to compare two objects of such a class (or its children) without having to write the implementation for every single class?
A record is not an option as they need to stay mutable. A struct is not possible as I would still like to have the objects be reference-type as they are passed around quite a bit.
Ideally I would also love to be able to only use the 'value-type-comparison' in some cases while leaving the 'reference-type-comparison' as the default.
Is there way to natively do this in c# without writing an additional function / overwriting .Equals()
and .GetHash()
for each class inheriting from the class in question?
My code looks somewhat similar to this:
using System;
namespace SubwaySystem{
public abstract class Passenger {
public string name;
public int identificationNumeber;
public int age;
}
public class SingleTicketPassenger : Passenger
{
public string ticketModel;
public string ticketprice;
}
public class SubscriptionPassenger : Passenger
{
public DateTime subscriptionExpiryDate;
public int subscriptionPrice;
public string subscriptionModel;
}
public class GateHopper : Passenger
{
public string entryStation;
public string? arrestRecordID;
public bool firstOffence;
}
// many more classes inherenting from Passenger
public class Subway {
public Passenger oldestPassengerOnBoard;
public string model;
public string line;
public DateTime constructionDate;
public int speed;
public override bool Equals(object? obj)
{
var otherSubway = obj as Subway;
return this.Equals(otherSubway);
}
public bool Equals(Subway otherSubway)
{
return otherSubway != null &&
otherSubway.oldestPassengerOnBoard == oldestPassengerOnBoard &&
otherSubway.model == model &&
otherSubway.line == line &&
otherSubway.constructionDate == constructionDate &&
otherSubway.speed == speed;
}
public override int GetHashCode()
{
unchecked
{
int hash = 7;
hash = hash * 11 + oldestPassengerOnBoard.GetHashCode();
hash = hash * 11 + speed.GetHashCode();
return hash;
}
}
}
}
What I want to do is make sure that two Subways are not only equal if the oldest passenger is equal by reference, but also if they are by value. (In the actual code it makes more sense, just bare with me in this example and assume that two people are the same person if they have the same properties). The Hash code of the Passenger would of course also have to be dependent on only their proprties.
In the end I basically would love a mutable record. Or something like a .CompareByValue()
method on generic objects.
Upvotes: 0
Views: 133
Reputation: 4837
For performance reasons you can make use of the Equ library (mentioned as an answer to the question referred to by dbc in the comments). The library essentially makes use of compiling expressions, so that you end up using reflection just once to figure out what the fields are. Afterwards, it's (almost?) as fast as the real thing.
That answer does not exactly fit your needs because it will make you inherit the library's MemberwiseEquatable<T>
and break your own inheritance hierarchy.
The easiest work-around would be do create an extension method for classes EqualByFieldsValue<T>
that makes use of the lazily created MemberwiseEqualityComparer<T>.ByFields
(there is also MemberwiseEqualityComparer<T>.ByProperties
in the library)
using Equ;
public static class EqualExtensions {
public static bool EqualByFieldsValue<T>(this T x, T y, bool exactTypeCheck = true)
where T : class {
if (x == y) return true;
if (x == null || y == null) return false;
// assure exact same type
// otherwise we could compare just the fields of the base class
// between two derived classes/or base+derived
if (exactTypeCheck && x.GetType() != y.GetType()) return false;
// ByFields backing field is Lazily Created Once
return MemberwiseEqualityComparer<T>.ByFields.Equals(x, y);
}
}
An important case to handle is when we have instances of different inherited classes of Passenger
that are held in variables/fields of type Passenger
- this could lead to checking just the common base fields between the two objects for equality. If you find this desirable you can pass exactTypeCheck:false
for the second parameter in the extension method (default value being true
)
Examples:
var passenger1 = new SubscriptionPassenger {
age = 42,
subscriptionPrice = 55
};
var passenger2 = new SubscriptionPassenger {
age = 42,
subscriptionPrice = 55
};
Console.WriteLine(passenger1.EqualByFieldsValue(passenger2)); // True
// Base class reference, same common fields for base class
Passenger passenger3 = new SingleTicketPassenger {
age = 42,
};
Passenger passenger4 = new SubscriptionPassenger {
age = 42,
};
// still false cause different types
Console.WriteLine(passenger3.EqualByFieldsValue(passenger4)); // False
Upvotes: 1
Reputation: 52280
There are a lot of possible edge cases when comparing objects by value (do you compare only properties? only fields? What if the field contains an object? What if the tree of objects inside the object form a loop?) but if all you want to do is compare by public fields, and you're not worried about infinite recursion, you can write a simple extension method that does it for all reference types. Use Reflection to iterate over the fields and get the values, then use the value type's Equals
method or call CompareByValue
recursively if it's a reference type.
static class ComparisonExtensions
{
static public bool CompareByValue<T>(this T source, T other) where T : class
{
var fields = typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public);
foreach (var field in fields)
{
var lhs = field.GetValue(source);
var rhs = field.GetValue(other);
var equal = field.FieldType.IsValueType ? lhs.Equals(rhs): lhs.CompareByValue(rhs);
if (!equal) return false;
}
return true;
}
}
Upvotes: 0