編寫高質量代碼改善C#程序的157個建議[勿選List 做基類、迭代器是只讀的、慎用集合可寫屬性]


前言

  本文已更新至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進行賦值,就會發現如下問題

上面也發現了兩種對集合進行初始化的方式。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM