编写高质量代码改善C#程序的157个建议[勿选List<T>做基类、迭代器是只读的、慎用集合可写属性]

简介: 原文:编写高质量代码改善C#程序的157个建议[勿选List做基类、迭代器是只读的、慎用集合可写属性]前言   本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html 。
原文: 编写高质量代码改善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进行赋值,就会发现如下问题

上面也发现了两种对集合进行初始化的方式。

 

目录
相关文章
|
22天前
|
Java 物联网 C#
C#/.NET/.NET Core学习路线集合,学习不迷路!
C#/.NET/.NET Core学习路线集合,学习不迷路!
|
2月前
|
缓存 C# Windows
C#程序如何编译成Native代码
【10月更文挑战第15天】在C#中,可以通过.NET Native和第三方工具(如Ngen.exe)将程序编译成Native代码,以提升性能和启动速度。.NET Native适用于UWP应用,而Ngen.exe则通过预编译托管程序集为本地机器代码来加速启动。不过,这些方法也可能增加编译时间和部署复杂度。
123 2
|
2月前
|
开发框架 NoSQL MongoDB
C#/.NET/.NET Core开发实战教程集合
C#/.NET/.NET Core开发实战教程集合
|
2月前
|
C#
C# 图形验证码实现登录校验代码
C# 图形验证码实现登录校验代码
88 2
|
2月前
|
设计模式 程序员 C#
C# 使用 WinForm MDI 模式管理多个子窗体程序的详细步骤
WinForm MDI 模式就像是有超能力一般,让多个子窗体井然有序地排列在一个主窗体之下,既美观又实用。不过,也要小心管理好子窗体们的生命周期哦,否则一不小心就会出现一些意想不到的小bug
156 0
|
2月前
|
中间件 数据库连接 API
C#数据分表核心代码
C#数据分表核心代码
41 0
|
2月前
|
XML 存储 安全
C#开发的程序如何良好的防止反编译被破解?ConfuserEx .NET混淆工具使用介绍
C#开发的程序如何良好的防止反编译被破解?ConfuserEx .NET混淆工具使用介绍
74 0
|
2月前
|
安全 API C#
C# 如何让程序后台进程不被Windows任务管理器强制结束
C# 如何让程序后台进程不被Windows任务管理器强制结束
66 0
|
2月前
|
API C#
C#实现Winform程序右下角弹窗消息提示
C#实现Winform程序右下角弹窗消息提示
106 0
|
1月前
|
C# 开发者
C# 一分钟浅谈:Code Contracts 与契约编程
【10月更文挑战第26天】本文介绍了 C# 中的 Code Contracts,这是一个强大的工具,用于通过契约编程增强代码的健壮性和可维护性。文章从基本概念入手,详细讲解了前置条件、后置条件和对象不变量的使用方法,并通过具体代码示例进行了说明。同时,文章还探讨了常见的问题和易错点,如忘记启用静态检查、过度依赖契约和性能影响,并提供了相应的解决建议。希望读者能通过本文更好地理解和应用 Code Contracts。
32 3