为什么要使用IList或List?


82

我知道有很多关于此的文章,但它仍然让我感到困惑,为什么您应该传入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,因为我只是在遍历所有内容。

编辑 因此,我一直在尝试一些方法。我仍然不确定返回类型(如果我应该使其更具体或更接口)。

 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);
                    }
                }
            }

我会遇到这个问题吗?由于它们不能传递数组(大小固定)吗?列出具体清单会更好吗?

Answers:


156

这里有三个问题:形式参数应该使用哪种类型?局部变量应该使用什么?返回类型应该使用什么?

形式参数:

这里的原则是不要求超出您的需求IEnumerable<T>传达“我需要从头到尾获取此序列的元素”。IList<T>传达“我需要以任意顺序获取和设置此序列的元素”。List<T>交流“我需要以任意顺序获取和设置此序列的元素,并且我只接受列表;我不接受数组。”

通过索要超出您所需的资源,您(1)使呼叫者进行不必要的工作来满足您的不必要需求,并且(2)向阅读者传达虚假信息。仅询问您将要使用的内容。这样,如果调用方具有序列,则无需在其上调用ToList即可满足您的需求。

局部变量:

使用任何您想要的。这是你的方法。您是唯一看到该方法的内部实现详细信息的人。

返回类型:

与以前相同的原理,相反。提供您的呼叫者所需的最低要求。如果呼叫者仅需要枚举序列的功能,请仅给他们一个IEnumerable<T>


9
您怎么知道呼叫者需要什么。例如,我将我的一种返回类型切换为IList <>,那么我很可能只想枚举它们就让我返回一个IEnumberable即可。然后,我查看了自己的视图(mvc),发现实际上我需要使用count方法,就像使用for循环一样。因此,在我自己的应用程序中,我估计了我实际需要的东西,您如何预计别人需要或不需要的东西。
chobo2 2012年

@ chobo2:在您的特定示例中,Count如果您IEnumerable是,LINQ在O(1)中工作ICollection。当然,您也可以使用foreach循环。
布赖恩

6
@ chobo2:那么,您如何预计呼叫者将需要什么方法?这似乎是首先要解决的问题。大概您以某种方式知道要为要打电话给他们的人写什么方法。问那些人他们想要返回什么方法。您的问题从根本上说是“我怎么知道要编写什么软件?” 您可以通过了解客户必须解决的问题并编写解决问题的代码来知道。
埃里克·利珀特

1
@Eric-您应该更新形式参数的答案,以包含仅需要添加项目的ICollection示例。同样,您的返回类型说明应遵循“仅提供允许呼叫者执行的功能的最低限度”的内容。如果它是一个只读列表只返回了IEnumerable等
查尔斯·兰伯特

4
@kvb:差不多了。考虑您的第一种情况。您可以通过做算法上的改进items as IList<T>,如果非零,请使用改进的代码。这样,如果你可以在你充分利用,同时仍然允许他们通过什么样的客户端的灵活性。
埃里克利珀

33

我见过的最实际的原因是Jeffrey Richter通过C#在CLR中给出的。

该模式是采取卑鄙的类或接口可能为您的参数和返回的最具体的类或接口可能为您的返回类型。这使您的调用者在将类型传递给您的方法时具有最大的灵活性,并提供了最多的机会来转换/重用返回值。

例如下面的方法

public void PrintTypes(IEnumerable items) 
{ 
    foreach(var item in items) 
        Console.WriteLine(item.GetType().FullName); 
}

允许以可以强制转换为枚举的任何类型的方式调用方法。如果您更具体

public void PrintTypes(List items)

然后,例如,如果您有一个数组并希望将其类型名称打印到控制台,则首先必须创建一个新的List并用您的类型填充它。而且,如果使用通用实现,则将只能使用仅对特定类型的对象适用于任何对象的方法。

在谈论返回类型时,您越具体,呼叫者就可以使用的越灵活。

public List<string> GetNames()

您可以使用此返回类型来迭代名称

foreach(var name in GetNames())

或者您可以直接索引到集合中

Console.WriteLine(GetNames()[0])

而如果您找回的是不太具体的类型

public IEnumerable GetNames()

您将不得不按摩返回类型以获得第一个值

Console.WriteLine(GetNames().OfType<string>().First());

10
请注意,此建议与返回类型建议中的Eric Lippert的答案相矛盾。杰弗里·里希特(Jeffrey Richter)的方法为方法的使用者提供了最大的灵活性,使他们可以随意使用返回的对象,而埃里克(Eric)的方法则为方法的维护者提供了最大的灵活性,可以在不修改方法的公开外观的情况下更改实现。对于内部代码,我倾向于遵循Jeffrey的建议,但是对于公共图书馆,我可能更倾向于遵循Eric的建议。
phoog 2012年

3
@phoog:考虑到Eric的来历,毫不奇怪他对打破变化持谨慎态度。但这绝对是一个正确的观点。

好辛苦 似乎双方都有人(无论有无回报)。我了解更多您为什么要针对有助于避免不必要的转换的参数进行操作。我仍然不确定如何处理返回类型。
chobo2 2012年

@ chobo2:的确,如果您指定的是更精确而非通用的内容,则只需要考虑将来是否需要更改返回类型。如果可以考虑您的方法,请确定您可能不会更改返回集合类型,那么返回更精确的类型可能是安全的。如果不确定或担心将来更改它,那么您将破坏其他人的代码,那么请更笼统。

1
+1琐事:此答案是Postel定律的一个很好的CLR特定示例。:)
Dan J

13

IEnumerable<T>允许您遍历集合。ICollection<T>在此基础上,还允许添加和删除项。IList<T>还允许在特定索引处访问和修改它们。通过公开您希望与客户合作的对象,您可以自由更改实现。List<T>恰好实现了这三个接口。

如果您将财产公开为一个List<T>或什至一个时间IList<T>,那么您想要让消费者拥有的就是遍历集合的能力。然后他们可能会依赖可以修改列表的事实。然后,如果您决定将实际的数据存储从a转换List<T>为aDictionary<T,U>并将字典键公开为该属性的实际值(我之前必须做过)。然后,已经期望他们的更改将在您的班级中得到体现的消费者将不再具有该功能。那是个大问题!如果您将公开List<T>为,则IEnumerable<T>可以轻松预测到您的收藏集没有在外部进行修改。这是List<T>作为上述任何一种接口公开的力量之一。

当它属于方法参数时,此抽象级别朝另一个方向发展。当您将列表传递给IEnumerable<T>可以接受的方法时,可以确保列表不会被修改。当您是实现该方法的人员时,您说您接受一个,IEnumerable<T>因为您要做的就是遍历该列表。然后,调用该方法的人员可以自由使用任何可枚举的数据类型来调用它。这使您的代码可以以意外但有效的方式使用。

由此可见,您的方法实现可以表示其局部变量,但是您可以这样做。实现细节未公开。您可以自由地将代码更改为更好的代码,而不会影响调用您代码的人员。

您无法预测未来。假定属性的类型将永远是有益的,因为aList<T>会立即限制您适应代码不可预见的期望的能力。是的,您可能永远不会从a更改该数据类型,List<T>但是可以确定是否必须这样做。您的代码已准备就绪。


9

简短答案:

您通过接口,以便无论您使用该接口的什么具体实现,您的代码都将支持它。

如果您使用列表的具体实现,则您的代码将不支持同一列表的其他实现。

阅读有关继承和多态的知识


8

这是一个例子:我曾经有一个项目,我们的列表很大,结果导致大对象堆的碎片化影响了性能。我们用LinkedList替换了List。LinkedList不包含数组,因此突然之间,我们几乎没有使用大对象堆。

通常,IEnumerable<T>无论如何我们都将列表用作,因此不需要进一步的更改。(是的,如果您要做的就是枚举引用,我建议将引用声明为IEnumerable。)在几个地方,我们需要列表索引器,因此我们编写了效率低下的列表。IList<T>在链接列表周围包装器。我们很少需要列表索引器,因此效率低下不是问题。如果可以的话,我们本可以提供IList的其他实现,也许是足够小的数组的集合,这样可以更有效地建立索引,同时避免大型对象。

最后,您可能出于任何原因都需要替换实现。性能只是一种可能性。无论出于何种原因,在更改对象的特定运行时类型时,尽可能使用派生最少的类型将减少对代码进行更改的需求。


3

在方法内部,应使用var代替IListList。当您的数据源更改为来自某个方法时,您的onlySomeInts方法将继续存在。

之所以使用IList而不是List作为参数,是因为有许多东西实现IList(List和[],作为两个示例),但是只有一件事实现List。对接口进行编码更加灵活。

如果您只是枚举值,则应使用IEnumerable。可以容纳多个值的每种数据类型都实现IEnumerable(或应该),并使您的方法具有极大的灵活性。


13
说他应该使用“ var”是完全不正确的。在那里使用什么都没关系-这是一个样式问题。它不影响方法的签名,并且在编译时固定在石头上。相反,您应该帮助他摆脱对像IList foo = new List这样的本地声明的困惑-这正是他的困惑所在。
x0n 2012年

因此,基本上,您说的是仅出于以下事实而接受IList:如果他们要发送数组,则不必先执行.ToList?退货怎么样?我不明白您使用vars时“方法将继续存在”的含义吗?我知道这将需要更多工作,但是您是否还需要将其更改为新类型呢?所以也许IList <int>到IList <String>?
chobo2 2012年

的确,“使用变量”这一点更多是建议不要在方法本身内部担心它,而应更加关注它对消费者的外观。
Bryan Boettcher,2012年

我同意@ x0n:var严重过度使用,将无助于使任何事情变得更清楚。
那个查克·盖伊(Chuck Guy)2012年

1

使用IList而不是List使编写单元测试变得更加容易。它允许您使用“模拟”库来传递和返回数据。

使用接口的另一个一般原因是向对象的用户公开最少的知识。

考虑(人为)情况,其中我有一个实现IList的数据对象。

public class MyDataObject : IList<int>
{
    public void Method1()
    {
       ...
    }
    // etc
}

您上面的函数仅关心能够迭代列表。理想情况下,他们不需要知道谁实现了该列表或如何实现它。

在您的示例中,IEnumerable是您认为的更好的选择。


1

尽可能减少代码之间的依赖性始终是一个好主意。

牢记这一点,最有意义的是传递尽可能少的外部依赖项类型并返回相同的类型。但是,这可能会有所不同,具体取决于您的方法及其签名的可见性。

如果您的方法构成接口的一部分,则需要使用该接口可用的类型来定义方法。具体类型可能对接口不可用,因此它们将必须返回非具体类型。例如,如果要创建框架,则需要执行此操作。

但是,如果您不编写框架,则以尽可能弱的类型(即基类,接口,甚至是委托)传递参数并返回具体类型可能是有利的。这使调用者可以对返回的对象进行尽可能多的处理,即使将其转换为接口也是如此。但是,这会使方法更加脆弱,因为对返回的对象类型的任何更改都可能破坏调用代码。但是实际上,这通常不是主要问题。


1

您接受接口作为方法的参数,因为它允许调用者提交不同的具体类型作为参数。给定示例方法LogAllChecked,参数someClasses可能具有各种类型,并且对于编写该方法的人员而言,所有参数都可能等效(即,无论参数的类型如何,您都将编写完全相同的代码)。但是对于调用该方法的人来说,这可能会产生很大的不同-如果他们有一个数组,而您要一个列表,那么他们在调用该方法时就必须将数组更改为列表或vv,这完全浪费了程序员和性能POV的时间。

返回接口还是返回具体类型取决于要让调用者处理创建的对象的方式-这是API设计的决定,没有硬性规定。您必须权衡他们充分利用对象的能力与他们轻松利用部分对象功能的能力(当然,是否愿意让他们充分利用对象)。例如,如果返回IEnumerable,则将它们限制为迭代-它们不能在对象中添加或删除项目,它们只能对对象起作用。如果您需要在类外部公开一个集合,但又不想让调用者更改该集合,则这是一种实现方法。另一方面,如果您返回的是您希望/希望他们填充的空集合,


0

这是我在.NET 4.5以上版本中的答案。

使用的IList <T>IReadonlyList <T>
              而不是名单<T> ,因为ReadonlyList <T>不存在。

IList <T>看起来与IReadonlyList <T>一致

  • 使用的IEnumerable <T>为最小曝光(属性)或条件(参数)如果的foreach是使用它的唯一方法,。
  • 如果还需要公开/使用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; } }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.