关于c#:风格上的差异:IDictionary与Dictionary

A difference in style: IDictionary vs Dictionary

我有一个朋友,他刚刚在Java开发了很多年后才进入.NET开发,在看过他的一些代码后,我注意到他经常这样做:

1
IDictionary<string, MyClass> dictionary = new Dictionary<string, MyClass>();

他将字典声明为接口而不是类。通常我会做以下工作:

1
Dictionary<string, MyClass> dictionary = new Dictionary<string, MyClass>();

我只在需要时使用IDictionary接口(例如,将字典传递给接受IDictionary接口的方法)。

我的问题是:他的做事方式有什么优点吗?在Java中这是常见的做法吗?


如果IDictionary是比Dictionary更通用的类型,那么在声明变量时使用更通用的类型是有意义的。这样,您就不必太在意分配给变量的实现类,将来您可以轻松地更改类型,而无需更改大量后续代码。例如,在Java中,通常认为做得更好。

1
List<Integer> intList=new LinkedList<Integer>();

比现在要做的还要多

1
LinkedList<Integer> intList=new LinkedList<Integer>();

这样,我确信所有下面的代码都将列表视为一个列表而不是LinkedList,这使得将来很容易为Vector或任何其他实现列表的类切换LinkedList。我认为这对于Java和一般的编程来说是很常见的。


这种做法不仅仅局限于Java。

它通常也用于.NET中,当您想从正在使用的类中分离对象的实例时。如果使用接口而不是类,则可以在需要时更改支持类型,而不必破坏其余代码。

您还将看到在处理IOC容器和使用工厂模式的声明时大量使用了这种实践。


你的朋友遵循了非常有用的原则:

"从实现细节中抽象自己"


对于已经是实现细节的局部变量和私有字段,使用具体类型比使用接口来声明要好,因为具体类提供了性能提升(直接调度比虚拟/接口调度更快)。如果不在本地实现细节中不必要地强制转换为接口类型,那么JIT还可以更容易地内联方法。如果从返回接口的方法返回具体类型的实例,则强制转换是自动的。


您应该总是尝试编程到接口,而不是具体的类。

在Java或任何其他面向对象的编程语言。

在.NET世界中,通常使用I来表示这是您正在使用的接口。我认为这更常见,因为在C中,它们没有用于引用类与接口继承的implementsextends

我想乳清可以打字

1
2
3
 class MyClass:ISome,Other,IMore
 {
 }

你可以告诉ISomeIMore是接口,而Other是类

在Java中不需要这样的东西

1
2
 class MyClass extends Other implements Some, More {
 }

这个概念仍然适用,您应该尝试对接口进行编码。


就我所见,Java开发人员往往比.NET开发人员更频繁地使用抽象(和设计模式)。这似乎是另一个例子:当实质上只使用接口成员时,为什么要声明具体的类?


通常,您会看到当成员暴露于外部代码时使用的接口类型(IDictionary),无论是在程序集外部还是在类外部。通常,大多数开发人员在类定义内部使用具体类型,而使用接口类型公开封装的属性。通过这种方式,它们可以利用具体类型的功能,但是如果它们更改了具体类型,那么声明类的接口就不需要更改。

1
2
3
4
5
6
7
8
public class Widget
{
    private Dictionary<string, string> map = new Dictionary<string, string>();
    public IDictionary<string, string> Map
    {
        get { return map; }
    }
}

以后会变成:

1
2
3
4
5
6
7
8
9
10
class SpecialMap<TKey, TValue> : IDictionary<TKey, TValue> { ... }

public class Widget
{
    private SpecialMap<string, string> map = new SpecialMap<string, string>();
    public IDictionary<string, string> Map
    {
        get { return map; }
    }
}

不需要改变小部件的接口,也不需要改变已经在使用它的其他代码。


来自Java世界,我同意"编程到接口"的咒语被钻入你的体内。通过编程到一个接口,而不是一个实现,您可以使您的方法更加可扩展,以满足未来的需求。


Java集合包括大量的实现。因此,我更容易利用

1
List<String> myList = new ArrayList<String>();

然后在将来,当我意识到我需要"mylist"是线程安全的,只需将这一行更改为

1
List<String> myList = new Vector<String>();

不要更改其他代码行。这也包括getter/setter。例如,如果您查看map的实现数量,您可以想象为什么这可能是一个好的实践。在其他语言中,只有两种实现(对不起,不是一个大的.NET用户),但是在Objective-C中,实际上只有nsDictionary和nsMutableDictionary。所以,这没什么意义。

编辑:

未能达到我的一个关键点(只是用getter/setter暗示)。

以上允许您:

1
2
3
public void setMyList(List<String> myList) {
    this.myList = myList;
}

使用此调用的客户机不必担心底层实现。使用符合列表接口的任何对象。


在所描述的情况下,几乎每个Java开发人员都会使用接口来声明变量。使用Java集合的方式可能是最好的例子之一:

1
2
Map map = new HashMap();
List list = new ArrayList();

我猜它只是在很多情况下实现了松耦合。


我遇到了与Java开发人员相同的情况。他以相同的方式将集合和对象实例化到它们的接口。例如,

1
IAccount account = new Account();

属性总是作为接口获取/设置。这会导致序列化出现问题,这里很好地解释了这一点。


使用接口意味着下面代码中的"dictionary"可能是IDictionary的任何实现。

1
2
Dictionary1 dictionary = new Dictionary1();
dictionary.operation1(); // if operation1 is implemented only in Dictionary1() this will fail for every other implementation

当您隐藏对象的结构时,最好看到:

1
IDictionary dictionary = DictionaryFactory.getDictionary(...);


我发现对于局部变量来说,使用接口或具体的类一般都不重要。

与类成员或方法签名不同的是,如果决定更改类型,重构工作很少,而且变量在其使用站点之外也不可见。事实上,当您使用var声明局部变量时,您得到的不是接口类型而是类类型(除非显式转换到接口)。

但是,在声明方法、类成员或接口时,我认为预先使用接口类型,而不是将公共API耦合到特定的类类型,这会让您省去不少麻烦。