Reputation: 6868
I want to compare 2 objects like below :
I am trying to implement IEqualityComparer to accept generic type argument to take Employee or Animals or any object and to the comparison for me but I ran into a problem because since I have T as an argument, I am not getting the properties on T.
public class GenericComparer<T> : IEqualityComparer<T>
{
public bool Equals(T x, T y)
{
// Not getting a property here T.??
//now if I would have T as an Employee then I could do something like this here:
// return String.Equals(x.Name, y.Name); // but this will work because I know that Name is string
// but lets say if I have a different datatype that I want to compare then how can i do that?
}
public int GetHashCode(T obj)
{
return obj.
}
}
The thing is I don't want to create 2 classes like EmployeeComparer: IEqualityComparer<Employee> and AnimalComparer : IEqualityComparer<Animal>
as the code will be somewhat similar.
Is it possible to create generic compared to compare any objects of the same types?
Update:
I am just trying to understand the limitations of generics with reference types. When should we create a Generic class or methods to accept reference types and when we should not.
I was thinking that since List<T>
can accept anything like List<Employee>
or List<Animal>
or anything then why my GenericComparer cannot.
Because when we create List<Employee>
then we can run for each loop and access the properties right:
foreach(var employee in employees)
{
string name = employee.Name; //Here we are able to access the properties
}
Then how come not in my case?
Answer to all the above questions will help me understand generics better with reference type especially. So, if someone can provide an answer to all this question and why it is not possible with my GenericComaprer<T>
and how it is possible with List<T>
then I will really appreciate :)
Upvotes: -2
Views: 1044
Reputation: 5791
What you describe is easily possible, just use delegates.
public class GenericComparer<T, TValue> : IEqualityComparer<T>
{
private Func<T, TValue> _getter;
private IEqualityComparer<TValue> _valueComparer;
public GenericComparer(Func<T, TValue> getter, IEqualityComparer<TValue> valueComparer = null)
{
_getter = getter;
_valueComparer = valueComparer ?? EqualityComparer<TValue>.Default;
}
public bool Equals(T x, T y)
{
return _valueComparer.Equals(_getter(x), _getter(y));
}
public int GetHashCode(T obj)
{
return _valueComparer.GetHashCode(_getter(obj));
}
}
To use it, simply tell it by which property you want it to compare:
var animalByNameComparer = new GenericComparer<Animal, string>(an => an?.Name);
Of course, you can also handle the null case already in the generic comparer, also.
To your second question
The simple difference between List<T>
and List<Employee>
is that the former is an open generic type while the latter is a closed generic type. If you define a generic type, you always define it as an open generic type, so the compiler does not know what T
will be. If you consume a generic type, you often work with closed generic types, i.e. the placeholder T
already has a value and therefore, the compiler is able to resolve a symbolic name like .Name
to a property. If you use the generic type in an open generic method (a method with a type parameter), you also cannot bind symbols.
Upvotes: 1
Reputation: 1500
What you're expecting of generics just isn't right:
I was thinking that since
List<T>
can accept anything likeList<Employee>
orList<Animal>
or anything then why my GenericComparer cannot.foreach(var employee in employees) { string name = employee.Name; //Here we are able to access the properties }
Here you say that you can access the properties, but that's specifically because the list employees
was instantiated as List<Employee>
. The implementation of List
however could Not access those properties, because it only sees T
!
There are some ways to achieve what you want, but you'd have to consider design and performance pros/cons depending on your specific use-case and needs.
Here's what you could do:
public abstract class CustomComparable
{
// Force implementation to provide a comparison value
public abstract object GetComparisonValue();
}
public class Employee: CustomComparable
{
public int Department { get; set; }
public override object GetComparisonValue()
{
return Department;
}
}
public class Animal : CustomComparable
{
public string Zoo { get; set; }
public override object GetComparisonValue()
{
return Zoo;
}
}
public class CustomComparer<T> : IEqualityComparer<T> where T: CustomComparable
{
public bool Equals(T x, T y)
{
return x != null && y != null && x.GetComparisonValue().Equals(y.GetComparisonValue());
}
public int GetHashCode(T obj)
{
return obj.GetComparisonValue().GetHashCode();
}
}
Then you'd get this, for example:
class Program
{
static void Main(string[] args)
{
Animal cat = new Animal() { Zoo = "cat zoo" };
Animal dog = new Animal() { Zoo = "dog zoo" };
Animal puppy = new Animal() { Zoo = "dog zoo" };
List<Animal> animals = new List<Animal>() { cat, dog, puppy };
CustomComparer<Animal> animalComparer = new CustomComparer<Animal>();
Console.WriteLine($"Distinct zoos ? {animals.Distinct(animalComparer).Count() == animals.Count}");
Employee bob = new Employee() { Department = 1 };
Employee janet = new Employee() { Department = 2 };
Employee paul = new Employee() { Department = 3 };
List<Employee> employees = new List<Employee>() { bob, janet, paul };
CustomComparer<Employee> employeeComparer = new CustomComparer<Employee>();
Console.WriteLine($"Distinct departments ? {employees.Distinct(employeeComparer).Count() == employees.Count}");
}
}
> Distinct zoos ? False
> Distinct departments ? True
With all that being said, do use IEquatable<T>
. The specific use of such equality implementations seems out of the scope of your initial question however, and many other resources and Q&A's can help you with these.
Upvotes: 1
Reputation: 1419
when we create
List<Employee>
then we can run for each loop and access the properties
Sure, because you are using List<T>
by providing it the concrete type (Employee
) to use in place for T
. But if you were to look at the internals of (or try to write your own version of) List<T>
you'd see you have no way to access an hypothetical Name
property of T
. That is because C# is a strictly typed language (contrarily to Javascript or Python for example). It won't let you use anything it doesn't know beforehand and for sure exists.
The way you defined your GenericComparer<T>
class, all the compiler know is that T
can do what every C# objects can, that is... not much (you can use ToString()
and GetType()
and compare references, that's pretty much all).
Then, because otherwise generics wouldn't be very useful, you can specify constraints on your generic type: you can, for example, tell the compiler that T
must implement some interface. If you do this, then you can access any member defined in this interface on your T
instances from inside the generic class.
For the sake of the example, let's say you have employees and animals; both employees and animals have a Name
, then employees also have a Salary
but animals have a Species
. This could look like this:
public class Employee {
public string Name { get; set; }
public double Salary { get; set; }
}
public class Animal {
public string Name { get; set; }
public string Species { get; set; }
}
If we keep it that way, as explained above, you won't be able to access the properties from your generic class. So let's introduce an interface:
public interface IHasName {
string Name { get; }
}
And add : IHasName
at the end of the Employee
and Animal
class declarations.
We also need to tweak GenericComparer
declaration this way:
public class GenericComparer<T> : IEqualityComparer<T> where T : IHasName
By doing this, we tell the compiler that the T
in GenericComparer
must implement IHasName
. The benefit is now you can access the Name
property from within GenericComparer
. On the other side, you won't be able to use GenericComparer
by passing it anything that doesn't implement IHasName
. Also note that the interface only defines the ability to get the Name
property, not to set it. So, although you can of course set the Name
on Employee
and Animal
instances, you won't be able to do so from inside GenericComparer
.
With the definitions above, here is an example of how you could write the GenericComparer<T>.Equals
method:
public bool Equals(T x, T y)
{
// Just a convention: if any of x or y is null, let's say they are not equal
if (x == null || y == null) return false;
// Another convention: let's say an animal and an employee cannot be equal
// even if they have the same name
if (x.GetType() != y.GetType()) return false;
// And now if 2 employees or animals have the same name, we'll say they are equal
if (x.Name == y.Name) return true;
return false;
}
All that said, I don't know exactly what your use case for GenericComparer<T>
is (maybe you need this in a Dictionary
or some other method that requires a way to compare instances... Anyway, as other commenters have stated, the proper way to go would probably be to:
Equals
and GetHashCode
==
and !=
operatorsIEquatable<T>
If you happen to use a recent version of Visual Studio (I'm using VS 2019 16.8 at the moment), you can automatically do all of this by right-clicking the class name, choosing Quick Actions and Refactorings > Generate Equals and GetHashCode. You will be presented with a window that allows you to choose the properties that should be part of the equality logic and you can also optionally choose to implement IEquatable<T>
and generate ==
and !=
. Once done, I'd recommend you review the generated code and make sure you understand what it does.
By the way, if you do so, you'll notice that the generated code uses EqualityComparer<Employee>.Default
. This is pretty much what you tried to implement by yourself with your GenericEqualityComparer
.
Now to the part of your questions relating to reference types. I'm not sure I understand your questions for I don't see an obvious link between generic types and references. Generic types can act just as well on both reference and value types.
What bothers you is maybe the fact that equality does not work the same way in reference and value types:
Name
and Salary
. Because they are distinct objects (with distinct places in memory, that is different references), emp1 == emp2
will return false
.Employee
is a struct
and not anymore a class
), the compiler does something else: it compares all the properties of the struct
and decides based upon their content whether the 2 employees are equal or not. In this case, emp1 == emp2
will return true
. Note that here, the compiler (or rather the .NET runtime) is doing something similar to what you attempt to do with your universal comparer. However, it only does so for value types and it is rather slow (this is why one should often implement IEquatable
and override Equals
and GetHashcode
on structures).Well, I'm not sure I've answered all your questions, but if you want to know more, you should definitely go through some C# tutorials or documentation to understand more about reference vs value types and equality (even before you jump into implementing your own generic types).
Upvotes: 4