尝试创建通用比较器以比较 2 个对象时未获取通用参数 T 的属性

Not getting properties on generic parameter T when trying to create generic comparer to compare 2 objects

我想比较 2 个对象,如下所示:

我正在尝试实现 IEqualityComparer 以接受泛型类型参数以获取 Employee 或 Animals 或任何对象并为我进行比较,但我 运行 遇到了一个问题,因为因为我有 T 作为参数,我我没有获取 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.
        }
    }

问题是我不想创建 2 个 class 类似 EmployeeComparer: IEqualityComparer<Employee> and AnimalComparer : IEqualityComparer<Animal>,因为代码会有些相似。

是否可以创建泛型比较来比较任何相同类型的对象?

更新:

我只是想了解泛型与引用类型的局限性。我们什么时候应该创建泛型 class 或方法来接受引用类型,什么时候不应该。

我在想,既然 List<T> 可以接受 List<Employee>List<Animal> 之类的任何东西,那么为什么我的 GenericComparer 不能。

因为当我们创建 List<Employee> 时,我们可以 运行 为每个循环访问属性:

foreach(var employee in employees)
{
  string name = employee.Name; //Here we are able to access the properties
}

那我怎么没有呢?

回答以上所有问题将帮助我更好地理解泛型,尤其是引用类型。所以,如果有人可以提供所有这些问题的答案,以及为什么我的 GenericComaprer<T> 不可能以及 List<T> 如何可能,那么我将非常感激 :)

when we create List<Employee> then we can run for each loop and access the properties

当然可以,因为您正在 使用 List<T>,方法是为其提供具体类型 (Employee) 以代替 T 使用。但是,如果您要查看 List<T> 的内部结构(或尝试编写您自己的版本),您会发现您无法访问 [=] 的假设 Name 属性 17=]。这是因为 C# 是一种严格类型化的语言(例如,与 Javascript 或 Python 相反)。它不会让你使用任何它事先不知道但肯定存在的东西。

你定义 GenericComparer<T> class 的方式,所有编译器都知道 T 可以做每个 C# 对象可以做的事情,也就是说......不多(你可以使用 ToString()GetType() 并比较引用,差不多就这些了)。

然后,因为否则泛型不会非常有用,您可以在泛型类型上指定 constraints:例如,您可以告诉编译器 T 必须实现一些接口。如果这样做,那么您可以从通用 class.

内部访问 T 实例上此接口中定义的任何成员

为举例起见,假设您有员工和动物;员工和动物都有 Name,然后员工也有 Salary,但动物有 Species。这可能看起来像这样:

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; }
}

如果我们保持这种方式,如上所述,您将无法从通用 class 访问属性。那么我们来介绍一个接口:

public interface IHasName {
    string Name { get; }
}

并在 EmployeeAnimal class 声明的末尾添加 : IHasName

我们还需要这样调整 GenericComparer 声明:

public class GenericComparer<T> : IEqualityComparer<T> where T : IHasName

通过这样做,我们告诉编译器 GenericComparer 中的 T 必须实现 IHasName。好处是现在您可以从 GenericComparer 中访问 Name 属性。另一方面,您将无法通过传递任何未实现 IHasName 的内容来使用 GenericComparer。另请注意,该接口仅定义了 get Name 属性 的能力,而不是 set 的能力。因此,虽然您当然可以在 EmployeeAnimal 实例上设置 Name,但您无法从 GenericComparer.

内部执行此操作

根据上面的定义,下面是如何编写 GenericComparer<T>.Equals 方法的示例:

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;
}

综上所述,我不确切知道 GenericComparer<T> 的用例是什么(也许您在 Dictionary 或其他需要比较实例的方法中需要它。 .. 无论如何,正如其他评论者所说,正确的方法可能是:

  • 覆盖 EqualsGetHashCode
  • 提供 ==!= 运算符
  • 实施IEquatable<T>
  • 在所有 class 上执行此操作以自定义实现相等性。

如果你碰巧使用的是最新版本的Visual Studio(我目前使用的是VS 2019 16.8),你可以通过右键单击class名称自动完成所有这些,选择 Quick Actions and Refactorings > Generate Equals and GetHashCode。您将看到一个 window,它允许您选择应该成为相等逻辑一部分的属性,您还可以选择实现 IEquatable<T> 并生成 ==!=.完成后,我建议您查看生成的代码并确保您了解它的作用。

顺便说一下,如果这样做,您会注意到生成的代码使用 EqualityComparer<Employee>.Default。这几乎就是您尝试使用 GenericEqualityComparer.

自己实现的内容

现在回答与 reference 类型相关的问题部分。我不确定我是否理解您的问题,因为我没有看到泛型类型和引用之间有明显的 link。通用类型可以在 referencevalue 类型上发挥同样的作用。

让您困扰的可能是 equality 在引用类型和值类型中的工作方式不同:

  • 对于引用类型,编译器认为事物相等的默认方式是查看它们的引用。如果引用相同,则事物被认为是相同的。换句话说,假设您创建了 Employe class 的 2 个实例,并为它们提供完全相同的 NameSalary。因为它们是不同的对象(在内存中有不同的位置,即不同的引用),emp1 == emp2 将 return false.
  • 在值类型的情况下(假设 Employeestruct 而不再是 class),编译器会做一些其他的事情:它比较 Employee 的所有属性=64=] 并根据他们的内容决定2名员工是否相等。在这种情况下,emp1 == emp2 将 return true。请注意,在这里,编译器(或者更确切地说是 .NET 运行时)正在执行类似于您尝试使用通用比较器执行的操作。但是,它只对 value 类型这样做,而且速度相当慢(这就是为什么人们应该经常实现 IEquatable 并覆盖 EqualsGetHashcode在结构上)。

好吧,我不确定我是否已经回答了你所有的问题,但如果你想了解更多,你一定要阅读一些 C# 教程或文档来了解更多关于引用与值类型和相等性的信息(甚至在你开始实现你自己的泛型类型之前)。

您对泛型的期望是不正确的:

I was thinking that since List<T> can accept anything like List<Employee> or List<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
}

这里你说你可以访问属性,但那是因为列表 employees 被实例化为 List<Employee>。然而 List 的实现无法访问这些属性,因为它只看到 T !


有一些方法可以实现您想要的,但您必须根据您的具体用例和需求考虑设计和性能pros/cons。

您可以执行以下操作:

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();
    }
}

然后你会得到这个,例如:

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

话虽如此,请务必使用 IEquatable<T>。然而,这种平等实施的具体使用似乎超出了您最初问题的范围,许多其他资源和问答可以帮助您解决这些问题。

您描述的很容易实现,只需使用委托即可。

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));
    }
}

要使用它,只需告诉它您希望它与哪个 属性 进行比较:

var animalByNameComparer = new GenericComparer<Animal, string>(an => an?.Name);

当然,您也可以处理通用比较器中已有的 null 情况。

关于你的第二个问题 List<T>List<Employee>的简单区别是前者是开放泛型,后者是封闭泛型。如果你定义一个泛型类型,你总是将它定义为一个开放的泛型类型,所以编译器不知道 T 会是什么。如果您使用泛型类型,您通常会使用封闭的泛型类型,即占位符 T 已经有一个值,因此,编译器能够将像 .Name 这样的符号名称解析为 属性.如果在开放的泛型方法(带类型参数的方法)中使用泛型类型,同样不能绑定符号。