章节索引

建议11:区别对待 == 和Equals

建议12:重写Equals也要重写GetHashCode

建议13:为类型输出格式化字符串

建议14:正确实现浅拷贝和深拷贝

建议15:使用dynamic来简化反射实现

建议16:元素数量可变的情况下不应使用数组

建议17:多数情况下使用foreach进行循环遍历

建议18:foreach不能代替for

建议19:使用更有效的对象和集合初始化

建议20:使用泛型集合代替非泛型集合

建议11:区别对待 == 和Equals

CLR中将“相等性”分为两类:
1、值相等性:两个变量包含的数值相等。
2、引用相等性:两个变量引用的是内存中的同一个对象。

但并不是所有的类型的比较都是按照其本身,比如string是一个特殊的引用类型,但是在FCL中,string的比较就被重载为针对“类型的值”的比较,而不是“引用本身”的比较。对于自定义类型来说,如果想要实现这样的值比较而不是引用比较的话,则需要重载Equals方法,比如对于Person类,如果IDCode相同,我们可以认为他们是同一个人。

 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去比较的话,则就会通过重载后的方法来进行了。

 object a = new Person("ABC");
object b = new Person("ABC");
Console.WriteLine(a == b); //False
Console.WriteLine(a.Equals(b)); //True

说到这里,作者依然没说白“==”和“Equals”的区别,只是说了一句建议的话:“对于引用类型,我们要定义值相等性,应该仅仅去重载Equals方法,同时让==表示引用相等性”
同时,为了明确有一种方法来肯定比较的是“引用相等性”,FCL提供了Object.ReferenceEquals方法。

bool equal= object.ReferenceEquals(object a,object b);

外事不决问Google,内事不决靠反编译、MSDN了。为了弄懂==和Equals的区别,我作如下搜集整理:
1、==是运算符,而Equals是方法;
2、对于值类型、string类型,==和Equals都是比较值内容相等,使用ILSpy对Int类型进行反编译观察;int类型中的Equals方法内部逻辑就是“==”;

// int
[__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
public bool Equals(int obj)
{
return this == obj;
}

string类型则是判断引用地址是否相同或者值内容是否相同,两者有一个符合条件则视为“相等”,请看string类的反编译代码。

// string
[__DynamicallyInvokable, ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail), TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
public bool Equals(string value)
{
if (this == null)
{
throw new NullReferenceException();
}
return value != null && (object.ReferenceEquals(this, value) || (this.Length == value.Length && string.EqualsHelper(this, value))); }

3、对于引用类型,==和Equals都是比较栈内存中的地址是否相等,并且自定义类型中可以进行运算符重载== 或者Override Equals 来改写认为两对象相等的条件,比如Person类中,我认为只要IDCard相同即对象相同等,此时可以进行重写或者重载。
看到这里,是不是觉得有点迷茫?==好像跟Equals差不多啊,为了想弄清这个问题,我加了作者陆敏技的QQ,以下是聊天记录:

建议12:重写Equals也要重写GetHashCode

坑爹啊!上一个建议的代码原来编译成功,但编译器会友情提示的,这里作者又引出了另外一个建议,何时了啊!

这是因为如果重写Equals方法而不重写GetHashCode方法,在使用Dictionary类的时候,可能会有一个潜在的Bug。

static Dictionary<Person, string> personValues = new Dictionary<Person, string>();
protected void Page_Load(object sender, EventArgs e)
{
AddPerson();
Person mike = new Person("Mike");
Response.Write(personValues.ContainsKey(mike)); //False
}
void AddPerson()
{
Person mike = new Person("Mike");
personValues.Add(mike, "mike");
Response.Write(personValues.ContainsKey(mike)); //True
}
本段代码输出结果:True False

这段代码的意思是,执行AddPerson()的时候,将idCode=Mike的Person对象存进Dictionary中,然后在Page_Load方法内,也同样new一个idCode=Mike的Person对象,使用ContainsKey方法搜索是否存在此对象Key,结果是不存在此对象。
      你可以会问,上一个建议中,我们已经重写了Person类的Equals方法了,只要idCode相等,我们就可以认为他们是相等的了,为什么此处会找不到Mike呢?
答:这是由于CLR已经优化了Dictionary这种查找,实际上是根据Key值的HashCode来查找Value值的。CLR首先调用Person类型的GetHashCode方法,发现这货根本就没有重写,于是就向上找Object的GetHashCode方法,Object为所有的CLR类型都提供GetHashCode默认实现,每new一个对象,CLR都会为该对象生成一个固定整形值,在对象生命周期内不会改变,对象默认的GetHashCode实现就是该整型值的HashCode,所以,虽然Mike值相等,但是HashCode是不相等的。

若要修正此问题,就必须重写GetHashCode方法

public override int GetHashCode()
{
return this.IDCode.GetHashCode();
}

进一步改进:GetHashCode方法存在一个问题,它返回的是一个整形类型,而整形类型的容量长度远远无法满足字符串的长度,也就是说,值不相同的情况下,HashCode可能存在相同的情况,为了减少产生相同HashCode的情况,做改进版本:

   public override int GetHashCode()
{
return (System.Reflection.MethodBase.GetCurrentMethod().DeclaringType.FullName + "#" + this.IDCode).GetHashCode();
}

小结:这个建议至少让我了解了HashCode,以前重写ToString方法的时候,就经常看到GetHashCode这个东东。

建议13:为类型输出格式化字符串

这个建议我读了两次才明白啊。
1、实现IFormattable接口实现ToString()输出格式化字符串
一般我们为类型提供格式化字符串的输出的做法是重写ToString(),但是这种方法提供的字符串输出是非常单一的,所以我们可以实现IFormattable接口的ToString方法,可以让类型根据用户的输入而格式化输出,因为重写的ToString方法没有参数,而实现 IFormattable接口的的ToString方法有参数,还是看代码最清晰。

    public class Person : IFormattable
{
public string FirstName { get; set; }
public string LastName { get; set; }
//重写的ToString方法输出字符串比较单一
public override string ToString()
{
return string.Format("{0},{1}", FirstName, LastName);
}
//实现IFormattable接口的ToString方法因为有参数,所以可以实现复杂的逻辑
public string ToString(string format, IFormatProvider formatProvider)
{
if (format == "ch")
return string.Format("中文名字:{0},{1}", FirstName, LastName);
else
return string.Format("EnglishName:{0},{1}", FirstName, LastName);
}
}

这样子调用:

Person p = new Person() { FirstName="wayne", LastName="chan" };
Response.Write(p.ToString());
Response.Write(p.ToString("ch",null));
Response.Write(p.ToString("english", null));

2、格式化器

上面的方法是在预见类型会存在格式化字符串输出的需求的时候,提前为类型实现了接口IFormattable,如果类型本身没有提供格式化字符串输出的功能,这时“格式化器”就派上用场了。

    //针对Person的格式化器
class PersonFormatter : IFormatProvider, ICustomFormatter
{
//IFormatProvider成员
public object GetFormat(Type formatType)
{
if (formatType == typeof(ICustomFormatter))
return this;
else
return null;
}
//ICustomFormatter成员
public string Format(string format, object arg, IFormatProvider formatProvider)
{
Person person = arg as Person;
if (person == null)
return string.Empty;
switch (format)
{
case "Ch":
return string.Format("{0}{1}", person.LastName, person.FirstName);
case "Eg":
return string.Format("{0}{1}", person.FirstName, person.LastName);
default:
return string.Format("{0}{1}", person.FirstName, person.LastName);
}
}
}

一个典型的格式化器应该要实现IFormatProvider, ICustomFormatter 接口,如果使用的话,就先初始化一个格式化器,如下:

Person person = new Person() { FirstName = "wayne", LastName = "chan", IDCode = "aaaa" };
//初始化格式化器
PersonFormatter pFormatter = new PersonFormatter();
Response.Write(pFormatter.Format("Ch", person, null));

其实看到这里,我觉得这个建议已经是非常细致的.NET知识了,一般人遇到这种情况,直接就会使用上一种方法了,在看书的时候,我也想直接跳过算了,但最后想,还是把他也记录下吧,毕竟这也是对自己的提高啊,即使以后还是会把这个知识点遗忘掉,还是可以在本博客找回来啊。

建议14:正确实现浅拷贝和深拷贝

浅拷贝和深拷贝的区别:
浅拷贝

修改副本的值类型字段不会影响源对象对应的字段,修改副本的引用类型字段会影响源对象,因为源对象复制给副本对象的时候,是引用类型的引用地址,也就是两者引用的是同一个对象。
深拷贝

无论值类型还是引用类型的字段,修改副本对象不会影响源对象,即使是引用类型,也是重新创建了一个新的对象引用。
要想自定义类型具有Clone拷贝的能力,就得继承ICloneable接口,然后根据需求,实现Clone方法以便实现浅拷贝或者深拷贝。

浅拷贝示例:

namespace WebApplication
{
public class Employee : ICloneable
{
public string IDCode { get; set; }
public int Age { get; set; }
public Department Department { get; set; }
//实现ICloneable接口成员
public object Clone()
{
return this.MemberwiseClone();
}
}
public class Department
{
public string Name { get; set; }
public override string ToString()
{
return this.Name;
}
}
public partial class WebForm1 : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
//初始化Employee对象employeeA
Employee employeeA = new Employee() { IDCode = "A", Age = , Department = new Department() { Name = "DepartmentA" } };
//从employeeA 浅拷贝出 employeeB
Employee employeeB = employeeA.Clone() as Employee;
//修改employeeB对象的属性
employeeA.IDCode = "B";
employeeA.Age = ;
employeeA.Department.Name = "DepartmentB";
//输出以便验证
Response.Write(employeeB.IDCode); // A
Response.Write(employeeB.Age); //
Response.Write(employeeB.Department.ToString()); //DepartmentB
}
}
}

从输出结果可以验证得到结果:
1、IDCode即使是string引用类型,Object.MemberwiseClone 依然为其创造了副本,在浅拷贝中,我们可以将string当做值类型来看待。
2、Employee的Department属性是引用类型,改变源对象employeeA中的值,会影响到副本对象employeeB

深拷贝示例
建议使用序列化的形式进行深拷贝:

      //实现ICloneable接口成员
public object Clone()
{
//浅拷贝
//return this.MemberwiseClone(); //使用序列化进行深拷贝
using (Stream objectStream = new MemoryStream())
{
IFormatter formatter = new BinaryFormatter();
formatter.Serialize(objectStream, this);
objectStream.Seek(, SeekOrigin.Begin);
return formatter.Deserialize(objectStream) as Employee;
}
}

这里我按照书中的代码来运行程序,结果爆黄页错误了,提示信息是:

中的类型“WebApplication.Employee”未标记为可序列化。

因为之前有相关的开发经验,知道那是因为实体类没有被标记为序列化属性,难道作者编写示例的时候没有检查出这个错误?或者是其他原因?
我们在实体类上标记一下即可运行成功,这是修改源对象employeeA中的值也不会影响到副本对象employeeB了。

 [Serializable]
public class Employee : ICloneable
[Serializable]
public class Department

建议15:使用dynamic来简化反射实现

dynamic是Framework4.0的新特性,dynamic的出现让C#具有了弱语言类型的特性,编译器在编译的时候,不再对类型进行检查,不会报错,但是运行时如果执行的是不存在的属性或者方法,运行程序还是会抛出RuntimeBinderException异常。

var 与 dynamic 的区别
var是编译器给我们的语法糖,编译期会匹配出实际类型并且替换该变量的声明。
dynamic 被编译后,实际是一个object类型,只不过编译器对dynamic做特殊处理,将类型检查放到了运行期。

这从VS的编译器窗口可以看出来,var 声明的变量在VS中有智能提示,因为VS能推断出来实际类型;dynamic声明的变量没有智能提示。

利用dynamic 简化反射

    public class DynamicSample
{
public string Name { get; set; }
public int Add(int a, int b)
{
return a + b;
}
}
public partial class DynamicPage : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
//普通的反射做法
DynamicSample dynamicSample = new DynamicSample();
var addMethod = typeof(DynamicSample).GetMethod("Add");
int res = (int)addMethod.Invoke(dynamicSample, new object[] { , }); //dynamic的做法,简洁,推荐
dynamic dynamicSample2 = new DynamicSample();
int res2 = dynamicSample2.Add(, ); //Add不会智能提示出来
}
}

使用dynamic还有一个优点就是,比没有优化过的反射性能好,跟优化过的反射性能相当,但代码整洁度高,作者也是贴了代码并贴出运行结果而已,没有作过多的介绍,所以此处作罢了。

建议16:元素数量可变的情况下不应使用数组

1、从内存使用角度看,数组在创建时被分配一段固定长度的内存,数据的存储结构一旦被分配,就不能再变化;
2、ArrayList是链表结构,可以动态增减内存空间;
3、List<T>是ArrayList的泛型实现,省去了拆箱和装箱带来的开销。

基于数组本身在内存的特点,因此,在使用数组的时候需要注意大对象(占用内存找过85000字节的对象)的问题,因为他们会被分配在大对象堆里,在回收过程中效率极低,所以,数组的长度不宜过份大。
再来回应本建议主旨,现在我们知道数组是不可变的,如果非得让数组变成“可变”的,那就只有像String那样,重新构造一个新的数组,再Copy过去了,这样可想性能是如此的差啊。

   public static class ClassForExtensions
{
public static Array ReSize(this Array array, int newSize)
{
//返回当前数组、指针或引用类型包含的或引用的对象的 System.Type
Type t = array.GetType().GetElementType();
//构造一个满足需要的新数组
Array newArray = Array.CreateInstance(t, newSize);
//将旧数组的内容Copy到新数组
Array.Copy(array, , newArray, , Math.Min(array.Length, newSize));
return newArray;
}
}

总结:
此建议跟“如果大规模string字符串拼接就用StringBuilder”异曲同工。

建议17:多数情况下使用foreach进行循环遍历

为什么会有这个建议,我就有些不解了,作者先是参照IEnumerator、IEnumerable自己实现了一个类似的迭代器,然后说它的内部实现用了for循环或者是while循环,写法都有点啰嗦,然后就说foreach出现了,还说foreach最大限度简化了代码,然后开始分析IL了,关于这个建议点,我觉得说得挺含糊的,不过根据作者的观点,foreach循环除了提供简化的语法外,还有两个优势。
1、自动将代码置入try-finally块
2、若类型实现IDispose接口,foreach会在循环结束后自动调用Dispose方法。

建议18:foreach不能代替for

foreach不支持循环时对集合进行增删操作,而for循环可以,其原因是foreach循环使用了迭代器进行集合的遍历,在迭代器里维护了一个集合版本的控制,我们对集合进行增删操作的时候,都会产生一个新的版本号,当foreach循环调用MoveNext 方法遍历元素时会对版本号进行检测,一旦检测版本号变动,则抛出异常,以下是我使用ILSpy反编译得出的代码, IEnumerator接口只定义了MoveNext成员,具体实现需要反编译其实现类,我是对List<T>进行反编译的。

// System.Collections.Generic.List<T>.Enumerator
[__DynamicallyInvokable]
public bool MoveNext()
{
List<T> list = this.list;
if (this.version == list._version && this.index < list._size)
{
this.current = list._items[this.index];
this.index++;
return true;
}
return this.MoveNextRare();
}

List<T>中对版本号的检测没有抛出异常,而某些实现类则会,比如:ArrayList类

// System.Collections.ArrayList.ArrayListEnumeratorSimple
public bool MoveNext()
{
if (this.version != this.list._version)
{
throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_EnumFailedVersion"));
}
// other code
}

而for循环则不会出现这个问题,我们通常在for循环的内部使用索引器来对集合成员的访问,不对版本号进行判断检测。以下是对List<T>的索引器的反编译代码。

// System.Collections.Generic.List<T>
[__DynamicallyInvokable]
public T this[int index]
{
[__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
get
{
if (index >= this._size)
{
ThrowHelper.ThrowArgumentOutOfRangeException();
}
return this._items[index];
}
[__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")]
set
{
if (index >= this._size)
{
ThrowHelper.ThrowArgumentOutOfRangeException();
}
this._items[index] = value;
this._version++;
}
}

可以看出,get属性没有对_version版本号进行检测,只要索引不超过size即可,而set属性,会对_version版本号+1。

建议19:使用更有效的对象和集合初始化

这个建议应该很多人都知道或者都已经在用了,如果你还不知道,那你就out了。

List<Person> list = new List<Person>();
Person p = new Person();
p.ID = ;
p.Name = "Tommy";
list.Add(p);

骚年,你还在这样进行对象、集合初始化吗?奥特了,借助了.NET的高级语法,我们可以使用对象和集合的初始化器来写出更加优雅的代码。设定项在大括号中对属性进行赋值

List<Person> lst = new List<Person>()
{
new Person(){ ID=,Name="Tommy"},
new Person(){ ID=,Name="Sammy"}
};

初始化设定项除了为对象、集合初始化方便外,还为Linq查询时的匿名类型进行属性的初始化的方便。

 List<Person> lst = new List<Person>()
{
new Person(){ Age = ,Name="Tommy"},
new Person(){ Age = ,Name="Sammy"}
};
var entity = from p in lst
select new { p.Name, AgeScope = p.Age > ? "Old" : "Young" };
foreach (var item in entity)
{
Response.Write(string.Format("name is {0},{1}", item.Name, item.AgeScope));
}

AgeScope 属性是经过计算得出的,有了如此方便的初始化方式,使得代码更加优雅灵活。

建议20:使用泛型集合代替非泛型集合

这个建议老生长谈了,尽量不要使用ArrayList,而是应该使用List<T> ,关于装箱拆箱的,不多说了,相信看过以上建议的朋友都比较熟悉了。

编写高质量代码改善C#程序的157个建议读书笔记【11-20】的更多相关文章

  1. 编写高质量代码改善C#程序的157个建议读书笔记【1-10】

    开篇 学生时代,老师常说,好记性不如烂笔头,事实上确实如此,有些知识你在学习的时候确实滚瓜烂熟,但是时间一长又不常用了,可能就生疏了,甚至下次有机会使用到的时候,还需要上网查找资料,所以,还不如常常摘 ...

  2. 编写高质量代码改善C#程序的157个建议[1-3]

    原文:编写高质量代码改善C#程序的157个建议[1-3] 前言 本文主要来学习记录前三个建议. 建议1.正确操作字符串 建议2.使用默认转型方法 建议3.区别对待强制转换与as和is 其中有很多需要理 ...

  3. 读书--编写高质量代码 改善C#程序的157个建议

    最近读了陆敏技写的一本书<<编写高质量代码  改善C#程序的157个建议>>书写的很好.我还看了他的博客http://www.cnblogs.com/luminji . 前面部 ...

  4. 编写高质量代码改善C#程序的157个建议——建议157:从写第一个界面开始,就进行自动化测试

    建议157:从写第一个界面开始,就进行自动化测试 如果说单元测试是白盒测试,那么自动化测试就是黑盒测试.黑盒测试要求捕捉界面上的控件句柄,并对其进行编码,以达到模拟人工操作的目的.具体的自动化测试请学 ...

  5. 编写高质量代码改善C#程序的157个建议——建议156:利用特性为应用程序提供多个版本

    建议156:利用特性为应用程序提供多个版本 基于如下理由,需要为应用程序提供多个版本: 应用程序有体验版和完整功能版. 应用程序在迭代过程中需要屏蔽一些不成熟的功能. 假设我们的应用程序共有两类功能: ...

  6. 编写高质量代码改善C#程序的157个建议——建议155:随生产代码一起提交单元测试代码

    建议155:随生产代码一起提交单元测试代码 首先提出一个问题:我们害怕修改代码吗?是否曾经无数次面对乱糟糟的代码,下决心进行重构,然后在一个月后的某个周一,却收到来自测试版的报告:新的版本,没有之前的 ...

  7. 编写高质量代码改善C#程序的157个建议——建议154:不要过度设计,在敏捷中体会重构的乐趣

    建议154:不要过度设计,在敏捷中体会重构的乐趣 有时候,我们不得不随时更改软件的设计: 如果项目是针对某个大型机构的,不同级别的软件使用者,会提出不同的需求,或者随着关键岗位人员的更替,需求也会随个 ...

  8. 编写高质量代码改善C#程序的157个建议——建议153:若抛出异常,则必须要注释

    建议153:若抛出异常,则必须要注释 有一种必须加注释的场景,即使异常.如果API抛出异常,则必须给出注释.调用者必须通过注释才能知道如何处理那些专有的异常.通常,即便良好的命名也不可能告诉我们方法会 ...

  9. 编写高质量代码改善C#程序的157个建议——建议152:最少,甚至是不要注释

    建议152:最少,甚至是不要注释 以往,我们在代码中不写上几行注释,就会被认为是钟不负责任的态度.现在,这种观点正在改变.试想,如果我们所有的命名全部采用有意义的单词或词组,注释还有多少存在的价值. ...

随机推荐

  1. javascript作用域和作用域链摘录

    作用域是JavaScript最重要的概念之一,想要学好JavaScript就需要理解JavaScript作用域和作用域链的工作原理.今天这篇文章对JavaScript作用域和作用域链作简单的介绍,希望 ...

  2. Apache错误:[error] (OS 10038)在一个非套接字上尝试了一个操作

    Apache错误:[error] (OS 10038)在一个非套接字上尝试了一个操作          博客分类: vb2005xu软件学习 OSApache防火墙PHPWindows  日志如下:[ ...

  3. 深入理解js——隐式原型

    每个函数都有一个prototye(原型),而每个对象都有一个_proto_,可成为隐式原型. _proto_是一个隐藏的属性,javascript不希望开发者用到这个属性值,有的低版本浏览器甚至不支持 ...

  4. css 中 list-style-image:

    用于设置<a>标签的默认格式的背景图片

  5. java核心知识点学习----多线程并发之线程同步

    1.什么是线程同步? 多线程编程是很有趣的事情,它很容易出现"错误情况",这种情况不是由编码造成的,它是由系统的线程调度造成的,当使用多个线程来访问同一个数据时,很容易出现&quo ...

  6. Photoshop 使用曲线

    曲线表示的是图像的明度, 通过信息办的 HSB 信息可以看到调整曲线时整个图像明度的变化 曲线的左下角表示图片的暗部, 右下角表示图片的高光部 而曲线本身的纵坐标则表示这个部分的明度, 例如将曲线的左 ...

  7. Linux ps命令详解与示例说明

      ps:要对进程进行监测和控制,首先必须要了解当前进程的情况,也就是需要查看当前进程,而ps命令就是最基本同时也是非常强大的进程查看命令.使用该命令可以确定有哪些进程正在运行和运行的状态.进程是否结 ...

  8. mORMot 数据库操作

    程序中要使用数据库,首先是引用SynCommons, SynDB单元,根据不同的数据库类型,简单举几个例子: 1 使用Access数据库,引用SynCommons, SynDB,SynOleDb三个单 ...

  9. 我开发的Quartz Cron表达式生成器

    Cron Expressions——Cron 表达式 Cron表达式被用来配置CronTrigger实例.Cron表达式是一个由7个子表达式组成的字符串.每个子表达式都描述了一个单独的日程细节.这些子 ...

  10. Android(Xamarin)之旅(一)

    Xamarin废话我就不多说了. 就是一款编写Android和IOS应用的IDE,从Visual Studio2010就开始有个这个插件.只要发展什么的,我觉得在这里说还不如自己去百度呢. 入正题: ...