关于c#:了解接口

Understanding Interfaces

我仍然很难理解什么接口是好的。我读了一些教程,但我仍然不知道它们到底是为其他人准备的,然后"它们让你的类信守承诺"和"它们帮助实现多重继承"。

就是这样。我仍然不知道什么时候我会在一个实际的工作示例中使用一个接口,甚至不知道什么时候识别什么时候使用它。

从我对接口的有限知识来看,它们可以起到帮助作用,因为如果有东西实现了它,那么您就可以通过接口,允许像不同的类一样传入,而不必担心它不是正确的参数。

但我永远不知道这是什么真正的意义,因为他们通常在这一点上停下来,显示代码通过接口后会做什么,如果他们这样做,他们似乎没有做任何有用的事情,我可以看,然后去"哇,他们会在一个真实世界的例子帮助"。

所以我想我要说的是,我正试图找到一个现实世界的例子,在那里我可以看到实际的界面。

我也不明白你可以像引用这样的对象:

1
ICalculator myInterface = new JustSomeClass();

所以现在,如果我把我的接口点和intellisense拉上来,我只会看到接口方法,而不是JustsomeClass中的其他方法。所以我还没有看到这一点。

我也开始做单元测试,他们似乎喜欢使用接口,但我仍然不明白为什么。

比如这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public AuthenticationController(IFormsAuthentication formsAuth)
{
    FormsAuth = formsAuth ?? new FormsAuthenticationWrapper();
}

public class FormsAuthenticationWrapper : IFormsAuthentication
{
    public void SetAuthCookie(string userName, bool createPersistentCookie)
    {
        FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);
    }
    public void SignOut()
    {
        FormsAuthentication.SignOut();
    }
}

public IFormsAuthentication FormsAuth
{
    get;
    set;
}

比如为什么要制作这个界面?为什么不直接用其中的方法制作formsAuthenticationWrapper并将其命名为"一天"?为什么先创建一个接口,然后让包装器实现该接口,最后编写方法?

那我就不明白这句话到底是怎么说的。

就像我现在知道声明是这么说的

1
FormsAuth = formsAuth ?? new FormsAuthenticationWrapper();

如果formsauth为空,则生成一个新的formsauthenticationWrapper,然后将其分配给作为接口的属性。

我想这可以追溯到为什么参考物的整个观点。尤其是在这种情况下,因为所有的方法都是完全相同的。包装器没有接口没有的任何新方法,我也不确定,但是当您这样做时,方法被正确填充(即它们有一个主体),它们不会被转换为存根,因为这对我来说真的是毫无意义(它将被转换回接口)。

然后在测试文件中,他们有:

1
var formsAuthenticationMock = new Mock<AuthenticationController.IFormsAuthentication>();

所以他们只是在形式上通过身份验证,我猜所有的假存根都是假的。我猜包装器类在程序实际运行时使用,因为它有真正的方法可以做一些事情(比如签出一个人)。

但看看新的moq(moq),它接受一个类或一个接口。为什么不让包装器类先放入这些方法,然后再放入新的模拟调用中呢?

那不仅仅是为你做树桩吗?


好吧,一开始我也很难理解,所以别担心。

想想这个,如果你有一个班级,那就说是一个电子游戏角色。

1
2
3
public class Character
{
}

现在说我想让角色拥有武器。我可以使用一个接口来确定武器所需的方法:

1
2
3
4
interface IWeapon
{
    public Use();
}

所以让我们给角色一个武器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Character
{
    IWeapon weapon;

    public void GiveWeapon(IWeapon weapon)
    {
        this.weapon = weapon;
    }

    public void UseWeapon()
    {
        weapon.Use();
    }
}

现在我们可以创建使用iweapon接口的武器,我们可以将它们提供给任何字符类,并且该类可以使用该项。

1
2
3
4
5
6
7
public class Gun : IWeapon
{
    public void Use()
    {
        Console.Writeline("Weapon Fired");
    }
}

然后你可以把它粘在一起:

1
2
3
4
Character bob = new character();
Gun pistol = new Gun();
bob.GiveWeapon(pistol);
bob.UseWeapon();

现在这是一个一般的例子,但是它提供了很大的力量。如果您查找策略模式,您可以阅读更多关于这方面的内容。


接口定义合同。

在您提供的示例中,如果您将null传递给构造函数,并且实际上与接口没有任何关系,那么??运算符只提供一个默认值。

更相关的是,您可以使用实际的FormsAuthenticationWrapper对象,但是您也可以实现自己的IFormsAuthentication类型,这与包装类完全无关。该接口告诉您需要实现哪些方法和属性来实现契约,并允许编译器验证您的对象是否真的遵守了该契约(在某种程度上,按照名称而不是精神来遵守契约很简单),因此如果您不想这样做,您不必使用预建的FormsAuthenticationWrapper。您可以创建一个完全不同的类,但仍然遵守所需的合同。

在这方面,接口很像正常继承,有一个重要的区别。在C中,类只能从一个类型继承,但可以实现多个接口。因此,接口允许您在一个类中完成多个契约。一个对象可以是一个iformsauthentication对象,也可以是其他对象,比如ienumerable。

当您从另一个方向看接口时,它们甚至更有用:它们允许您将许多不同的类型视为它们都是相同的。各种集合类就是一个很好的例子。获取此代码示例:

1
2
3
4
5
6
7
void OutputValues(string[] values)
{
   foreach (string value in values)
   {
       Console.Writeline(value);
   }
}

它接受一个数组并将其输出到控制台。现在应用这个简单的更改来使用接口:

1
2
3
4
5
6
7
void OutputValues(IEnumerable<string> values)
{
   foreach (string value in values)
   {
       Console.Writeline(value);
   }
}

此代码仍然接受一个数组并将其输出到控制台。但它也需要一个List或其他任何你想给它实现IEnumerable的东西。因此,我们采用了一个接口,并使用它使一个简单的代码块更加强大。

另一个很好的例子是ASP.NET成员资格提供程序。您告诉ASP.NET通过实现所需的接口来履行成员资格合同。现在,您可以轻松地自定义内置的ASP.NET身份验证以使用任何源代码,这一切都归功于接口。System.Data命名空间中的数据提供程序以类似的方式工作。

最后一个注意事项:当我看到一个具有"默认"包装器实现的接口时,我认为它有点像ANIT模式,或者至少是代码味道。它向我表明,接口可能过于复杂,您要么需要将其拆分,要么考虑使用组合+事件+委托的组合,而不是派生来完成相同的事情。


也许最好的方法是使用一个来自.NET框架的例子来理解接口。

考虑以下功能:

1
2
3
4
5
void printValues(IEnumerable sequence)
{
    foreach (var value in sequence)
        Console.WriteLine(value);
}

现在我可以编写这个函数来接受Listobject[]或任何其他类型的具体序列。但是,由于我编写此函数是为了接受IEnumerable类型的参数,这意味着我可以将任何具体类型传递到实现IEnumerable接口的函数中。

之所以需要这样做,是因为通过使用接口类型,您的函数比其他类型更灵活。此外,您还增加了这个函数的实用性,因为许多不同的调用者可以在不需要修改的情况下使用它。

通过使用接口类型,您可以将参数的类型声明为从传入的任何具体类型所需的契约。在我的示例中,我不关心您传递给我的是什么类型,我只关心我可以迭代它。


这里的所有答案都很有帮助,我怀疑我能为这个组合添加任何新的东西,但是在阅读这里的答案时,两个不同答案中提到的两个概念在我的头脑中确实很好地啮合,所以我将在这里构建我的理解,希望它能帮助你。

类具有方法和属性,类的每个方法和属性都具有签名和正文。

1
2
3
4
public int Add(int x, int y)
{
return x + y;
}

add方法的签名是第一个大括号字符之前的所有内容

1
public int Add(int x, int y)

方法签名的目的是为方法分配一个名称,并描述其保护级别(公共、受保护、内部、私有和/或虚拟),该级别定义了从代码中访问方法的位置。

签名还定义了方法返回的值的类型,上面的add方法返回一个int,以及方法期望由调用方传递给它的参数。

方法通常被认为是一个对象可以做的事情,上面的例子意味着该方法在处理数字时定义的类

方法体(在代码中)精确地描述了对象如何执行由方法名描述的操作。在上面的示例中,add方法通过将addition运算符应用于其参数并重新计算结果来工作。

在语言语法方面,接口和类的主要区别之一是,接口只能定义methd的签名,而不能定义方法体。

换句话说,接口可以用类的操作(方法)来描述,但它不能描述如何执行操作。

既然您希望更好地理解接口是什么,那么我们可以继续讨论问题的第二部分和第三部分,什么时候,以及为什么我们要在真正的程序中使用接口。

在程序中使用接口的主要时间之一是,当一个人想要执行一个动作,而不想知道,或者与这些动作是如何执行的细节联系在一起。

对于grapsp来说,这是一个非常抽象的概念,因此也许有一个例子可以帮助您在头脑中巩固事物。

想象一下,你是一个非常受欢迎的网络浏览器的作者,每天有数百万人使用它,你有成千上万的用户请求的功能,一些大的,一些小的,一些好的,还有一些像"带回"和支持"。

因为您只有很少的开发人员,而且一天中的小时数甚至更少,所以您不可能自己实现每个请求的特性,但是您仍然希望满足客户的需求。

所以你决定允许用户开发他们自己的插件,这样他们就可以在奶牛回家之前使用cx1(2)。

要实现这一点,您可能会想到一个插件类,如下所示:

1
2
3
4
5
6
7
public class Plugin
{
public void Run (PluginHost browser)
{
//do stuff here....
}
}

但是你如何合理地实现这个方法呢?你不可能精确地知道未来每个poossible插件的工作原理。

解决此问题的一种可能方法是将插件定义为接口,并让浏览器使用该接口引用每个插件,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface IPlugin
{
void Run(PluginHost browser);
}

public class PluginHost
{
public void RunPlugins (IPlugin[] plugins)
{
foreach plugin in plugins
{
plugin.Run(this);
}
}
}

注意,正如前面讨论的,iplugin接口描述了run方法,但没有指定它是如何运行的,因为它是特定于每个插件的,我们不希望插件宿主关注每个插件的具体情况。

为了演示类和接口之间关系的"can-be-a"方面,我将在下面为插件主机编写一个实现标记的插件。

1
2
3
4
5
6
7
8
9
10
11
public class BlinkPlugin: IPlugin
{
private void MakeTextBlink(string text)
{
//code to make text blink.
}
public void Run(PluginHost browser)
{
MakeTextBlink(browser.CurrentPage.ParsedHtml);
}
}

从这个角度,你可以看到插件是在一个名为blinkplugin的类中定义的,但是由于它也实现了iplugin接口,它也可以被称为iplugin对象,就像上面的pluginhost类那样,因为它不知道或不关心类的实际类型,只是它可以是iplugin

我希望这有帮助,我真的不打算这么久。


我将在下面给您举一个例子,但让我从您的一个陈述开始。"我不知道如何识别何时使用"。把它放在边缘。您不需要确定何时使用它,而需要确定何时不使用它。任何参数(至少对公共方法)、任何(公共)属性(我个人会将列表扩展到其他属性)都应该声明为接口的某种内容,而不是特定的类。我唯一一次声明特定类型的东西是没有合适的接口。

我会去

1
IEnumerable<T> sequence;

当我能而且几乎不可能(我唯一能想到的是如果我真的需要foreach方法)

1
List<T> sequence;

现在是一个例子。假设您正在构建一个可以比较汽车和计算机价格的系统。每个都显示在一个列表中。

汽车价格是从一组网站和计算机价格从一组服务中获取的。

解决方案可能是:创建一个网页,例如使用DataGrid和IDataRetriever的依赖项注入(IDataRetriever是一个接口,它使数据获取变得可用,而不必知道数据来自哪里(DB、XML、Web服务或…)或如何获取(数据挖掘、SQL查询内部数据或从文件中读取)。

由于这两个场景,我们只有一个普通的超级类的用法,所以没有任何意义。但是使用我们的两个类(一个用于汽车,一个用于计算机)的页面需要在两种情况下执行完全相同的操作,以使这成为可能,我们需要告诉页面(编译器)哪些操作是可能的。我们通过一个接口来实现这一点,然后这两个类实现了这个intercae。

使用依赖注入与何时或如何使用接口无关,但我之所以包括它是另一个常见的场景,其中接口使您的生活更容易。测试。如果使用注入和接口,那么在测试时可以很容易地用生产类替换测试类。(这也可能是为了切换数据存储或强制执行在发布代码中可能很难产生的错误,例如竞争条件)


几乎所有关于接口的东西都写了,让我试一试。

简单地说,接口是将两个或多个类联系起来的东西,否则,是非相关类。

接口定义契约,以确保任何两个或多个类(即使不是完全相关的)碰巧实现一个公共接口,将包含一组公共操作。

结合多态性的支持,可以使用接口编写更清晰的动态代码。

如。

界面生物

--speak()//表示任何一个活着的人都需要定义他们的说话方式

类狗实现生命

--speak()bark;//作为狗说话的实现

类鸟实现生命

--speak()chirp;//将speak作为bird的实现


我的理解是:

派生关系是"IS-A"关系,例如,狗是动物,牛是动物,但接口从未派生,而是实现的。

因此,接口是一种"可以"的关系,例如,狗可以是间谍狗,狗可以是马戏团狗等。但要做到这一点,狗必须学习一些具体的东西。在OO术语中,这意味着如果类实现了一个接口,它必须能够做一些特定的事情(它们称之为契约)。例如,如果您的类实现IEnumerable,那么它显然意味着您的类具有(必须具有)这样一种功能,可以枚举它的对象。

因此,本质上,通过接口实现,类向其用户公开一个功能,它可以做一些事情,而不是继承。


我们使用接口(或抽象基类)来允许多态性,这是面向对象编程中非常重要的概念。它允许我们以非常灵活的方式组合行为。如果你还没有,你应该阅读设计模式——它包含了许多使用接口的例子。

对于测试双精度(例如模拟对象),我们使用接口来删除当前不想测试的功能,或者在单元测试框架内无法工作的功能。

尤其是在使用Web开发时,当代码在Web上下文之外执行时,我们所依赖的许多服务(如HTTP上下文)都不可用,但是如果我们将该功能隐藏在接口后面,我们可以在测试期间用其他东西替换它。


1
2
ICalculator myInterface = new JustSomeClass();
JustSomeClass myObject = (JustSomeClass) myInterface;

现在您有两个"接口"来处理对象。

我对这个也很陌生,但我喜欢把界面看作是遥控器上的按钮。当使用iCalculator接口时,您只能访问接口设计器指定的按钮(功能)。当使用justsomeClass对象引用时,您有另一组按钮。但它们都指向同一个物体。

这样做有很多原因。对我来说最有用的是同事之间的交流。如果他们可以在一个界面上达成一致(按钮将被按下),那么一个开发人员可以实现按钮的功能,另一个开发人员可以编写使用按钮的代码。

希望这有帮助。