疊代器是一種設計模式,不管是Java還是C#,基本都是和foreach配合使用的,Java又略有不同。疊代器存在的目的是不給使用者看到原始資料,客戶需要資料的時候,由提供者提供給客戶一個疊代器,客戶通過這個疊代器來拿資料。不管内部的資料結構如何,内部的周遊如何實作,對外的疊代器的格式都是固定的。是以資料具體什麼樣的,由什麼格式來存儲,存儲在什麼位置,客戶是不知情的。疊代器的作用就是查詢,而不是修改,如果直接把原始資料丢給客戶,那麼客戶就可以随意修改原始資料了,如果不想讓客戶随意修改原始資料,那麼就要拷貝一份,這會導緻記憶體開銷加大。有些不必要的操作也沒必要,比如要擷取第一個,那麼隻需要疊代一次就可以了,但是如果選擇傳統的方式,則需要先拷貝一份資料源,然後拿到第一個,或者寫一個first()方法擷取到第一個,這都不如疊代器友善。
總結疊代器的作用:
1)資料對外不可見,使用者不可以随意修改原始資料源。
2)有些資料結構比較複雜,比如二叉樹結構的資料,使用者周遊起來麻煩,那麼則對外提供一個疊代器,内部如果對這部分資料進行周遊,使用者不再需要去關心。
3)有些場景下,疊代的過程需要複雜的操作,例如讀取磁盤下的所有檔案 ,那麼提供給使用者一個疊代器,視使用者周遊的情況來取資料,需要多少取多少,不用一次性從磁盤裡面讀出所有的資訊。
本文将會針對c#和java兩種語言的疊代器使用進行講解。
C#:
c#建立疊代器的關鍵字是yield,要求要進行疊代的類繼承IEnumerable接口,比如MyEnumerator這個類:
public class MyEnumerator : IEnumerable
{
private int[] nums = new int[5] { 1, 2, 3, 4, 5 };
public IEnumerator GetEnumerator()
{
foreach (var v in nums)
yield return v;
}
}
public class MyEnumerator : IEnumerable
{
public IEnumerator GetEnumerator()
{
yield return 1;
yield return 2;
yield return 3;
yield return 4;
yield return 5;
}
}
//使用代碼
static void Main(string[] args)
{
var myEnumerator = new MyEnumerator();
//這裡不需要調用GetEnumerator方法。
foreach (var v in myEnumerator)
{
Console.WriteLine(v);
}
System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
上面的兩種寫法結果是完全一樣的。
yield 關鍵字在這裡的作用是向編譯器訓示它所在的方法是疊代器塊。 編譯器生成一個類來實作疊代器塊中表示的行為。 在疊代器塊中,yield 關鍵字與 return 關鍵字結合使用,向枚舉器對象提供值。
盡管這裡以方法的形式編寫疊代器,但編譯器會将其轉換為一個實際上是狀态機的嵌套類。 隻要用戶端代碼中的 foreach 循環繼續進行,此類就會跟蹤疊代器的位置。
是以關鍵的點在于用yield告訴編譯器某一塊的代碼是疊代器,然後編譯器會在編譯時将其進行轉換成實際上是狀态機的嵌套類。
這就是為什麼傳回之後繼續疊代,能夠接着之前的位置繼續往下查找結果的原因。另外在使用的時候要使用foreach語句,而不是其他的循環語句。
在為類或結建構立疊代器時,不必實作整個 IEnumerator 接口。 當編譯器檢測到疊代器時,它将自動生成 IEnumerator 或 IEnumerator<T> 接口的 Current、MoveNext 和 Dispose 方法。
下面是官方提供的一個泛型清單疊代器的例子(https://docs.microsoft.com/zh-cn/previous-versions/visualstudio/visual-studio-2010/ee5kxzk0(v=vs.100)):
using System.Collections;
using System.Collections.Generic;
namespace GenericIteratorExample
{
public class Stack<T> : IEnumerable<T>
{
private T[] values = new T[100];
private int top = 0;
public void Push(T t) { values[top++] = t; }
public T Pop() { return values[--top]; }
// These make Stack<T> implement IEnumerable<T> allowing
// a stack to be used in a foreach statement.
public IEnumerator<T> GetEnumerator()
{
for (int i = top - 1; i >= 0; i-- )
{
yield return values[i];
}
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
// Iterate from top to bottom.
public IEnumerable<T> TopToBottom
{
get
{
// Since we implement IEnumerable<T>
// and the default iteration is top to bottom,
// just return the object.
return this;
}
}
// Iterate from bottom to top.
public IEnumerable<T> BottomToTop
{
get
{
for (int i = 0; i < top; i++)
{
yield return values[i];
}
}
}
// A parameterized iterator that return n items from the top
public IEnumerable<T> TopN(int n)
{
// in this example we return less than N if necessary
int j = n >= top ? 0 : top - n;
for (int i = top; --i >= j; )
{
yield return values[i];
}
}
}
// This code uses a stack and the TopToBottom and BottomToTop properties
// to enumerate the elements of the stack.
class Test
{
static void Main()
{
Stack<int> s = new Stack<int>();
for (int i = 0; i < 10; i++)
{
s.Push(i);
}
// Prints: 9 8 7 6 5 4 3 2 1 0
// Foreach legal since s implements IEnumerable<int>
foreach (int n in s)
{
System.Console.Write("{0} ", n);
}
System.Console.WriteLine();
// Prints: 9 8 7 6 5 4 3 2 1 0
// Foreach legal since s.TopToBottom returns IEnumerable<int>
foreach (int n in s.TopToBottom)
{
System.Console.Write("{0} ", n);
}
System.Console.WriteLine();
// Prints: 0 1 2 3 4 5 6 7 8 9
// Foreach legal since s.BottomToTop returns IEnumerable<int>
foreach (int n in s.BottomToTop)
{
System.Console.Write("{0} ", n);
}
System.Console.WriteLine();
// Prints: 9 8 7 6 5 4 3
// Foreach legal since s.TopN returns IEnumerable<int>
foreach (int n in s.TopN(7))
{
System.Console.Write("{0} ", n);
}
System.Console.WriteLine();
// Keep the console window open in debug mode.
System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
}
}
/* Output:
9 8 7 6 5 4 3 2 1 0
9 8 7 6 5 4 3 2 1 0
0 1 2 3 4 5 6 7 8 9
9 8 7 6 5 4 3
*/
這個例子基本包含了疊代器的正常操作,疊代器的作用就是疊代資料,不要把它想得太複雜。
Java:
//實作Iterable接口後,這個類就可以被foreach循環了
public class MyEnumerator implements Iterable<Integer>{
private Integer[] list=new Integer[]{1,2,3,4,5};
private Integer[] list1=new Integer[]{10,20,30,40,50};
//當這個類被foreach循環的時候,會首先調用這個方法擷取疊代器,然後進行循環
@Override
public Iterator<Integer> iterator() {
return new Itr();
}
//接受lamda表達式的foreach循環,有預設實作,可以不實作
@Override
public void forEach(Consumer<? super Integer> action) {
Objects.requireNonNull(action);
for (Integer t : list) {
action.accept(t);
}
}
//具體的疊代器類的實作
// 利用的是成員内部類的特性, 可以友善的拿到上層類的資料集,然後在這個基礎上進行疊代
class Itr implements Iterator<Integer>
{
private int index=0;
@Override
public boolean hasNext() {
return index<list1.length;
}
@Override
public Integer next() {
return list1[index++];
}
}
}
上面是java的一個疊代器的标準實作,使用的是成員内部類去實作,成員内部類編譯器會自動幫你生成一個構造方法,将生層類作為參數傳遞給内部類,這樣内部類就可以很友善地拿到上層類的變量了。如果不了解内部類的話,可以看我的另外一篇部落格https://blog.csdn.net/dap769815768/article/details/87625620
使用上面的自定義疊代器:
public class Main {
public static void main(String[] args) {
System.out.println("hello world");
MyEnumerator myEnumerator=new MyEnumerator();
for (Integer i:myEnumerator)
{
System.out.println(i);
}
myEnumerator.forEach(x->System.out.println(x));
}
}
輸出結果:
hello world
10
20
30
40
50
1
2
3
4
5
總結下來就是,如果類實作了Iterable,那麼這個類就可以被foreach循環,同時還可以調用forEach方法進行循環,forEach方法接口有預設實作,同時也可以寫自己的實作,這個列子裡我寫了一個自己的實作。
要想被疊代就必須添加自己的疊代器類,也就是本例中的内部類Itr,它實作Iterator接口,并實作了next和hasNext方法,這樣這個疊代器才能正常使用。
當這個類被foreach循環,其過程是,首先調用iterator方法擷取到這個類的疊代器,然後利用這個疊代器将資料周遊一遍。
如果你使用java反編譯工具,就可以看到,原本的foreach循環被改掉了,這裡我使用idea自帶的反編譯工具,檢視到的代碼變成了下面的這樣:
即foreach循環隻是一個文法糖,最終編譯的位元組檔案,使用的還是傳統的擷取疊代器的方法。