编写高质量代码改善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=25},
            new Student(){Name="Kris",Age=23}
        };
        static void Main(string[] args)
        {
            StudentTeamA teamA = new StudentTeamA();
            Thread t1 = new Thread(() => 
            {
                teamA.Students = list;
                Thread.Sleep(3000);
                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=25},
            new Student(){Name="Kris",Age=23}
        };
        static void Main(string[] args)
        {
            StudentTeamA teamA = new StudentTeamA();
            teamA.Students.AddRange(list);
            teamA.Students.Add(new Student() { Name="Leo", Age=22 });
            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。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏逸鹏说道

Python3 与 C# 基础语法对比(新排版)

VSCode设置python3的开发环境(linux下默认是python2)https://www.cnblogs.com/dotnetcrazy/p/9095...

1292
来自专栏程序你好

理解C#语言中相等Equality 和唯一 Identity

1022
来自专栏飞扬的花生

集合中随机取不重复的索引

有时候希望从一个集合中随机取n个元素不重复 那么就取到这n个数字的索引 public static int[] GetRandomArray(int Numb...

3228
来自专栏二进制文集

JSON Java 解析

要使程序可以运行必须引入JSON-lib包——org.json.jar包。综合来看,这个JAR包比较好用。

1472
来自专栏码农阿宇

关于C#委托的一些学习笔记

1.什么是委托就是把方法作为参数传给另一个方法。委托说指向的函数,必须和函数具有相同的签名(返回值和参数类型) Public delegate void D...

2737
来自专栏Java后端技术

22中编程语言的HelloWorld

751
来自专栏GreenLeaves

C# 字符串操作基本过程(Equals、Compare、EndsWith等处理方法)

3912
来自专栏大内老A

由for V.S. for each想到的

一直想写一系列如何提高Performance和Scalability的文章,把我的相关经验和所知道的相关的技巧同大家分享。前一阵在园子里有一篇讨论for eac...

1888
来自专栏菩提树下的杨过

Silverlight与WPF中BeginInvoke的差异

Silverlight/WPF中,如果要在多线程中对界面控件值做修改,用Dispatcher对象的BeginInvoke方法无疑是最方便的办法 ,见:温故而知新...

2248
来自专栏程序员的SOD蜜

使用反射+缓存+委托,实现一个不同对象之间同名同类型属性值的快速拷贝

最近实践一个DDD项目,在领域层与持久层之间,Domain Model与Entity Model之间有时候需要进行属性值得拷贝,而这些属性,尽管它所在的类名称不...

2259

扫码关注云+社区

领取腾讯云代金券