6、区别各种不同的Equal方法
C#提供了以下四种方法来判断两个对象是否相等:
1.static bool ReferenceEquals(object left, object right);
2.static bool Equals(object left, object right);
3.virtual bool Equals(object right);
4.static bool operator ==(MyClass left, MyClass right);
当实现你自己的类的相等方法时,一般重写第3个方法,在判断有值类型的相等时重写第4个方法,第1、2个方法永远不要重写它。
对象的相等应当满足数学上相等的几个性质:1.自身等于自身。2.可传递性,即a==b,b==c,那么a==c。3.对称性,即a==b,那么b==a。
第一个方法ReferenceEquals,在当两个变量指向同一个对象的时候返回True,否则返回False.
该方法只比较对象的标识,而不会比较对象的内容,所以当该方法应用在值类型的变量时,永远返回False.--(因为此时会为每个值类型变量产生装箱操作生成不同的对象),同时该方法执行的效率最块,因为它只比较对象标识。
第二个方法Equals的实现就和如下代码一样:
public static new bool Equals(object left, object right)
{
// Check object identity
if (Object.ReferenceEquals(left, right) )
return true;
// both null references handled above
if (Object.ReferenceEquals(left, null) ||
Object.ReferenceEquals(right, null))
return false;
return left.Equals(right);
}
注意System.ValueType未重写1、2方法,只重写了第3个方法,故自定义值类型,也不建议重写这两个方法。(自定义值类型一般也会重写第4个方法==)
但是重写的Equals方法比较糟糕,因为它要比较所有的成员变量,而它又不知道这些成员的具体类型,所以在这里它使用了反射技术,众所周知值类型的反射是比较耗性能的(存在装箱、拆箱操作)。因此我们在定义自己的值类型的时候,应当重写Equals方法,这样会提高效率。
而引用类型,尽量使用预定义的相等方法,在当你重写了Equals()时,你将也需要实现IEquatable<T>接口,因为不实现泛型接口,就会存在一个类型转换的问题,派生类转换为基类可能正常,也就是派生类对象==基类对象,但是基类无法转换为派生类,所以基类对象不等于派生类对象,因此违背了数学意义上的相等。所以我们应当通过实现IEquatable<T>接口方法,来让基类和派生类都转换为接口对象,这样它们就能相互进行转换再判断是否相等了。
自定义引用类型一般不建议重写==方法。
7.GetHashCode()的陷阱
GetHashCode()只用在一个地方:在基于Hash的集合里为Key定义Hash值,通常是在HashSet<T>和Dictionary<K,V>容器里。
GetHashCode()存在不少问题,对于引用类型,它效率低下;对于值类型,它通常又是不正确的。因此不建议重写此函数,而且在使用这个函数时也需要加倍小心。
8.偏爱查询语法而不是循环
对比下面两块代码:
使用循环给数组赋值并输出
int[] foo = new int[100];
for(int num = 0; num < foo.Length; num++)
foo[num] = num * num;
foreach (int i in foo)
Console.WriteLine(i.ToString());
使用查询语法(LINQ)赋值及Lambda表达式来输出
int[] foo = (from n in Enumerable.Range(0, 100)
select n * n).ToArray();
foo.ForAll((n) => Console.WriteLine(n.ToString()));
其中ForAll是在List<T>中实现的,这里我们需要简单的扩展一下:
public static class Extensions
{
public static void ForAll<T>(this IEnumerable<T> sequence,Action<T> action)
{
foreach (T item in sequence)
action(item);
}
}
完整程序如下:
using System;
using System.Diagnostics.Contracts;
using System.Collections.Generic;
using System.Linq;
public static class Extensions
{
public static void ForAll<T>(
this IEnumerable<T> sequence,
Action<T> action)
{
foreach (T item in sequence)
action(item);
}
}
class App
{
static void Main()
{
int[] foo = (from n in Enumerable.Range(0, 100)
select n * n).ToArray();
foo.ForAll((n) => Console.WriteLine(n.ToString()));
Console.ReadKey();
}
}
上面是简单的一组数组创建及输出,似乎看不出查询语法和循环的区别,下面来对比二维数组的排序操作:
使用循环创建二维数组后排序如下:
private static IEnumerable<Tuple<int, int>> ProduceIndices()
{
var storage = new List<Tuple<int, int>>();
for (int x = 0; x < 100; x++)
for (int y = 0; y < 100; y++)
if (x + y < 100)
storage.Add(Tuple.Create(x, y));
storage.Sort((point1, point2) =>
(point2.Item1 * point2.Item1 +point2.Item2 * point2.Item2).CompareTo(
point1.Item1 * point1.Item1 +point1.Item2 * point1.Item2));
return storage;
}
而使用查询语法的排序如下:
private static IEnumerable<Tuple<int, int>> QueryIndices()
{
return from x in Enumerable.Range(0, 100)
from y in Enumerable.Range(0, 100)
where x + y < 100
orderby (x * x + y * y) descending
select Tuple.Create(x, y);
}
现在很明显了,查询语法很简单的实现了复杂的排序操作,而循环却显得很冗长,也不具有可读性。
9、在你的API中应避免用户自定义类型的转换
自定义类型转换如下:(隐式转换)
static public implicit operator Ellipse(Circle c)
{
return new Ellipse(c.center, c.center,
c.radius, c.radius);
}
此时你可以隐式的将Circle对象转换为Ellipse对象。这种隐式转换是自动的,然后执行下面的方法:
public static void Flatten(Ellipse e)
{
e.R1 /= 2;
e.R2 *= 2;
}
// call it using a circle:
Circle c = new Circle(new PointF(3.0f, 0), 5.0f);
Flatten(c);
此时程序虽能正常进行转换,但产生的临时对象(e)被修改了从而成为了垃圾,而原对象却没得到修改。
而且一旦一个对象能转换为另一个对象,那么就是说彼此对象的内部成员可以得到访问,从而失去了类的封装性。
10、使用可选参数来减少方法的重载
带缺省值的命名参数即是可选参数,你在调用方法时,可以只指定你需要用的参数。这显然比多个重载方法要方便。实际上,使用4个可选参数的方法,如果用重载来完成将会需要15个不同的重载.改善C#编程的50个建议(6-10),布布扣,bubuko.com
改善C#编程的50个建议(6-10)
原文:http://blog.csdn.net/edcvf3/article/details/21755703