编写高质量代码改善C#程序的157个建议[勿选List<T>做基类、迭代器是只读的、慎用集合可写属性]
前言
本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html 。本文主要学习记录以下内容:
建议23、避免将List<T>作为自定义集合类的基类
建议24、迭代器应该是只读的
建议25、谨慎集合属性的可写操作
建议23、避免将List<T>作为自定义集合类的基类
如果要实现一个自定义的集合类,最好不要以List<T>作为基类,而应该扩展相应的泛型接口,通常是Ienumerable<T>和ICollection<T>(或ICollection<T>的子接口,如IList<T>。
public class Employee1:List<Employee>
public class Employee2:IEnumerable<Employee>,ICollection<Employee>
不过,遗憾的是继承List<T>并没有带来任何继承上的优势,反而丧失了面向接口编程带来的灵活性,而且可能不稍加注意,隐含的Bug就会接踵而至。
来看一下Employee1为例,如果要在Add方法中加入一点变化
public class Employee
{
public string Name { get; set; }
}
public class Employee1:List<Employee>
{
public new void Add(Employee item)
{
item.Name += "Changed";
base.Add(item);
}
}
进行调用
public static void Main(string[] args)
{
Employee1 employee1 = new Employee1() {
new Employee(){Name="aehyok"},
new Employee(){Name="Kris"},
new Employee(){Name="Leo"}
};
IList<Employee> employees = employee1;
employees.Add(new Employee(){Name="Niki"});
foreach (var item in employee1)
{
Console.WriteLine(item.Name);
}
Console.ReadLine();
}
结果竟然是这样
这样的错误如何避免呢,所以现在我们来来看看Employee2的实现方式
public class Employee2:IEnumerable<Employee>,ICollection<Employee>
{
List<Employee> items = new List<Employee>();
public IEnumerator<Employee> GetEnumerator()
{
return items.GetEnumerator();
} ///省略
}
这样进行调用就是没问题的
public static void Main(string[] args)
{
Employee2 employee1 = new Employee2() {
new Employee(){Name="aehyok"},
new Employee(){Name="Kris"},
new Employee(){Name="Leo"}
};
ICollection<Employee> employees = employee1;
employees.Add(new Employee() { Name = "Niki" });
foreach (var item in employee1)
{
Console.WriteLine(item.Name);
}
Console.ReadLine();
}
运行结果
建议24、迭代器应该是只读的
前端时间在实现迭代器的时候我就发现了这样一个问题,迭代器中只有GetEnumeratior方法,没有SetEnumerator方法。所有的集合也没有一个可写的迭代器属性。原来这里面室友原因的:
其一:这违背了设计模式中的开闭原则。被设置到集合中的迭代可能会直接导致集合的行为发生异常或变动。一旦确实需要新的迭代需求,完全可以创建一个新的迭代器来满足需求,而不是为集合设置该迭代器,因为这样做会直接导致使用到该集合对象的其他迭代场景发生不可知的行为。
其二:现在,我们有了LINQ。使用LINQ可以不用创建任何新的类型就能满足任何的迭代需求。
关于如何实现迭代器可以来阅读我这篇博文http://www.cnblogs.com/aehyok/p/3642103.html
现在假设存在一个公共集合对象,有两个业务类需要对这个集合对象进行操作。其中业务类A只负责将元素迭代出来进行显示:
IMyEnumerable list = new MyList();
IMyEnumerator enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
{
object current = enumerator.Current;
Console.WriteLine(current.ToString());
}
Console.ReadLine();
业务类B出于自己的某种需求,需要实现一个新的针对集合对象的迭代器,于是它这样操作:
MyEnumerator2 enumerator2 = new MyEnumerator2(list as MyList);
(list as MyList).SetEnumerator(enumerator2);
while (enumerator2.MoveNex())
{
object current = enumerator2.Current;
Console.WriteLine(current.ToString());
}
Console.ReadLine();
问题的关键就是,现在我们再回到业务类A中执行一次迭代显示,结果将会是B所设置的迭代器完成输出。这相当于BG在没有通知A的情况下对A的行为进行了干扰,这种情况应该避免的。
所以,不要为迭代器设置可写属性。
建议25、谨慎集合属性的可写操作
如果类型的属性中有集合属性,那么应该保证属性对象是由类型本身产生的。如果将属性设置为可写,则会增加抛出异常的几率。一般情况下,如果集合属性没有值,则它返回的Count等于0,而不是集合属性的值为null。我们来看一段简单的代码:
public class Student
{
public string Name { get; set; }
public int Age { get; set; }
}
public class StudentTeamA
{
public List<Student> Students { get; set; }
}
class Program
{
static List<Student> list = new List<Student>()
{
new Student(){Name="aehyok",Age=},
new Student(){Name="Kris",Age=}
};
static void Main(string[] args)
{
StudentTeamA teamA = new StudentTeamA();
Thread t1 = new Thread(() =>
{
teamA.Students = list;
Thread.Sleep();
Console.WriteLine("t1"+list.Count);
});
t1.Start();
Thread t2 = new Thread(() =>
{
list= null;
});
t2.Start();
Console.ReadLine();
}
}
首先运行后报错了
这段代码的问题就是:线程t1模拟将对类型StudentTeamA的Students属性进行赋值,它是一个可读/可写的属性。由于集合属性是一个引用类型,而当前针对该属性对象的引用却有两个,即集合本身和调用者的类型变量list。
线程t2也许是另一个程序猿写的,但他看到的只有list,结果,针对list的修改会直接影响到另一个工作线程中的对象。在例子中,我们将list赋值为null,模拟在StudentTeamA(或者说工作线程t1)不知情的情况下使得集合属性变为null。接着,线程t1模拟针对Students属性进行若干操作,导致异常的抛出。
下面我们对上面的代码做一个简单的修改,首先,将类型的集合属性设置为只读,其次,集合对象由类型自身创建,这保证了集合属性永远只有一个引用:
public class Student
{
public string Name { get; set; }
public int Age { get; set; }
}
public class StudentTeamA
{
public List<Student> Students { get;private set; }
public StudentTeamA()
{
Students = new List<Student>();
}
public StudentTeamA(IEnumerable<Student> list):this()
{
Students.AddRange(list); }
}
class Program
{
static List<Student> list = new List<Student>()
{
new Student(){Name="aehyok",Age=},
new Student(){Name="Kris",Age=}
};
static void Main(string[] args)
{
StudentTeamA teamA = new StudentTeamA();
teamA.Students.AddRange(list);
teamA.Students.Add(new Student() { Name="Leo", Age= });
Console.WriteLine(teamA.Students.Count);
///另外一种实现方式
StudentTeamA teamB = new StudentTeamA(list);
Console.WriteLine(teamB.Students.Count);
Console.ReadLine();
}
}
修改之后,在StudentTemaA中尝试对属性Students进行赋值,就会发现如下问题
上面也发现了两种对集合进行初始化的方式。
英语小贴士
1、I have an outing plan tommorrow。 ——明天我有一个徒步的计划。
2、Amazing——使……人惊讶 unbelievable——令人不可思议
3、blue sky,white beach——蓝天白云
4、Could you please show me the way to cinema?——你可以向我展示去电影院的路吗?
5、go straight——直走 turn left——向左转 at the first cross——在第一个十字路口
作者:aehyok
出处:http://www.cnblogs.com/aehyok/
感谢您的阅读,如果您对我的博客所讲述的内容有兴趣,那不妨点个推荐吧,谢谢支持:-O。
编写高质量代码改善C#程序的157个建议[勿选List<T>做基类、迭代器是只读的、慎用集合可写属性]的更多相关文章
- 编写高质量代码改善C#程序的157个建议[1-3]
原文:编写高质量代码改善C#程序的157个建议[1-3] 前言 本文主要来学习记录前三个建议. 建议1.正确操作字符串 建议2.使用默认转型方法 建议3.区别对待强制转换与as和is 其中有很多需要理 ...
- 读书--编写高质量代码 改善C#程序的157个建议
最近读了陆敏技写的一本书<<编写高质量代码 改善C#程序的157个建议>>书写的很好.我还看了他的博客http://www.cnblogs.com/luminji . 前面部 ...
- 编写高质量代码改善C#程序的157个建议——建议157:从写第一个界面开始,就进行自动化测试
建议157:从写第一个界面开始,就进行自动化测试 如果说单元测试是白盒测试,那么自动化测试就是黑盒测试.黑盒测试要求捕捉界面上的控件句柄,并对其进行编码,以达到模拟人工操作的目的.具体的自动化测试请学 ...
- 编写高质量代码改善C#程序的157个建议——建议156:利用特性为应用程序提供多个版本
建议156:利用特性为应用程序提供多个版本 基于如下理由,需要为应用程序提供多个版本: 应用程序有体验版和完整功能版. 应用程序在迭代过程中需要屏蔽一些不成熟的功能. 假设我们的应用程序共有两类功能: ...
- 编写高质量代码改善C#程序的157个建议——建议155:随生产代码一起提交单元测试代码
建议155:随生产代码一起提交单元测试代码 首先提出一个问题:我们害怕修改代码吗?是否曾经无数次面对乱糟糟的代码,下决心进行重构,然后在一个月后的某个周一,却收到来自测试版的报告:新的版本,没有之前的 ...
- 编写高质量代码改善C#程序的157个建议——建议154:不要过度设计,在敏捷中体会重构的乐趣
建议154:不要过度设计,在敏捷中体会重构的乐趣 有时候,我们不得不随时更改软件的设计: 如果项目是针对某个大型机构的,不同级别的软件使用者,会提出不同的需求,或者随着关键岗位人员的更替,需求也会随个 ...
- 编写高质量代码改善C#程序的157个建议——建议153:若抛出异常,则必须要注释
建议153:若抛出异常,则必须要注释 有一种必须加注释的场景,即使异常.如果API抛出异常,则必须给出注释.调用者必须通过注释才能知道如何处理那些专有的异常.通常,即便良好的命名也不可能告诉我们方法会 ...
- 编写高质量代码改善C#程序的157个建议——建议152:最少,甚至是不要注释
建议152:最少,甚至是不要注释 以往,我们在代码中不写上几行注释,就会被认为是钟不负责任的态度.现在,这种观点正在改变.试想,如果我们所有的命名全部采用有意义的单词或词组,注释还有多少存在的价值. ...
- 编写高质量代码改善C#程序的157个建议——建议151:使用事件访问器替换公开的事件成员变量
建议151:使用事件访问器替换公开的事件成员变量 事件访问器包含两部分内容:添加访问器和删除访问器.如果涉及公开的事件字段,应该始终使用事件访问器.代码如下所示: class SampleClass ...
随机推荐
- POJ 3687 Labeling Balls()
Labeling Balls Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 9641 Accepted: 2636 Descri ...
- C#连接MySql数据库的方法
1.要连接MySql数据库必须首先下载MySql的连接.net的文件, 文件下载地址为http://download.csdn.net/detail/xiaoliu123586/91455792.解压 ...
- C语言变参问题
C++中有函数重载这种方法,以供我们调用时要可以不确定实参的个数,其实 C 语言也可以,而且更高明! 我们在stdio.h 中可以看到 printf() 函数的原型: int printf(char ...
- 欧拉函数 cojs 2181. 打表
cojs 2181. 打表 ★☆ 输入文件:sendtable.in 输出文件:sendtable.out 简单对比时间限制:1 s 内存限制:256 MB [题目描述] 有一道比赛题 ...
- hdu-4811 Ball
题目链接: Ball Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total ...
- sql 入门经典(第五版) Ryan Stephens 学习笔记 (第一,二,三,,四,五章)
SQL - Structured Query Language (结构化查询语言) 1/ SQL 命令的类型 : 数据定义语言: DDL 数据操作语言: DML 数据查询语言: DQL 数据控制语言 ...
- Eclipse安装Database Development插件。
早期版本的Eclipse,自带Database Development,用着挺方便的,可是自己的最新版Eclipse反而没有.于是乎钻研了下,找到了安装方法.和汉化包安装很类似: 菜单栏里选择 ...
- [cb]NGUI组件基类之 UIWidget
UIWidget NGUI的UIWidget是所有组件的基类,它承担了存储显示内容,颜色调配,显示深度,显示位置,显示大小,显示角度,显示的多边形形状,归属哪个UIPanel.这就是UIWidget所 ...
- DataGridView 行、列的隐藏和删除
) 行.列的隐藏 [VB.NET] ' DataGridView1的第一列隐藏 DataGridView1.Columns(0).Visible = False ' DataGridView1的第一行 ...
- 考虑与Maya结合
今天改进了Hessian各块的计算代码,减少了一些内存操作.下一步准备把模拟平台与Maya结合,这样就可以利用Maya丰富的变形算法了. 这一步需要考虑以下问题: 1.把场景设置为某一帧.这一点可以用 ...