尝试创建通用比较器以比较 2 个对象时未获取通用参数 T 的属性
Not getting properties on generic parameter T when trying to create generic comparer to compare 2 objects
我想比较 2 个对象,如下所示:
- 我不希望同一部门有 2 名员工
- 我不想在同一个动物园里放 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; }
}
并在 Employee
和 Animal
class 声明的末尾添加 : IHasName
。
我们还需要这样调整 GenericComparer
声明:
public class GenericComparer<T> : IEqualityComparer<T> where T : IHasName
通过这样做,我们告诉编译器 GenericComparer
中的 T
必须实现 IHasName
。好处是现在您可以从 GenericComparer
中访问 Name
属性。另一方面,您将无法通过传递任何未实现 IHasName
的内容来使用 GenericComparer
。另请注意,该接口仅定义了 get Name
属性 的能力,而不是 set 的能力。因此,虽然您当然可以在 Employee
和 Animal
实例上设置 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
或其他需要比较实例的方法中需要它。 .. 无论如何,正如其他评论者所说,正确的方法可能是:
- 覆盖
Equals
和 GetHashCode
- 提供
==
和 !=
运算符
- 实施
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。通用类型可以在 reference 和 value 类型上发挥同样的作用。
让您困扰的可能是 equality 在引用类型和值类型中的工作方式不同:
- 对于引用类型,编译器认为事物相等的默认方式是查看它们的引用。如果引用相同,则事物被认为是相同的。换句话说,假设您创建了 Employe class 的 2 个实例,并为它们提供完全相同的
Name
和 Salary
。因为它们是不同的对象(在内存中有不同的位置,即不同的引用),emp1 == emp2
将 return false
.
- 在值类型的情况下(假设
Employee
是 struct
而不再是 class
),编译器会做一些其他的事情:它比较 Employee
的所有属性=64=] 并根据他们的内容决定2名员工是否相等。在这种情况下,emp1 == emp2
将 return true
。请注意,在这里,编译器(或者更确切地说是 .NET 运行时)正在执行类似于您尝试使用通用比较器执行的操作。但是,它只对 value 类型这样做,而且速度相当慢(这就是为什么人们应该经常实现 IEquatable
并覆盖 Equals
和 GetHashcode
在结构上)。
好吧,我不确定我是否已经回答了你所有的问题,但如果你想了解更多,你一定要阅读一些 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
这样的符号名称解析为 属性.如果在开放的泛型方法(带类型参数的方法)中使用泛型类型,同样不能绑定符号。
我想比较 2 个对象,如下所示:
- 我不希望同一部门有 2 名员工
- 我不想在同一个动物园里放 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; }
}
并在 Employee
和 Animal
class 声明的末尾添加 : IHasName
。
我们还需要这样调整 GenericComparer
声明:
public class GenericComparer<T> : IEqualityComparer<T> where T : IHasName
通过这样做,我们告诉编译器 GenericComparer
中的 T
必须实现 IHasName
。好处是现在您可以从 GenericComparer
中访问 Name
属性。另一方面,您将无法通过传递任何未实现 IHasName
的内容来使用 GenericComparer
。另请注意,该接口仅定义了 get Name
属性 的能力,而不是 set 的能力。因此,虽然您当然可以在 Employee
和 Animal
实例上设置 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
或其他需要比较实例的方法中需要它。 .. 无论如何,正如其他评论者所说,正确的方法可能是:
- 覆盖
Equals
和GetHashCode
- 提供
==
和!=
运算符 - 实施
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。通用类型可以在 reference 和 value 类型上发挥同样的作用。
让您困扰的可能是 equality 在引用类型和值类型中的工作方式不同:
- 对于引用类型,编译器认为事物相等的默认方式是查看它们的引用。如果引用相同,则事物被认为是相同的。换句话说,假设您创建了 Employe class 的 2 个实例,并为它们提供完全相同的
Name
和Salary
。因为它们是不同的对象(在内存中有不同的位置,即不同的引用),emp1 == emp2
将 returnfalse
. - 在值类型的情况下(假设
Employee
是struct
而不再是class
),编译器会做一些其他的事情:它比较Employee
的所有属性=64=] 并根据他们的内容决定2名员工是否相等。在这种情况下,emp1 == emp2
将 returntrue
。请注意,在这里,编译器(或者更确切地说是 .NET 运行时)正在执行类似于您尝试使用通用比较器执行的操作。但是,它只对 value 类型这样做,而且速度相当慢(这就是为什么人们应该经常实现IEquatable
并覆盖Equals
和GetHashcode
在结构上)。
好吧,我不确定我是否已经回答了你所有的问题,但如果你想了解更多,你一定要阅读一些 C# 教程或文档来了解更多关于引用与值类型和相等性的信息(甚至在你开始实现你自己的泛型类型之前)。
您对泛型的期望是不正确的:
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 }
这里你说你可以访问属性,但那是因为列表 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
这样的符号名称解析为 属性.如果在开放的泛型方法(带类型参数的方法)中使用泛型类型,同样不能绑定符号。