编写高质量代码改善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进行赋值,就会发现如下问题

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

 

目录
相关文章
|
1月前
|
缓存 C# Windows
C#程序如何编译成Native代码
【10月更文挑战第15天】在C#中,可以通过.NET Native和第三方工具(如Ngen.exe)将程序编译成Native代码,以提升性能和启动速度。.NET Native适用于UWP应用,而Ngen.exe则通过预编译托管程序集为本地机器代码来加速启动。不过,这些方法也可能增加编译时间和部署复杂度。
|
1月前
|
C#
C# 图形验证码实现登录校验代码
C# 图形验证码实现登录校验代码
75 2
|
27天前
|
设计模式 程序员 C#
C# 使用 WinForm MDI 模式管理多个子窗体程序的详细步骤
WinForm MDI 模式就像是有超能力一般,让多个子窗体井然有序地排列在一个主窗体之下,既美观又实用。不过,也要小心管理好子窗体们的生命周期哦,否则一不小心就会出现一些意想不到的小bug
|
1月前
|
中间件 数据库连接 API
C#数据分表核心代码
C#数据分表核心代码
35 0
|
1月前
|
XML 存储 安全
C#开发的程序如何良好的防止反编译被破解?ConfuserEx .NET混淆工具使用介绍
C#开发的程序如何良好的防止反编译被破解?ConfuserEx .NET混淆工具使用介绍
51 0
|
1月前
|
安全 API C#
C# 如何让程序后台进程不被Windows任务管理器强制结束
C# 如何让程序后台进程不被Windows任务管理器强制结束
62 0
|
2月前
|
C# 容器
C#中的命名空间与程序集管理
在C#编程中,`命名空间`和`程序集`是组织代码的关键概念,有助于提高代码的可维护性和复用性。本文从基础入手,详细解释了命名空间的逻辑组织方式及其基本语法,展示了如何使用`using`指令访问其他命名空间中的类型,并提供了常见问题的解决方案。接着介绍了程序集这一.NET框架的基本单位,包括其创建、引用及高级特性如强名称和延迟加载等。通过具体示例,展示了如何创建和使用自定义程序集,并提出了针对版本不匹配和性能问题的有效策略。理解并善用这些概念,能显著提升开发效率和代码质量。
98 4
|
1月前
|
API C#
C#实现Winform程序右下角弹窗消息提示
C#实现Winform程序右下角弹窗消息提示
82 0
|
2月前
|
Linux C# 开发者
Uno Platform 驱动的跨平台应用开发:从零开始的全方位资源指南与定制化学习路径规划,助您轻松上手并精通 C# 与 XAML 编程技巧,打造高效多端一致用户体验的移动与桌面应用程序
【9月更文挑战第8天】Uno Platform 的社区资源与学习路径推荐旨在为初学者和开发者提供全面指南,涵盖官方文档、GitHub 仓库及社区支持,助您掌握使用 C# 和 XAML 创建跨平台原生 UI 的技能。从官网入门教程到进阶技巧,再到活跃社区如 Discord,本指南带领您逐步深入了解 Uno Platform,并提供实用示例代码,帮助您在 Windows、iOS、Android、macOS、Linux 和 WebAssembly 等平台上高效开发。建议先熟悉 C# 和 XAML 基础,然后实践官方教程,研究 GitHub 示例项目,并积极参与社区讨论,不断提升技能。
86 2