if (!ilist.IsReadOnly)
{
ilist.Add(4);
ilist.Insert(0, 0);
ilist.Remove(3);
ilist.RemoveAt(0);
}
else
{
// what were you planning to do if you were given a read only list anyway?
}
如果有人通过了一个数组或一个列表,如果你每次都检查一下这个标志并且有一个后备,你的代码就可以正常工作......但是真的;那样做的?唐't you know in advance if your method needs a list that can take additional members; don' t你在方法签名中指定了吗?如果你通过像 int[] 这样的只读列表,你究竟打算做什么?
The interface ensures that you at least get the methods you are expecting ;了解界面的定义,即 . 所有抽象方法都由继承该接口的任何类实现 . 所以,如果有人使用几种方法创建了他自己的大类,除了他从接口继承的一些附加功能,并且那些对你没用,最好使用对子类的引用(在这种情况下,接口)并为其分配具体的类对象 .
18 回答
如果您通过其他人将使用的库公开您的类,您通常希望通过接口而不是具体实现来公开它 . 如果您决定稍后更改类的实现以使用其他具体类,这将有所帮助 . 在这种情况下,您的库的用户将不需要更新他们的代码,因为接口不会更改 .
如果您只是在内部使用它,您可能不在乎,并且使用
List<T>
可能没问题 .不那么受欢迎的答案是程序员喜欢假装他们的软件将在全世界重复使用,当事实上大多数项目将由少量人保持,然而与界面相关的声音很好,你就是在欺骗你自己 .
Architecture Astronauts . 你有可能编写自己的IList,它可以为已经在.NET框架中添加任何内容添加任何东西,这是非常遥远的,它是为"best practices"保留的理论果冻小点 .
显然,如果你被问到你在面试中使用了什么,你会说IList,微笑,并且两个人都很高兴自己这么聪明 . 或者面向公众的API,IList . 希望你明白我的意思 .
Interface is a promise (或 Contract ) .
因为它总是与承诺 - smaller the better .
有人说“总是使用
IList<T>
而不是List<T>
” .他们希望您将方法签名从
void Foo(List<T> input)
更改为void Foo(IList<T> input)
.这些人错了 .
它比那更细致入微 . 如果您将
IList<T>
作为公共接口的一部分返回到您的库中,那么您可能会留下一些有趣的选项,以便将来制作自定义列表 . 你可能永远不需要那个选项,但是's an argument. I think it'是返回接口而不是具体类型的整个参数 . 值得一提,但在这种情况下它有一个严重的缺陷 .作为次要的反驳,您可能会发现每个调用者都需要
List<T>
,并且调用代码中充满了.ToList()
But far more importantly, 如果您接受IList作为参数,最好小心,因为
IList<T>
和List<T>
的行为方式不同 . 尽管名称相似,但尽管共享了界面,但他们仍然会公开相同的 Contract .假设你有这个方法:
一位乐于助人的同事"refactors"接受
IList<int>
的方法 .您的代码现在已损坏,因为
int[]
实现了IList<int>
,但其大小固定 .ICollection<T>
(IList<T>
的基础)的 Contract 要求使用它的代码在尝试添加或删除集合中的项目之前检查IsReadOnly
标志 .List<T>
的 Contract 没有 .Liskov替换原则(简化)规定派生类型应该能够用于代替基类型,而不需要额外的前置条件或后置条件 .
这感觉它打破了Liskov替代原则 .
但事实并非如此 . 对此的答案是该示例使用了IList <T> / ICollection <T>错误 . 如果使用ICollection <T>,则需要检查IsReadOnly标志 .
如果有人通过了一个数组或一个列表,如果你每次都检查一下这个标志并且有一个后备,你的代码就可以正常工作......但是真的;那样做的?唐't you know in advance if your method needs a list that can take additional members; don' t你在方法签名中指定了吗?如果你通过像
int[]
这样的只读列表,你究竟打算做什么?您可以将
List<T>
替换为正确使用IList<T>
/ICollection<T>
的代码 . 您无法保证可以将IList<T>
/ICollection<T>
替换为使用List<T>
的代码 .在许多使用抽象而不是具体类型的参数中,单一责任原则/接口隔离原则是有吸引力的 - 取决于最窄的可能接口 . 在大多数情况下,如果您使用
List<T>
并且您认为可以使用更窄的界面 - 为什么不IEnumerable<T>
?如果您不需要添加项目,这通常更合适 . 如果需要添加到集合中,请使用具体类型List<T>
.对我来说
IList<T>
(和ICollection<T>
)是.NET框架中最糟糕的部分 .IsReadOnly
违反了最不惊讶的原则 . 从不允许添加,插入或删除项的类(如Array
)不应使用Add,Insert和Remove方法实现接口 . (另见https://softwareengineering.stackexchange.com/questions/306105/implementing-an-interface-when-you-dont-need-one-of-the-properties)是
IList<T>
非常适合您的组织?如果同事要求您更改方法签名以使用IList<T>
而不是List<T>
,请询问他们如何将元素添加到IList<T>
. 如果他们不知道IsReadOnly
(大多数人都不会IList<T>
. 永远 .请注意,IsReadOnly标志来自ICollection <T>,并指示是否可以从集合中添加或删除项目;但只是为了真正混淆事物,它并不表示它们是否可以被替换,在Arrays的情况下(返回IsReadOnlys == true)可以 .
有关IsReadOnly的更多信息,请参阅msdn definition of ICollection<T>.IsReadOnly
List<T>
是IList<T>
的特定实现,它是一个容器,可以使用整数索引以与线性数组T[]
相同的方式进行寻址 . 如果指定IList<T>
作为方法参数的类型,则只指定需要容器的某些功能 .例如,接口规范不强制使用特定的数据结构 .
List<T>
的实现在访问,删除和添加元素作为线性数组时具有相同的性能 . 但是,您可以想象一个由链接列表支持的实现,对于该实现,向末尾添加元素更便宜(恒定时间)但随机访问更加昂贵 . (请注意,.NETLinkedList<T>
未实现IList<T>
. )此示例还告诉您,可能存在需要在参数列表中指定实现而非接口的情况:在此示例中,每当您需要特定的访问性能特征时 . 这通常可以保证容器的特定实现(
List<T>
documentation:“它使用一个数组实现IList<T>
泛型接口,该数组的大小根据需要动态增加 . ”) .此外,您可能希望考虑公开所需的最少功能 . 例如 . 如果您不需要更改列表的内容,您应该考虑使用
IEnumerable<T>
,其中IList<T>
已扩展 .我会稍微改变一下这个问题,而不是证明为什么你应该在具体实现上使用接口,试图证明你为什么要使用具体的实现而不是接口 . 如果你无法证明这一点,请使用界面 .
IList <T>是一个接口,因此您可以继承另一个类并仍然实现IList <T>,而继承List <T>会阻止您这样做 .
例如,如果有一个A类,而你的B类继承它,那么就不能使用List <T>
TDD和OOP的原理通常是编程到接口而不是实现 .
在这个特定的情况下,因为你基本上是在谈论一个语言结构,而不是一个自定义语言结构,它通常无关紧要,但比如说你发现List不支持你需要的东西 . 如果您在应用程序的其余部分中使用了IList,则可以使用自己的自定义类扩展List,并且仍然可以在不进行重构的情况下传递它 .
这样做的成本微乎其微,为什么不在以后省去头痛呢?这就是接口原理的全部意义所在 .
在这种情况下,您可以传入任何实现IList <Bar>接口的类 . 如果您使用List <Bar>,则只能传入List <Bar>实例 .
IList <Bar>方式比List <Bar>方式更松散耦合 .
假设这些列表与IList问题(或答案)都没有提到签名差异 . (这就是我在SO上搜索这个问题的原因!)
所以这里是List包含的方法,在IList中找不到,至少从.NET 4.5开始(大约2015年)
AddRange
AsReadOnly
BinarySearch
容量
ConvertAll
存在
查找
FindAll
FindIndex
FindLast
FindLastIndex
ForEach
GetRange
InsertRange
LastIndexOf
删除所有
RemoveRange
反向
排序
ToArray
TrimExcess
TrueForAll
在实现中使用接口的最重要的情况是API的参数 . 如果您的API采用List参数,那么使用它的任何人都必须使用List . 如果参数类型是IList,那么调用者可以更自由,并且可以使用您从未听说过的类,这些类在您的代码编写时甚至可能不存在 .
如果.NET 5.0替换了怎么办?
System.Collections.Generic.List<T>
至System.Collection.Generics.LinearList<T>
. .NET始终拥有名称List<T>
,但他们保证IList<T>
是 Contract . 所以恕我直言,我们(至少我)不应该使用别人的名字(虽然在这种情况下它是.NET)并且以后会遇到麻烦 .在使用
IList<T>
的情况下,调用者始终需要保证工作,并且实现者可以自由地将基础集合更改为IList
的任何其他具体实现 .所有概念基本上都在上面的大多数答案中陈述,为什么使用接口而不是具体实现 .
IList<T>
MSDN link添加
清除
包含
CopyTo
GetEnumerator
IndexOf
插入
删除
RemoveAt
List<T>
实现了这九种方法(不包括扩展方法),最重要的是它有大约41种公共方法,这会考虑您在应用程序中使用哪种方法 .List<T>
MSDN link您可能会因为定义IList或ICollection而打开接口的其他实现 .
您可能希望拥有一个IOrderRepository,用于在IList或ICollection中定义订单集合 . 然后,您可以使用不同类型的实现来提供订单列表,只要它们符合IList或ICollection定义的“规则”即可 .
The interface ensures that you at least get the methods you are expecting ;了解界面的定义,即 . 所有抽象方法都由继承该接口的任何类实现 . 所以,如果有人使用几种方法创建了他自己的大类,除了他从接口继承的一些附加功能,并且那些对你没用,最好使用对子类的引用(在这种情况下,接口)并为其分配具体的类对象 .
另外一个优点是你的代码对于具体类的任何更改都是安全的,因为你只订阅了具体类的几个方法,只要具体类继承自你的接口,那些就会存在 . 使用 . 所以它对你的安全和编程人员的自由,他正在编写具体的实现来改变或增加他的具体课程的更多功能 .
根据其他海报的建议,IList <>几乎总是优选的,但是当使用WCF DataContractSerializer通过一个以上的序列化/反序列化循环运行IList <>时,请注意there is a bug in .NET 3.5 sp 1 .
现在有一个SP来修复这个bug:KB 971030
您可以从几个角度来看这个论点,包括一个纯粹的OO方法,它说的是针对接口而不是实现进行编程 . 有了这个想法,使用IList遵循相同的原则,即传递和使用从头定义的接口 . 我也相信接口提供的可扩展性和灵活性因素 . 如果需要扩展或更改实现IList <T>的类,则不必更改使用代码;它知道IList接口 Contract 遵守的内容 . 但是,如果在更改的类上使用具体实现和List <T>,则可能导致调用代码也需要更改 . 这是因为遵循IList <T>的类保证了使用List <T>的具体类型无法保证的某种行为 .
也有能力做一些事情,比如在类上修改List <T>的默认实现,实现IList <T>,比如.Add,.Remove或任何其他IList方法,给开发人员带来很大的灵活性和功能,否则预定义通过List <T>
通常,一种好的方法是在面向公众的API中使用IList(在适当的时候,需要列表语义),然后在内部使用List来实现API . 这允许您更改为IList的不同实现,而不会破坏使用您的类的代码 .
可以在下一个.net框架中更改类名列表,但是接口永远不会随着接口的变化而改变 .
请注意,如果您的API仅用于foreach循环等,那么您可能需要考虑仅公开IEnumerable .