为什么使用IList或List?
我知道有很多这方面的post,但它仍然混淆了我为什么要传递像IList这样的接口,并返回像IList的接口而不是具体的列表。
我阅读了大量的文章,说明如何更容易地改变实现,但我不完全看到这是如何工作的。
说如果我有这个方法
public class SomeClass { public bool IsChecked { get; set; } } public void LogAllChecked(IList<SomeClass> someClasses) { foreach (var s in someClasses) { if (s.IsChecked) { // log } } }
我不确定将来如何通过使用IList来帮助我。
如果我已经在方法中呢? 我还应该使用IList吗?
public void LogAllChecked(IList<SomeClass> someClasses) { //why not List<string> myStrings = new List<string>() IList<string> myStrings = new List<string>(); foreach (var s in someClasses) { if (s.IsChecked) { myStrings.Add(s.IsChecked.ToString()); } } }
我现在使用IList得到什么?
public IList<int> onlySomeInts(IList<int> myInts) { IList<int> store = new List<int>(); foreach (var i in myInts) { if (i % 2 == 0) { store.Add(i); } } return store; }
现在怎么样? 是否有一些int的列表的新的实现,我将需要改变?
所以基本上我需要看到一些实际的代码示例,说明如何在使用IList的情况下解决某些问题,而不是仅仅将List引入到所有问题中。
从我的阅读,我认为我可以使用IEnumberable而不是IList,因为我只是循环的东西。
编辑所以我一直在玩我的一些方法如何做到这一点。 我仍然不确定返回types(如果我应该使它更具体或一个接口)
public class CardFrmVm { public IList<TravelFeaturesVm> TravelFeaturesVm { get; set; } public IList<WarrantyFeaturesVm> WarrantyFeaturesVm { get; set; } public CardFrmVm() { WarrantyFeaturesVm = new List<WarrantyFeaturesVm>(); TravelFeaturesVm = new List<TravelFeaturesVm>(); } } public class WarrantyFeaturesVm : AvailableFeatureVm { } public class TravelFeaturesVm : AvailableFeatureVm { } public class AvailableFeatureVm { public Guid FeatureId { get; set; } public bool HasFeature { get; set; } public string Name { get; set; } } private IList<AvailableFeature> FillAvailableFeatures(IEnumerable<AvailableFeatureVm> avaliableFeaturesVm) { List<AvailableFeature> availableFeatures = new List<AvailableFeature>(); foreach (var f in avaliableFeaturesVm) { if (f.HasFeature) { // nhibernate call to Load<>() AvailableFeature availableFeature = featureService.LoadAvaliableFeatureById(f.FeatureId); availableFeatures.Add(availableFeature); } } return availableFeatures; }
所以我现在正在返回IList的一个简单的事实,我将然后添加到我的域模型有这样的属性
public virtual IList<AvailableFeature> AvailableFeatures { get; set; }
以上是IList本身,因为这似乎是与nhibernate一起使用的标准。 否则,我可能已经返回IEnumberable但不知道。 仍然无法弄清楚用户会100%需要什么(这是返回一个混凝土有优势的地方)。
编辑2
我也在想,如果我想在我的方法中通过引用传递会发生什么?
private void FillAvailableFeatures(IEnumerable<AvailableFeatureVm> avaliableFeaturesVm, IList<AvailableFeature> toFill) { foreach (var f in avaliableFeaturesVm) { if (f.HasFeature) { // nhibernate call to Load<>() AvailableFeature availableFeature = featureService.LoadAvaliableFeatureById(f.FeatureId); toFill.Add(availableFeature); } } }
我会遇到这个问题吗? 既然他们不能通过一个数组(具有固定的大小)? 也许是更好的一个具体名单?
这里有三个问题:我应该使用什么types的forms参数? 我应该使用什么局部variables? 我应该使用什么types的返回types?
forms参数:
这里的原则是不要求超过你的需要 。 IEnumerable<T>
通信“我需要从头到尾得到这个序列的元素”。 IList<T>
沟通“我需要以任意顺序获取和设置这个序列的元素”。 List<T>
通信“我需要以任意顺序获取和设置这个序列的元素,我只接受列表;我不接受数组。
(1)让来电者做不必要的工作来满足你不必要的要求,(2)向读者传达虚假信息。 只问你要用什么。 这样,如果调用者有一个序列,他们不需要调用ToList来满足你的需求。
局部variables:
使用任何你想要的。 这是你的方法。 你是唯一一个看到方法的内部实现细节的人。
返回types:
和以前一样的原理颠倒过来。 提供您的呼叫者要求的最低限度。 如果调用者只需要枚举序列的能力,只给它们一个IEnumerable<T>
。
我见过的最实际的原因是由Jeffrey Richter通过C#在CLR中给出的。
该模式是采取最基本的类或接口可能为您的参数,并返回可能为您的返回types最具体的类或接口 。 这为您的调用者提供了将types传递给您的方法的最大灵活性以及最大的投射/重用返回值的机会。
例如,下面的方法
public void PrintTypes(IEnumerable items) { foreach(var item in items) Console.WriteLine(item.GetType().FullName); }
允许该方法被称为传递的任何types,可以投给一个枚举 。 如果你更具体
public void PrintTypes(List items)
然后说,如果你有一个数组,并希望将他们的types名称打印到控制台,你首先必须创build一个新的列表,并填写你的types。 而且,如果您使用的是通用实现,则只能使用仅适用于特定types对象的任何对象的方法。
当谈到返回types时,你越具体,灵活的呼叫者可以使用它。
public List<string> GetNames()
您可以使用此返回types来迭代名称
foreach(var name in GetNames())
或者你可以直接索引到集合中
Console.WriteLine(GetNames()[0])
而如果你回来了一个不太具体的types
public IEnumerable GetNames()
你将不得不按摩返回types以获得第一个值
Console.WriteLine(GetNames().OfType<string>().First());
IEnumerable<T>
允许你迭代一个集合。 ICollection<T>
build立在这个基础上,也允许添加和删除项目。 IList<T>
也允许访问和修改它们在一个特定的索引。 通过暴露你期望你的消费者使用的那个,你可以自由地改变你的实现。 List<T>
碰巧实现了这三个接口。
如果您将您的财产公开为List<T>
,甚至是IList<T>
,那么当您希望您的消费者拥有的是您可以遍历该集合的能力时。 然后,他们可以依靠他们可以修改列表的事实。 然后,如果您决定将实际的数据存储从List<T>
转换为Dictionary<T,U>
,并将字典键公开为属性的实际值(之前我不得不这样做),那么稍后如果。 那么谁来预期他们的变化会反映在你的class级里面的消费者将不再有这种能力。 这是个大问题! 如果将List<T>
公开为IEnumerable<T>
,则可以轻松地预测您的集合没有被外部修改。 这是将List<T>
公开为上述任何接口的权力之一。
当它属于方法参数时,这个抽象级别变成另一个方向。 当您将列表传递给接受IEnumerable<T>
的方法时,您可以确定您的列表不会被修改。 当你是实现该方法的人,并且你说你接受一个IEnumerable<T>
因为你需要做的就是迭代这个列表。 然后调用该方法的人可以自由地调用任何可枚举的数据types。 这可以让您的代码以意想不到的,但完全有效的方式使用。
从这里可以看出,你的方法实现可以代表你的本地variables。 实现细节没有公开。 让您可以自由地将代码更改为更好的代码,而不会影响调用代码的人员。
你无法预测未来。 假设一个属性的types总是有益的,因为一个List<T>
会立即限制你适应代码无法预料的期望的能力。 是的,您可能永远不会从List<T>
更改该数据types,但是如果必须的话,您可以确定。 你的代码已经准备好了。
简答:
你传递接口,不pipe你用什么接口的具体实现,你的代码都会支持它。
如果您使用列表的具体实现,您的代码将不支持同一列表的另一个实现。
读一点inheritance和多态性 。
下面是一个例子:我曾经有一个项目,在这个项目中,我们的列表变得非常庞大,造成大型对象堆的碎片化,从而影响了性能。 我们用LinkedListreplace了List。 LinkedList不包含数组,所以突然之间,我们几乎没有使用大对象堆。
大多数情况下,我们使用IEnumerable<T>
作为列表,所以不需要进一步的修改。 (是的,如果你只是在枚举它们,我会build议将引用声明为IEnumerable。)在几个地方,我们需要列表索引器,所以我们在链表中写了一个效率低下的IList<T>
包装器。 我们不经常需要列表索引器,所以低效率不成问题。 如果是这样的话,我们可以提供一些其他IList的实现,也许是作为一个足够小的数组的集合,这样可以更有效地索引,同时避免大对象。
最后,您可能需要以任何理由replace实现; 性能只是一种可能性。 无论什么原因,当您更改对象的特定运行时types时,使用可能派生最less的types将减less对代码进行更改的需要。
在这个方法里面,你应该使用var
而不是IList
或者List
。 当您的数据源更改为来自某个方法时,您的onlySomeInts
方法将生存。
使用IList
而不是List
作为参数的原因是因为很多东西都实现了IList
(List和[],两个例子),但是只有一件事情实现了List
。 编码到接口更灵活。
如果你只是枚举值,你应该使用IEnumerable
。 每种可以容纳多个值的数据types都实现了IEnumerable
(或者应该),使得你的方法非常灵活。
使用IList而不是List使编写unit testing变得更容易。 它允许你使用“模拟”库来传递和返回数据。
使用接口的另一个一般原因是向对象的用户公开最less量的知识。
考虑(做作)的情况,我有一个实现IList的数据对象。
public class MyDataObject : IList<int> { public void Method1() { ... } // etc }
上面的函数只关心能够迭代列表。 理想情况下,他们不需要知道谁实现了这个列表或者他们是如何实现的。
在你的例子中,IEnumerable是你想象的更好的select。
尽可能减less代码之间的依赖关系总是一个好主意。
考虑到这一点,最有意义的是传递具有最less数量的外部依赖关系的types,并返回相同的结果。 但是,这可能会有所不同,取决于您的方法和他们的签名的可见性。
如果你的方法构成一个接口的一部分,这些方法将需要使用该接口可用的types来定义。 具体types可能无法用于接口,所以它们将不得不返回非具体types。 例如,如果你正在创build一个框架,你会想要这样做。
然而,如果你没有编写框架,那么传递最弱可能types的参数(即基类,接口甚至委托)和返回具体types可能是有利的。 这使得调用者能够尽可能多地对返回的对象进行操作,即使它被作为一个接口进行转换。 但是,这会使方法更脆弱,因为对返回对象types的任何更改都可能会破坏调用代码。 但实际上,这通常不是一个主要问题。
您接受一个接口作为方法的参数,因为它允许调用者提交不同的具体types作为参数。 给定您的示例方法LogAllChecked,参数someClasses可以是各种types的,对于编写该方法的人来说,所有参数都可能是等价的(即,不pipe参数的types如何,您都会编写完全相同的代码)。 但是对于调用这个方法的人来说,这可能会产生巨大的影响 – 如果他们有一个数组并且要求一个列表,那么无论何时调用该方法,都必须将该数组更改为列表或vv。从程序员和性能POV的时间。
无论你返回一个接口还是一个具体types,都取决于你想让你的调用者用你创build的对象做什么 – 这是一个APIdevise决定,并没有硬性规定。 你必须权衡自己的能力,充分利用对象的能力,轻松使用部分对象function(当然,你是否希望他们充分利用对象)。 例如,如果你返回一个IEnumerable,那么你限制它们迭代 – 它们不能从你的对象中添加或删除项目,它们只能对对象起作用。 如果您需要在类之外公开集合,但不希望让调用者更改集合,则可以这样做。 另一方面,如果您要返回一个您期望/希望它们填充的空集合,那么IEnumerable是不合适的。
这是我在这个.NET 4.5 +世界的答案。
使用IList <T>和IReadonlyList <T> ,
而不是List <T> ,因为ReadonlyList <T>不存在。
IList <T>与IReadonlyList <T>看起来非常一致
- 如果foreach是唯一使用它的方法,请使用IEnumerable <T>获取最小暴露(属性)或需求(参数)。
- 如果您还需要公开/使用Count和[]索引器,请使用IReadonlyList <T> 。
- 如果您还允许调用者添加/更新/删除元素,请使用IList <T>
因为List <T>实现了IReadonlyList <T> ,所以不需要任何明确的转换。
示例类:
// manipulate the list within the class private List<int> _numbers; // callers can add/update/remove elements, but cannot reassign a new list to this property public IList<int> Numbers { get { return _numbers; } } // callers can use: .Count and .ReadonlyNumbers[idx], but cannot add/update/remove elements public IReadOnlyList<int> ReadonlyNumbers { get { return _numbers; } }