设计模式1. Iterator模式

源码地址https://github.com/sadgeminids/IteratorLearning

 

设计模式目的:

为一个集合类提供遍历元素的功能,并且不暴露过多类的内部细节给使用者,尽量降低类编写者和使用者的耦合。

用通俗一点的话描述就是:想要对A遍历,A会给你一个B,然后B会告诉你怎么去遍历A。

 

UML图(扒自 图解设计模式):

设计模式1. Iterator模式_第1张图片

模式核心概述:

1.接口Aggregate,实现该接口的类可遍历。接口最核心的方法是返回一个Iterator对象,该对象负责真正的遍历工作

2.需要遍历的集合类实现接口Aggregate

3.接口Iterator,提供遍历对象所需的接口

4.实现Iterator接口的类,针对具体的集合类完成遍历逻辑

 

再举个例子:有一个班(集合类),里面有很多学生(集合类里的元素)。老师想要遍历这个班所有对学生点名,但是他不知道怎么去遍历。这时候这个班给了老师一个点名手册(实现接口Iterator的类),然后老师就可以根据这个学生手册上记录的名字(Iterator提供的方法)去遍历学生。

这个例子中A就是班级,B是学生名单手册。

代码实现:

理解了设计模式核心,怎么去实现代码呢?不同的语言可能具体接口提供函数不太一样,但核心思想是一致的。这里用C#来实现,所以Iterator提供的接口可能会与上面的XML类图有一定出入。

1.接口Aggregate(IEnumerable)

很多语言标准库都提供了该接口,不用我们再单独去实现。比如C#中,这个接口叫IEnumerable,只要继承了这个接口,就表示你的类是可遍历的。

using UnityEngine;
using System.Collections;

// Please ignore box and unbox, this is just an example
public class Collectionable : IEnumerable{

	private int[] m_iArray1 = new int[3]{1, 2, 3};
	private int[] m_iArray2 = new int[3]{4, 5, 6};

}

 

2.实现继承IEnumerable接口里的方法

IEnumerable接口方法只有一个 -- public IEnumerator GetEnumerator()。这个IEnumerator又是什么东西呢?看看.Net库里的代码:

namespace System.Collections
{
	[ComVisible (true), Guid ("496B0ABF-CDEE-11D3-88E8-00902754C43A")]
	public interface IEnumerator
	{
		//
		// Properties
		//
		object Current
		{
			get;
		}

		//
		// Methods
		//
		bool MoveNext ();

		void Reset ();
	}
}

里面有2个接口和1个属性。Reset暂且不说,意思很明显。另外两个Current和MoveNext恰好就是我们遍历一个对象关键所在。其实看名字也能知道,我们遍历一个对象是不是刚好需要能够取当前值,并且移动到下一个?

看来要想遍历Collectionable类,必须还得再实现一个继承IEnumerator的类了:

public class CollectionEnumrator : IEnumerator{
		
		private Collectionable m_collection;
		private int m_index = -1;
		
		public CollectionEnumrator(Collectionable collection){
			m_collection = collection;
		}
		
		public System.Object Current{
			get { 
				if(m_index < m_collection.m_iArray1.Length)
					return m_collection.m_iArray1[m_index];
				else if (m_index < m_collection.m_iArray1.Length + m_collection.m_iArray2.Length)
					return m_collection.m_iArray2[m_index - m_collection.m_iArray1.Length];
				else
					return null;
			}
		}
		
		public bool MoveNext(){
			m_index += 1;
			return m_index < m_collection.m_iArray1.Length + m_collection.m_iArray2.Length;
		}

		public void Reset(){

		}
		
	}

有了CollectionEnumrator对象,我们就可以实现public IEnumerator GetEnumerator()函数了:

public class Collectionable : IEnumerable{

	private int[] m_iArray1 = new int[3]{1, 2, 3};
	private int[] m_iArray2 = new int[3]{4, 5, 6};


	public IEnumerator GetEnumerator(){
		return new CollectionEnumrator (this);
	}
}

 

如何保持集合类内部数据的私有性:

上面代码可以了吗?当然不可以,因为这样是编译不过去的!

CollectionEnumrator 类保存了一个遍历对象Collectionable,要遍历对象必然也要访问对象。然而最大的问题是Collectionable的内部数据是private的!

可不可以把数据修改成public?可以,但是数据公开会暴露太多细节,这是类提供者不想看到的。而且公开了内部数据结构,何必再提供一个遍历对象呢,直接让使用者自己去访问就可以了,是不是?

Iterator设计模式出现本身就是为了解决类提供者和使用者耦合太强的问题,不想让暴露太多数据细节。可是我们要怎么解决无法访问private数据的问题呢?

很简答,把CollectionEnumrator放入Collectionable中,让它成为一个内部类就解决所有问题了:

using System.Collections;

// Please ignore box and unbox, this is just an example
public class Collectionable : IEnumerable{

	private int[] m_iArray1 = new int[3]{1, 2, 3};
	private int[] m_iArray2 = new int[3]{4, 5, 6};


	public IEnumerator GetEnumerator(){
		return new CollectionEnumrator (this);
	}


	public class CollectionEnumrator : IEnumerator{
		
		private Collectionable m_collection;
		private int m_index = -1;
		
		public CollectionEnumrator(Collectionable collection){
			m_collection = collection;
		}
		
		public System.Object Current{
			get { 
				if(m_index < m_collection.m_iArray1.Length)
					return m_collection.m_iArray1[m_index];
				else if (m_index < m_collection.m_iArray1.Length + m_collection.m_iArray2.Length)
					return m_collection.m_iArray2[m_index - m_collection.m_iArray1.Length];
				else
					return null;
			}
		}
		
		public bool MoveNext(){
			m_index += 1;
			return m_index < m_collection.m_iArray1.Length + m_collection.m_iArray2.Length;
		}

		public void Reset(){

		}
		
	}
}

这样我们现在就实现来一个可遍历的集合类。

如何去遍历:

 

有个集合类,有了遍历类,怎么去调用接口遍历对象呢?

using System;
using System.Collections;

public class CollectionClient {
	static void Main(string[] args) {

		Collectionable collectAble = new Collectionable ();

		IEnumerator iterator = collectAble.GetEnumerator ();
		while (iterator.MoveNext()) {
			Debug.LogError(iterator.Current);
		}

    }

}

先从集合类中取出IEnumerator对象,然后用MoveNext方法判断是否仍然有值,如果有可以用Current取出当前值。

感觉好像很麻烦啊,一点都不智能,能不能更简洁去调用这些接口呢?

当然,可以!为什么C#会设计这么一个接口,并且需要去实现这样几个方法呢,仅仅是为了规范吗?

当然,不是。C#会这么设计接口,是因为内部对于这种实现方式,提供了更为快捷的调用机制。

大名鼎鼎foreach:

C#本身提供给我们一个关键字foreach,能够快捷实现遍历。先看如何使用foreach:

using System;
using System.Collections;

public class CollectionClient {
	static void Main(string[] args) {

	Collectionable collectAble = new Collectionable ();
        foreach (int ele in collectAble)
			Debug.LogError (ele);

    }

}

对比上一种调用方式,代码简洁、方便了许多,一行代码就能搞定。而且foreach还能配合类型自动识别,更加方便:

foreach (var ele in collectAble)
    Debug.LogError (ele);

 

延伸思考1之集合类必须继承IEnumerable吗?

从上面可以代码依稀可见,实现遍历的核心是IEnumerator对象,而IEnumerable所要做的仅仅只是去返回这么一个对象。那能不能不继承IEnumerable,绕过去,我们直接让集合类继承IEnumerator?试一试:

public class Collectiontor : IEnumerator {
	private int[] m_iArray1 = new int[3]{1, 2, 3};
	private int[] m_iArray2 = new int[3]{4, 5, 6};
	private int m_index = -1;


	public System.Object Current{
		get {
			if(m_index < m_iArray1.Length)
				return m_iArray1[m_index];
			else if (m_index < m_iArray1.Length + m_iArray2.Length)
				return m_iArray2[m_index - m_iArray1.Length];
			else
				return null;
		}
	}

	public bool MoveNext(){
		m_index += 1;
		return m_index < m_iArray1.Length + m_iArray2.Length;
	}

	public void Reset(){
		
	}
}

有个编译错误:error CS1579: foreach statement cannot operate on variables of type `Collectiontor' because it does not contain a definition for `GetEnumerator' or is not accessible

大意就是说foreach不能作用在一个不包含 `GetEnumerator'方法的类型对象上面。我们把这个方法也实现一个:

	public IEnumerator GetEnumerator(){
		return this;
	}

再用foreach调用,发现结果跟之前方式对比,一摸一样。调用方式也一样。我们是不是可以干掉 IEnumerable,感觉没什么卵用啊,直接让集合类继承IEnumerator效果没区别啊,还能省掉一个类的实现呢!

这里就要涉及到设计原则了,通常一个类只应该有一个职责,单一的职责使得类结构更清晰。同时,将遍历的职责拆分出来,单独实现,也在一定程度上使得对于集合类本身的修改不会影响遍历类。更重要的是,修改遍历类的实现不会触及到集合类的修改。比如我们可以实现从前往后,从后往前,各种各样的遍历类。每一个遍历方式灵活替换,不会触碰到原始的集合类任何代码。

 

延伸思考2之foreach到底做了什么?

上一个思考中出现的编译错误,已经在某种程度上提示我们,foreach是在内部涉及到 GetEnumerator 、Current 、MoveNext等方法了?

反编译一下代码看看(这两张图是在公司截的,所以类名有所不同,但是实现代码几乎一致,反编译的IL代码也是一样的):

设计模式1. Iterator模式_第2张图片

设计模式1. Iterator模式_第3张图片

红框里都是关键信息:

1.实例化集合类对象;

2.调用了GetEnumerator()方法;

3.调用get_current()方法,这个就是Current属性的get方法

4.调用MoveNext()方法,移动到下一个值

这就是C#本身对常用代码的优化,为我们提供了便捷的调用方式。

延伸思考3之每个自定义继承IEnumerable的集合类,都必须实现一个对应的IEnumerator吗?

是的。每个自定义的集合类实现方式都不同,没有一种通用的Iterator可以适用于每一个集合类。不过,一般而言,集合类的编写者都会提供对应的Iterator实现类,所以使用者无需关注太多实现细节。

不仅是C#中提供的List,Dictionary等集合类实现了Iterator设计模式。C++中也有,只是名字不一样,就叫迭代器(iterator)。

#include 
#include 
  
using namespace std;
 
int main() {
vector ivec;
ivec.push_back(1);
ivec.push_back(2);
ivec.push_back(3);
ivec.push_back(4);
 
for(vector::iterator iter = ivec.begin();1. iter != ivec.end(); ++iter)
cout << *iter << endl;
}

c++中的迭代器也是根据每种类型都有单独对应的实现,确保跟集合类型契合。

延伸思考4之到底要不要用foreach?

foreach这么简单,那是不是可以肆无忌惮使用?

就如我一开始在代码中注释所言,这篇博客的代码是没有去关心装箱(box)和拆箱(unbox)的。仔细查看IEnumerator的方法,Current的返回类型是system.object,这是一个引用类型。可是我们集合类中的元素是值类型。

上面的IL截图中,也可以看到明确的 box int32 和 unbox.any int32。 装箱和拆箱肯定是有效率损耗的,所以,在性能无比珍贵的场景(比如手机游戏)中慎用foreach,至少引起装箱拆箱的要注意。如果,不考虑任何性能,怎么方便怎么来吧。

你可能感兴趣的:(C#,设计模式,设计模式,Iterator,遍历集合)