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:
- override
Equals
and GetHashCode
- provide
==
and !=
operators
- implement
IEquatable<T>
- Do this on all classes you want to have a custom implementation of equality.
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:
- For reference types, the default way the compiler considers things equal is to look at their references. If the references are the same, then the things are considered the same. To put it differently suppose you create 2 instances of an Employe class and feed them with exactly the same
Name
and Salary
. Because they are distinct objects (with distinct places in memory, that is different references), emp1 == emp2
will return false
.
- In the case of value types (suppose
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).