建议10: 创建对象时需要考虑是否实现比较器
有对象的地方就会存在比较,在.NET的世界中也一样。举个最简单的例子,在UI中,有一个10个人的Salary列表。根据排序的需要,列表要支持针对基本工资来罗列Salary。这个时候,接口IComparable就会起作用,代码如下所示:
- class Salary : IComparable
- {
- public string Name { get; set; }
- public int BaseSalary { get; set; }
- public int Bonus { get; set; }
- #region IComparable 成员
- public int CompareTo(object obj)
- {
- Salary staff = obj as Salary;
- if (BaseSalary > staff.BaseSalary)
- {
- return 1;
- }
- else if (BaseSalary == staff.BaseSalary)
- {
- return 0;
- }
- else
- {
- return -1;
- }
- //return BaseSalary.CompareTo(staff.BaseSalary);
- }
- #endregion
- }
注意 上面代码中CompareTo方法有一条注释的代码,其实本方法完全可以使用该注释代码代替,因为利用了整型的默认比较方法。此处未使用本注释代码,是为了更好地说明比较器的工作原理。
实现了接口IComparable后,我们就可以根据BaseSalary对Salary进行排序了,代码如下所示:
- ArrayList companySalary = new ArrayList();
- companySalary.Add(new Salary() { Name = "Mike", BaseSalary = 3000 });
- companySalary.Add(new Salary() { Name = "Rose", BaseSalary = 2000 });
- companySalary.Add(new Salary() { Name = "Jeffry", BaseSalary = 1000 });
- companySalary.Add(new Salary() { Name = "Steve", BaseSalary = 4000 });
- companySalary.Sort();
- foreach (Salary item in companySalary)
- {
- Console.WriteLine(item.Name + "\t BaseSalary: " + item.BaseSalary.ToString());
- }
上面代码的输出如下:
- Jeffry BaseSalary: 1000
- Rose BaseSalary: 2000
- Mike BaseSalary: 3000
- Steve BaseSalary: 4000
现在,问题来了:如果不想以基本工资BaseSalary进行排序,而是以奖金Bonus进行排序,该如何处理呢?这个时候,接口IComparer的作用就体现出来了,可以使用IComparer来实现一个自定义的比较器。如下所示:
- class BonusComparer : IComparer
- {
- #region IComparer 成员
- public int Compare(object x, object y)
- {
- Salary s1 = x as Salary;
- Salary s2 = y as Salary;
- return s1.Bonus.CompareTo(s2.Bonus);
- }
- #endregion
- }
我们在排序的时候为Sort方法提供此比较器,代码如下所示:
- ArrayList companySalary = new ArrayList();
- companySalary.Add(new Salary() { Name = "Mike", BaseSalary = 3000,
- Bonus = 1000 });
- companySalary.Add(new Salary() { Name = "Rose", BaseSalary = 2000,
- Bonus = 4000 });
- companySalary.Add(new Salary() { Name = "Jeffry", BaseSalary = 1000,
- Bonus = 6000 });
- companySalary.Add(new Salary() { Name = "Steve", BaseSalary = 4000,
- Bonus = 3000 });
- companySalary.Sort(new BonusComparer()); //提供
- //一个非默认的比较器
- foreach (Salary item in companySalary)
- {
- Console.WriteLine(string.Format("Name:{0} \tBaseSalary:{1} \tBonus:{2}",
- item.Name, item.BaseSalary, item.Bonus));
- }
输出结果如下:
- Name:Mike BaseSalary:3000 Bonus:1000
- Name:Steve BaseSalary:4000 Bonus:3000
- Name:Rose BaseSalary:2000 Bonus:4000
- Name:Jeffry BaseSalary:1000 Bonus:6000
如果我们稍有经验,就会发现上面的代码使用了一个已经不建议使用的集合类ArrayList(当泛型出来后,就建议尽量不使用所有非泛型集合类)。至于原因,从上面的代码中我们也可以看出端倪。 注意查看代码中的Compare函数,如:
- public int Compare(object x, object y)
- {
- Salary s1 = x as Salary;
- Salary s2 = y as Salary;
- return s1.Bonus.CompareTo(s2.Bonus);
- }
我们发现这个函数进行了转型,这是会影响性能的。如果集合中有成千上万个复杂的实体对象,在排序的时候所耗费掉的性能就是可观的;而泛型的出现,可以避免运行时转型。 因此,以上代码中的ArrayList,应该换成List,对应地,我们就该实现IComparable和IComparer。最终的代码应该像下面这样:
- static void Main(string[] args)
- {
- List<Salary> companySalary = new List<Salary>()
- {
- new Salary() { Name = "Mike", BaseSalary = 3000, Bonus = 1000 },
- new Salary() { Name = "Rose", BaseSalary = 2000, Bonus = 4000 },
- new Salary() { Name = "Jeffry", BaseSalary = 1000, Bonus = 6000 },
- new Salary() { Name = "Steve", BaseSalary = 4000, Bonus = 3000 }
- };
- companySalary.Sort(new BonusComparer()); //提供一个非默认的比较器
- foreach (Salary item in companySalary)
- {
- Console.WriteLine(string.Format("Name:{0} \tBaseSalary:{1} \tBonus:{2}",
- item.Name, item.BaseSalary, item.Bonus));
- }
- }
- class Salary : IComparable<Salary>
- {
- public string Name { get; set; }
- public int BaseSalary { get; set; }
- public int Bonus { get; set; }
- #region IComparable<Salary> 成员
- public int CompareTo(Salary other)
- {
- return BaseSalary.CompareTo(other.BaseSalary);
- }
- #endregion
- }
- class BonusComparer : IComparer<Salary>
- {
- #region IComparer<Salary> 成员
- public int Compare(Salary x, Salary y)
- {
- return x.Bonus.CompareTo(y.Bonus);
- }
- #endregion
- }
建议11: 区别对待==和Equals
在开始本建议之前,首先要明确概念“相等性”。CLR中将“相等性”分为两类:“值相等性”和“引用相等性”。如果用来比较的两个变量所包含的数值相等,那么将其定义为“值相等性”;如果比较的两个变量引用的是内存中的同一个对象,那么将其定义为“引用相等性”。
无论是操作符“==”还是方法“Equals”,都倾向于表达这样一个原则:
对于值类型,如果类型的值相等,就应该返回True。
对于引用类型,如果类型指向同一个对象,则返回True。
下面的代码输出所遵循的就是以上原则:
- static void ValueTypeOPEquals()
- {
- int i = 1;
- int j = 1;
- //True
- Console.WriteLine(i == j);
- j = i;
- //True
- Console.WriteLine(i == j);
- }
- static void ReferenceTypeOPEquals()
- {
- object a = 1;
- object b = 1;
- //False
- Console.WriteLine(a == b);
- b = a;
- //True
- Console.WriteLine(a == b);
- }
- static void ValueTypeEquals()
- {
- int i = 1;
- int j = 1;
- //True
- Console.WriteLine(i.Equals(j));
- j = i;
- //True
- Console.WriteLine(i.Equals(j));
- }
- static void ReferenceTypeEquals()
- {
- object a = new Person("NB123");
- object b = new Person("NB123");
- //False
- Console.WriteLine(a.Equals(b));
- b = a;
- //True
- Console.WriteLine(a.Equals(b));
- }
但是,我们同时也要了解,无论是操作符“==”还是“Equals”方法都是可以被重载的。比如,对于string这样一个特殊的引用类型,微软觉得它的现实意义更接近于值类型,所以,在FCL中,string的比较被重载为针对“类型的值”的比较,而不是针对“引用本身”的比较。
从设计上来说,很多自定义的类型(尤其是自定义的引用类型)会存在和string类型比较接近的情况。如例子中所举的类型Person,在现实生活中,如果两者的IDCode是相等的,我们就认为两者是同一个人,这个时候,就要重载Equals这个方法,代码如下所示:
- class Person
- {
- public string IDCode { get; private set; }
- public Person(string idCode)
- {
- this.IDCode = idCode;
- }
- public override bool Equals(object obj)
- {
- return IDCode == (obj as Person).IDCode;
- }
- }
这时,再通过Equals去比较两个具有相同IDCode的Person对象的值,返回的就会是true,代码如下所示:
- object a = new Person("NB123");
- object b = new Person("NB123");
- //False
- Console.WriteLine(a == b);
- // True
- Console.WriteLine(a.Equals(b));
这里,再引出操作符“==”和“Equals”方法之间的一点区别。一般来说,对于引用类型,我们要定义“值相等性”,应该仅仅去重载Equals方法,同时让“==”表示“引用相等性”。
注意 由于操作符“==”和“Equals”方法从语法实现上来说,都可以被重载为表示“值相等性”和“引用相等性”。所以,为了明确有一种方法肯定比较的是“引用相等性”,FCL中提供了Object. ReferenceEquals方法。该方法比较的是:两个示例是否是同一个示例。