{ Snake @ NET } 废寝忘食的程序工作者.

19八/100

用接口与委托的常用方法来谈二者的区别

早先的一篇文章让我与几个朋友引发了一些讨论,大部分都在谈论的表达能力的问题,于是我在这一篇重整语言,并且将二者的部分通用方法列举出来再次试图表明他们的区别.希望我这次的行文会更容易让人接受一些.废话不多说,正题开始.

1. 困惑

接口与委托之所以出现混淆,是因为二者都能影响某一个方法的执行结果或执行过程,写一个极为简单的例子就能看出他们的相似:

public class DeleTest
{
	public DeleTest(decimal a, decimal b)
	{
		this.a=a;
		this.b=b;
	}
	private decimal a,b;
	//这是一个通过传递一个Func<T1,T2,TResult>委托来计算值的方法.
	public decimal Calculate(Func<decimal, decimal, decimal> func)
	{
		return func(a,b);
	}
	//下面是2个匹配Func<T1,T2,TResult>的方法.
	public decimal Add(decimal a,decimal b)
	{
		return a+b;
	}
	public decimal Substract(decimal a,decimal b)
	{
		return a-b;
	}
	//我们通过以下方式调用来改变Calculate的值
	public static void Main(string[] args)
	{
		DeleTest t=new DeleTest(1,2);
		Console.WriteLine(t.Calculate(t.Add));		//结果为3
		Console.WriteLine(t.Calculate(t.Substract));//结果为-1
	}
}

DeleTest类是一个委托的示范类,其中包含2个符合Func<T1,T2,TResult>签名的方法Add,Substract.当我们调用该类的Calculate方法时,需要传进一个委托,而整个Calculate方法完全依靠这个委托来获得计算结果.

public interface ICal
{
	decimal Calculate(decimal a,decimal b);
}
public class Addition
{
	public decimal Calculate(decimal a, decimal b)
	{
		return a+b;
	}
}
public class Substraction
{
	public decimal Calculate(decimal a, decimal b)
	{
		return a-b;
	}
}
public class IfTest
{
	public static void Main(string[] args)
	{
		decimal a=1,b=2;
		ICal ical=new Addition();
		Console.WriteLine(ical.Calculate(a,b)); //相加得3
		ical=new Substraction();
		Console.WriteLine(ical.Calculate(a,b)); //相减得-1
	}
}

以上是接口的实现版本,首先定义一个ICal接口,实现ICal接口的类都必须实现Caculate方法.此时接口给我们提供了一个不变切入点.只要我们声明一个ICal接口成员,并且赋一个合法的值(即实现了ICal接口的类),那么这个类一定包含了我们想要调用Calculate方法.于是在改变其ical变量的引用时,输出的结果当然不同.好比通过以下代码:

string a="snake";

a="cat;"

a最终的值是什么一样简单.

那么我们仔细研究一下2者的区别在何处.

委托是.NET平台中一个独特的东西,它可以向指针一样把方法当做变量(或参数).但是委托不但能调动一个方法,还能顺序调用多个方法,而实现方式只要:

委托+=方法,匿名方法,Lambda表达式;

就可以轻松实现.

另外委托还有异步调用的功能,使用:

委托.BeginInvoke()

方法即可通过异步方式执行委托,实现多线程操作.这些都是委托极为醒目的特点.

相比接口,接口并不是一个新事物,但它却是传统面对对象语言所不可或缺的一个元素.

类需要强制要求实现它的类必须实现它所声明的一切(包括属性,方法等),此时在应用程序中,只要声明了一个接口成员,那么接口背后的实例就绝对实现了接口中的一切.此时应用程序就可以毫无顾虑地调用这个接口成员的指定方法(如上例的ical.Calculate方法).

2. 解疑

俗话说条条大路通罗马,但是我们在编程中需要极力避免出现南辕北辙耗时耗力的错误.虽然在Java中事件是基于接口形式实现的,但在.NET平台下的语言中,事件则是基于委托.使用委托能大量节省事件的实现步骤.另外,因为委托和接口在.NET平台下的关注点不同,所以二者所胜任的工作也是大不相同的.下面我就通过二者的适用情况来解释二者的区别.

l 委托关注点在方法,作用范围小.

l 接口关注点在于类的设计,作用范围广.

2.1 委托

关注点:

不止读者您有没注意到在设计一个方法的时候我们可以随意安插委托的位置,使该委托能够发挥它的效力.我们不妨小小模拟一下集合IList<T>中的扩展方法OrderBy的实现策略(OrderBy方法能通过类型T的某一成员进行重新排序):

public void OrderBy<T>(this IList<T> list,Func<T1, TResult> dele)
{
	T temp;
	for(int i=0;i<list.Count;i++)
	{
		for(int j=0;j<list.Count;j++)
		{
			if(dele(list[i])>dele(list[j]))
			{
				temp=list[j];
				list[j]=list[i];
				list[i]=temp;
			}
		}
	}
}

上面的if(dele(list[i])>dele(list[j]))就是通过委托来获取类型T的某一成员的值来互相比较这时我就可以这样调用OrderBy方法:

//设一类型为projectList集合proj,需要通过project.ConstructionTime(施工期)的长//短来排序

proj.OrderBy(p => p.ConstructionTime);//Lambda表达式的调用方式

//通过匿名方法来调用该方法则为:

proj.OrderBy(new delegate(p){

return p.ConstructionTime;

});

我们可以看到委托是生存于方法内的.明确的说,委托在活动期间是与方法相关联的.我们在适当地地方调用委托,让委托发挥效应从而改变方法的执行结果.

作用范围:

在最初的计算例子中我们能看到,当我们以方法为参数的时候其实只传递了一个方法签名(t.Calculate(t.Add).)我们并没有向往常一样看到方法的常规结尾闭合括弧( [参数们] )”.因为委托只负责调用方法,至于如何传递参数,那是调用委托的负责人所要干的事情.

不妨把调用委托的方法称为老板,把委托称为秘书,把被委托调用的方法称为员工.

这时老板想到了一个点子需要能赚到一笔不小的生意,但是自己都已经身为老板不能什么都自己揽着干,找了那么多小弟也不是白给工资的.于是老板通过秘书,让秘书将老板的要求传达给他手下的员工们做,最后他只要能赚到这笔钱就成了,管他是老板干的还是员工干的.

2.2接口

关注点:

接口的关注点在于类的设计.它约束类必须实现接口所声明的一切.这样才能有一个不变的调用途径.就像所有实现了IEnumerable接口的类都可以通过foreach语句进行迭代,还有所有实现了IDisposable接口的类都有一个Dispose方法可供使用者进行资源释放,而使用using语句更是能简化这一操作.这是一个类所具有的特征,而委托就无法在做到这种效果.

另外,.NET3.0以后在声明泛型类或方法的时候,我们可以在类名或方法签名之后使用 where语句来限制T的类型,避免错误传递类型引发应用程序错误.

作用范围:

接口声明的成员都在类中实现,所以无论是属性还是方法,他们都有很大的自由度.比如截然不同的数据库存储模式,虽然通过接口我们都能获取到一致的结果,但是二者的实现方式和可利用的资源绝对不仅仅只有委托签名中仅有的几个参数而已.

//定义一个实体类
public class Entity
{
	Guid ID{get;set;}
}

//这是我们数据库连接层接口
public interface IDataAccessLayer
{
    void Add(Entity e);
    void Update(Entity e);
    void Delete(entity e);
    Entity SelectAll();
}

//MySql数据库连接层
public class MySqlDAL : IDataAccessLayer
{
    private static MySqlDataContext c = new MySqlDataContext("MySql的连接字符串");
    public void Add(Entity e)
    {
        string addString = "insert ***";
        //这里包含了各种sql语句生成内容,不一一编造.
        c.ExecuteCommand(addString);
    }
    //其他方法就不写了,咱们一个方法就能说明问题.
}

//Oracle数据库连接层
public class OracleDAL : IDataAccessLayer
{
    private static OracleDataBase db = new OracleDataBase("Oracle连接字符串");
    private static Table<Entity> table = db.GetTable("Entity");

    public void Add(Entity e)
    {
        table.Add(e);
        db.SaveChanges();
    }
}

//客户端上的实现方案
public class Client
{
    IDataAccessLayer dal;

//构造器,通过传入不同的字符串来支持不同的数据库类型.
    public Client(string dbType)
    {
       switch (dbType)
        {
           case "mysql":
               dal = new MySqlDAL();
           case "oracle":
               dal = new OracleDAL();
           default:
               throw new System.NotSupportedException("We're not supported the " + dbType + " Database");
        }
    }

//通过这个属性来进行数据库操作.
    public IDataAccessLayer DAL{ get { return dal; } }

    public static void Main()
    {
        Client c=new Client("oracle");
        c.DAL.Add(new Entity()
        {
            ID = "new item"
        });
    }
}

//以上为伪代码,不可执行

可以通过上面稍微复杂的代码看到,2种不同的类虽然实现了同一接口,但是他们完全调用了所有类中能调用的一切资源进行操作.而这么庞大的操作,并不是委托的强项.

3.总结

我们首先通过委托与接口二者的相似之处开始展开,又有因为细节部分而将二者区分开来,也提到了而且完全不同的地方.

本文提到通过编程语言编写一个功能不是不可能,只是实现方法的问题,我们需要利用编程语言和平台的每个对象的优点和特性来决定最适合他们应用方向,切忌南辕北辙.所以最后本文通过两个最为重要的重点解释和区分了委托和接口二者的区别及适用场景.

最后祝大家事业顺利!

25二/100

还不明白什么是接口(Interface)?看我能不能帮到你!

ASP.NET 2.0以上版本中(没接触过1.1)有一个非常有用的东西叫做接口(Interface).当初理解接口时花了不少时间.也在了解的过程中看到过误人子弟的文章和说法.由于Snake并非特别注重概念.所以我将以比喻方法向大家解释什么是接口,当然,遇到一些不严谨的地方我尽可能提醒您.

以下是接口在MSDN中的定义:

接口描述的是可属于任何类或结构的一组相关功能。接口可由方法、属性、事件、索引器或这四种成员类型的任意组合构成。接口不能包含字段。接口成员一定是公共的。

当类或结构继承接口时,意味着该类或结构为该接口定义的所有成员提供实现。接口本身不提供类或结构能够以继承基类功能的方式继承的任何功能。但是,如果基 类实现接口,派生类将继承该实现。

类和结构可以按照类继承基类或结构的类似方式继承接口,但有两个例外:

  • 类或结构可继承多个接口。
  • 类或结构继承接口时,仅继承方法名称和签名,因为接口本身不包含实现。

接口具有下列属性:

  • 接口类似于抽象基类:继承接口的任何非抽象类型都必须实现接口的所有成员。
  • 不能直接实例化接口。
  • 接口可以包含事件、索引器、方法和属性。
  • 接口不包含方法的实现。
  • 类和结构可从多个接口继承。
  • 接口自身可从多个接口继承。

首先接口在好多人的第一印象中就是:”像USB接口那样”.很好,这就是一个入口点,此时我们虚拟一下我们的思路:

接口->USB接口?

我下一步想的是—USB接口可以连接MP3,可以连接U盘,可以连接USB台灯等等..

请注意,上面这个思路是错误的!我们要把注意力放在USB接口上,并且微观化!那么现在的思路是:

接口->USB接口?->USB接口的结构是什么?

据我所知,USB接口里有4个”针脚”,所能提供的仅仅只有5V直流电和数据交换通路,那么我们回归编程语言,这个USB接口用C#到底怎么写.

public interface IUsbHub
{
	void Power();
	void DataTransfer(object data, bool isPut);
}

(看到这里如果你要问”为什么不给接口里面的这两个方法写具体实现代码”的话,请您先搞清楚接口是如何定义的,再继续看这篇文章.)

好了.接口定义完了.这就是接口的指责,非常的简单.那么电脑就可以不管这个USB接口上插的是什么东东,只提供5V直流电和数据交换的通道.而到底传递的是什么数据?这就不是接口要管的东西,具体的数据应该交给别的硬件,比如主板.

看到这里您是不是感觉”好像明白了接口是什么,但是不清楚接口是干嘛的”?那么我现在再来举个例子告诉您接口是干什么用的.继续研究USB接口.现在再把思路展现出来就是这样的:

接口->USB接口?->USB接口的结构是什么?->接口是干什么用的?

上面已经定义过接口,现在就不重复定义了.我们现在要定义几个使用USB接口的产品.

MP3:

public class MP3 : IUsbHub
{
	private bool hasPower = false;
	public void Power()
	{
		hasPower = true;
	}
	public void DataTransfer(object data, bool isPut)
	{
		if (hasPower)
		{
			if (isPut)
			{
				Console.WriteLine("发送数据包予电脑中.");
				Console.WriteLine("已发送" + data.GetType() + "类型的数据.");
			}
			else
			{
				Console.WriteLine("接收来自电脑传来的数据中..");
				Console.WriteLine("已接收" + data.GetType() + "类型的数据.");
			}
		}
		else
		{
			Console.WriteLine("无法连接电脑,请检查电源是否打开.");
		}
	}

	public void PlayMusic(int musicIndex)
	{
		Console.WriteLine("正在播放第{0}首歌", musicIndex);
	}
}

这个MP3在通入电源的时候(也就是执行Power函数)的时候会使其私有变量hasPower成为true,这样就代表MP3已经通电了.可以工作了.而执行DataTransfer方法的时候,先会判断就是已经连接电源了,然后再进行进一步与电脑进行数据交互.

USB台灯:

public class UsbLight : IUsbHub
{
	private void Light()
	{
		Console.WriteLine("电灯被点亮了");
	}
	public void Power()
	{
		Light();
	}
	public void DataTransfer(object data, bool isPut)
	{

	}
}

这个USB台灯显然就简单的多,虽然执行了DataTransfer方法,但并没有任何代码实现.说明台灯是不需要跟电脑交互的,只需要电源供电就好.

那么现在我们写一个”电脑”这个类,负责调用这两个USB产品.先给代码,再在代码里用注释说明.

public class Computer
{
	//首先,电脑在不经过数据交换是不可能知道插在USB接口上的东东是啥
	//所以我们要把这个USB产品定义为IUsbHub类型
	//当然既然成为了IUsbHub类型就只能执行IUsbHub已有的方法--那就是Power()和DataTransfer(object data,bool isPut)了.
	protected void Main()
	{
		IUsbHub mp3 = new MP3();
		mp3.DataTransfer(1, true);
		mp3.Power();
		mp3.DataTransfer(1, true);
		mp3.DataTransfer("1", false);
		//我们此时无法执行MP3的PlayMusic这个方法,因为这个方法和USB接口没啥关系.
		IUsbHub light = new UsbLight();
		light.Power();
		//至于USB台灯就没啥好讲了.
	}
}

好了,到此我们知道接口应该怎么用或者干啥用的了,那么什么时候用到接口才比较好呢?

本来想讲些概念再说例子的,但是肚子里的墨水不够多,有些观念不知道正确错误,所以不好说出口,但是下面的例子一定是最好的说明:

我们知道GridView是一个强大的数据源控件,那么GridView在我们定义GridView.DataSource的时候按F12进入GridView代码中,点开DataSource的注释说明可以看到下面这句话.

// 返回结果:System.Collections.IEnumerable 或 System.ComponentModel.IListSource 对象,包含用于为此控件提供数据的值的集合。默认值为null。

也就是说GridView的数据源必须实现了IEnumerable或者IListSource接口,而这两个接口必须实现的方法就是可以一一列举出该类型的每一条数据.比如List,数组,SqlDataSource.GridView在显示数据时无非就是使用了一个foreach或for循环一条一条将数据源里面的数据枚举出来再加以显示.

上面的例子说明IEnumerable或者IListSource接口给与了实现这2个接口的类型以可枚举的能力.而这个枚举能力恰好就是GridView.DataSource所迫切需要的.

所以,接口就像一个通用的规范,规范其实现者必须拥有这个规范的具体内容.

举个例子,每个人都需要有身份证,那么只要你有了身份证我们就可以称你为持有中华人名共和国居民身份证的公民,只要你是合法公民,就是进行银行贷款的前提,而不管你是男人,女人,老人,小孩--这就扩大了限制范围,也更通用了.

扩大了限制范围的好处就是提高代码的利用率.就拿前面的GridView.DataSource来说吧.

我们知道 List,数组,SqlDataSource都可以作为它的数据源,如果脱离了接口来写这个DataSource的属性就得这样:

public List<object> SqlDataSource{}

public object[] SqlDataSource{}

public SqlDataSource SqlDataSource{}

public XmlDataSource SqlDataSource{}

不但麻烦,而且缺乏扩展性.这个GridView的DataSource不就是需要一个可枚举的类型将之一个一个提取出来并且绑定显示就可以了嘛(管他来的人是中国的还是美国的,是黑人还是白人,只要是人,那么就具有其他动物不可比拟的智商水平一样的道理).

接口就在这时候起到了非常强大的作用,我们称其为—可扩展性强.

这时再会想下USB接口,我只管提供电源和数据通路,那么实现IUsbHub这个接口的东西也一定需要电源支持和与电脑进行数据交互.那么电脑就可以通过USB接口与连接在USB接口上的任何一个东东进行电源输送和数据交互.而不管差在USB接口上的东东到底能不能播放MP3—这明显不是电脑要担心的事情.

再说人就要懵了.希望能帮到你 :-)

如果有什么不对的地方欢迎指出,我会立即更正!