【C#】讓自定義類變爲可迭代

假設有兩個類,一個學生類Student,它用來存儲一個學生的信息,如名字和年齡。第二個是學校類School,它是學生類的集合。接下來我們一點點的看可迭代對象是如何進化的

一、石器時代的寫法

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp1
{
    internal class Student
    {
        public string Name { get; set; }

        public int Age { get; set; }

        public Student(string name, int age)
        {
            Name = name;
            Age = age;
        }
    }

    internal class School
    {
        private readonly List<Student> _list = new List<Student>();

        public int Count => _list.Count();

        public Student this[int index] => _list[index];

        public void Add(Student item)
        {
            _list.Add(item);
        }
    }

    internal class Program
    {
        private static void Main(string[] args)
        {
            var school = new School();

            // 添加3個學生
            school.Add(new Student("ahri", 15));
            school.Add(new Student("ashe", 20));
            school.Add(new Student("annie", 25));

            // 遍歷學校輸出所有學生信息
            for (var i = 0; i < school.Count; i++)
            {
                var item = school[i];
                Console.WriteLine($"{item.Name}\t{item.Age}");
            }

            Console.ReadLine();
        }
    }
}

通過for循環遍歷是在任何編程語言的入門書籍中都會提到的方法,因爲它最基礎。

二、使用IEnumerable

現代編程語言中基本上都會提供類似foreach這樣的關鍵字,它可以說是for的升級版本,它不需要你提供集合對象的元素數量就可以遍歷。而第一種遍歷方法的缺點是無法使用foreach關鍵字

看到提示,一個集合類想通過foreach來遍歷就得提供GetEnumerator方法,而這個方法正是IEnumerable接口唯一的一個方法,我們只需要繼承這個接口

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp1
{
    internal class Student
    {
        public string Name { get; set; }

        public int Age { get; set; }

        public Student(string name, int age)
        {
            Name = name;
            Age = age;
        }
    }

    internal class School : IEnumerable
    {
        private readonly List<Student> _list = new List<Student>();

        IEnumerator IEnumerable.GetEnumerator()
        {
            return new StudentEnumerator(_list);
        }

        public void Add(Student item)
        {
            _list.Add(item);
        }
    }

    internal class StudentEnumerator : IEnumerator
    {
        private readonly List<Student> _list;
        private int _index = -1;

        public StudentEnumerator(List<Student> items)
        {
            _list = items;
        }

        public bool MoveNext()
        {
            var count = _list.Count();
            if (_index < count)
            {
                _index++;
            }
            return _index < _list.Count();
        }

        public void Reset()
        {
            _index = -1;
        }

        public object Current => _list[_index];
    }

    internal class Program
    {
        private static void Main(string[] args)
        {
            var school = new School { new Student("ahri", 15), new Student("ashe", 20), new Student("annie", 25) };

            foreach (var item in school)
            {
                var student = (Student)item;
                Console.WriteLine($"{student.Name}\t{student.Age}");
            }
            Console.ReadLine();
        }
    }
}

怎麼看上去代碼越來越多了呢?是的,爲了能用上foreach,結果還得多寫一個StudentEnumerator類,它繼承自IEnumerator,必須實現2個方法和1個屬性,在foreach遍歷時會調用MoveNext來得知遍歷是否要結束了(內部通過索引來判斷是否到列表末尾了),沒遍歷完的話則通過Current屬性來獲取元素。可以看出,遍歷的核心實質上都是在StudentEnumerator這個類裏面,反而School顯得多餘了,C#支持繼承多接口,所以對於這個例子我們是可以將它們合併的

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp1
{
    internal class Student
    {
        public string Name { get; set; }

        public int Age { get; set; }

        public Student(string name, int age)
        {
            Name = name;
            Age = age;
        }
    }

    internal class School : IEnumerable, IEnumerator
    {
        private readonly List<Student> _list = new List<Student>();
        private int _index = -1;

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this;
        }

        public void Add(Student item)
        {
            _list.Add(item);
        }

        public bool MoveNext()
        {
            var count = _list.Count();
            if (_index < count)
            {
                _index++;
            }
            return _index < _list.Count();
        }

        public void Reset()
        {
            _index = -1;
        }

        public object Current => _list[_index];
    }

    internal class Program
    {
        private static void Main(string[] args)
        {
            var school = new School { new Student("ahri", 15), new Student("ashe", 20), new Student("annie", 25) };

            foreach (var item in school)
            {
                var student = (Student)item;
                Console.WriteLine($"{student.Name}\t{student.Age}");
            }
            Console.ReadLine();
        }
    }
}

一般不推薦這樣寫,因爲在GetEnumerator這裏返回的是自身,意味着迭代器不是獨立的,如果在foreach嵌套的情況下可能不是你想要的

foreach (var item in school)
{
    var student = (Student)item;
    Console.WriteLine($"Dep1:{student.Name}\t{student.Age}");
    foreach (var item2 in school)
    {
        var student2 = (Student)item2;
        Console.WriteLine($"Dep2:{student2.Name}\t{student2.Age}");
    }
}

因爲共享的是一個迭代器,所以外面的foreach只執行了一次

三、引入yield,拋棄IEnumerator

爲了循環時方便一點,似乎在集合類內部需要更多處理,有些得不償失的感覺。還好,C#沒讓我們失望,它提供了一個更現代的武器:yield

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp1
{
    internal class Student
    {
        public string Name { get; set; }

        public int Age { get; set; }

        public Student(string name, int age)
        {
            Name = name;
            Age = age;
        }
    }

    internal class School : IEnumerable
    {
        private readonly List<Student> _list = new List<Student>();

        IEnumerator IEnumerable.GetEnumerator()
        {
            foreach (var item in _list)
            {
                yield return item;
            }
        }

        public void Add(Student item)
        {
            _list.Add(item);
        }
    }

    internal class Program
    {
        private static void Main(string[] args)
        {
            var school = new School { new Student("ahri", 15), new Student("ashe", 20), new Student("annie", 25) };

            foreach (var item in school)
            {
                var student = (Student)item;
                Console.WriteLine($"{student.Name}\t{student.Age}");
            }
            Console.ReadLine();
        }
    }
}

通過yield return來一個個返回元素,就可以完全拋棄了StudentEnumerator類了。但請明白,yield只是語法糖,實際上在程序編譯時C#會自動實現一個對應的Enumerator類來完成迭代工作,只不過我們看不到而已。

最後,在foreach循環中每次都要做一次強制轉換也讓人受不了!還好,IEnumerable支持泛型

using System;
using System.Collections;
using System.Collections.Generic;

namespace ConsoleApp1
{
    internal class Student
    {
        public string Name { get; set; }

        public int Age { get; set; }

        public Student(string name, int age)
        {
            Name = name;
            Age = age;
        }
    }

    internal class School : IEnumerable<Student>
    {
        private readonly List<Student> _list = new List<Student>();

        public void Add(Student item)
        {
            _list.Add(item);
        }

        public IEnumerator<Student> GetEnumerator()
        {
            foreach (var item in _list)
            {
                yield return item;
            }
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }

    internal class Program
    {
        private static void Main(string[] args)
        {
            var school = new School { new Student("ahri", 15), new Student("ashe", 20), new Student("annie", 25) };

            foreach (var student in school)
            {
                Console.WriteLine($"{student.Name}\t{student.Age}");
            }

            Console.ReadLine();
        }
    }
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章