前言

  本文已更新至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>做基类、迭代器是只读的、慎用集合可写属性]的更多相关文章

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  9. 编写高质量代码改善C#程序的157个建议——建议151:使用事件访问器替换公开的事件成员变量

    建议151:使用事件访问器替换公开的事件成员变量 事件访问器包含两部分内容:添加访问器和删除访问器.如果涉及公开的事件字段,应该始终使用事件访问器.代码如下所示: class SampleClass ...

随机推荐

  1. 字符设备驱动——memory编译问题及解决办法

    1.fatal error:asm/system.h:No such file or directory #include <linux/version.h> #if LINUX_VERS ...

  2. redis 五种数据结构详解(string,list,set,zset,hash)

    redis 五种数据结构详解(string,list,set,zset,hash) Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存 ...

  3. 20150912华为机考1之"输入一个字符串,将其中出现次数最多的字符输出"

    不吐槽华为的服务器了,直接上正文 输入:字符串(英文字母),长度不超过128 输出:出现频率最高的字母 思路写在注释文档 /* Input a string * Output the most fre ...

  4. C++ 基本知识

    无论父类与子类的析构函数是否是virutal,子类的析构函数都会调用父类的析构函数 调用构造函数是与构造函数顺序相反,先子类后基类,否则如果基类先析构,子类的有些资源已经不存在了,会出错. 在C++中 ...

  5. [转]highcharts图表入门之:如何让highcharts图表自适应浏览器窗体的大小或者页面大小

    本文转自: http://jsfiddle.net/vCZ8V/1/ http://www.stepday.com/topic/?740 http://blog.csdn.net/yueritian/ ...

  6. Redis安装,mongodb安装,hbase安装,cassandra安装,mysql安装,zookeeper安装,kafka安装,storm安装大数据软件安装部署百科全书

    伟大的程序员版权所有,转载请注明:http://www.lenggirl.com/bigdata/server-sofeware-install.html 一.安装mongodb 官网下载包mongo ...

  7. mysql查询语句包含有关键字

    查询mysql的时候,有时候mysql表名或者列名会有关键字.这时候查询会有错误.例如表名是order,查询时候会出错. 简单的办法是sql语句里表名或者列名加上`[tab键上面]来加以区别,例如se ...

  8. java 13-3 int类型的包装包Integer

    1.Integer的概述 需求1:把100这个数据的二进制,八进制,十六进制计算出来 需求2:判断一个数据是否是int范围内的.  首先你得知道int的范围是多大? 为了对基本数据类型进行更多的操作, ...

  9. android初学问题集

    1. Manifest中的Application tag用途? 2. java中的组件设计模型是指什么? 3. java Bean是指什么? 4. Proxy-Stub设计模式又指的是什么? 有要的网 ...

  10. web前端开发资源整理

    webpack中文文档 http://webpackdoc.com/index.html 饿了么UI http://mint-ui.github.io/#!/zh-cn http://element. ...