【问题标题】:Return type as IEnumerable instead of just List?返回类型为 IEnumerable 而不仅仅是 List?
【发布时间】:2019-05-11 16:13:32
【问题描述】:

我正在做一个 .NET MVC 教程。话虽如此,我遇到了这样的代码:

public class MoviesController : Controller
{
    public ActionResult Index()
    {
        var movies = GetMovies();

        return View(movies);
    }

    private IEnumerable<Movie> GetMovies()
    {
        return new List<Movie>
        {
            new Movie {Id = 1, Name = "Shrek"},
            new Movie {Id = 2, Name = "LotR"}
        };
    }
}

电影的索引视图如下所示:

@model IEnumerable<VideoStore.Models.Movie>

@{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Movies</h2>
<table class="table table-bordered table-hover">
    <thead>
        <tr>
            <th>Movie</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var movie in Model)
        {
            <tr>
                <td>@movie.Name</td>
            </tr>
        }
    </tbody>
</table>

所以我的问题是,为什么在私有方法GetMovies() 中的MoviesController 中,使用了IEnumerable 返回类型?为什么不使用List 返回类型?

【问题讨论】:

  • 这样您就可以将其更改为返回一个数组或任何实现 IEnumerable 的东西,而您不必更改使用它的代码。基本上,您只需公开具有所有所需功能的最小派生类型。

标签: c# asp.net-mvc ienumerable


【解决方案1】:

返回类型为 IEnumerable 而不仅仅是 List?

//controller

var a = new Dictionary<string, string>();
return View(a);

var b = new List<string>();
return View(b);

var c = new LinkedList<string>();
return View(c);

// All work with:

@model IEnumerable<string>

虽然使用IEnumerable&lt;&gt;开放 sOlid Principles,但我很少建议将任何集合类型接口/类传递给视图。

虽然在 C# 数组/集合中是 First-Class Citizens,但问题是它们在维护 Single Responsibility Principle 时不可扩展。

例如:

// Controller Returns:
var people = .... as IEnumerable<Person>;
return View(people);

@model IEnumerable<Person>

现在假设要向视图添加与组无关的任何信息(如页面标题).. 你是怎么做的?您可以扩展并创建自己的派生自 IEnumerable&lt;T&gt; 的类,但这会破坏 SRP,因为页面的标题与人群无关。相反,您应该创建一个代表视图所需的一切的一流模型:

public class MyViewModel
{
  public string Title { get; set;}
  public IEnumerable<Person> People { get; set;}
}

return View(myViewModel);

@model MyViewModel

我建议总是这样做。一旦你开始在 MVC 中使用部分或模板,或者想要回发相同的对象,离开IEnumerable&lt;&gt; 变得越来越困难,因为你需要更改部分和/或模板和/或 Javascript...

所以我的问题是,为什么在私有方法GetMovies()中的MoviesController中,使用了IEnumerable&lt;&gt;返回类型?

一般来说Program against an Interface and not an Implementation,也称为Design by contract (DbC), also known as contract programming, programming by contract and design-by-contract programming,

这是一个soLid Principle 特别是 Liskov 替换原则。摘录:

可替换性是面向对象编程中的一个原则,它指出,在计算机程序中,如果 S 是 T 的子类型,则类型 T 的对象可以替换为类型 S 的对象(即类型 T 的对象可以是替换为子类型 S) 的任何对象,而不改变程序的任何所需属性(正确性、执行的任务等)。更正式地说,Liskov 替换原则 (LSP) 是子类型关系的特定定义,称为(强)行为子类型,最初由 Barbara Liskov 在 1987 年的会议主题演讲中引入,标题为“数据抽象和层次结构”。它是一种语义关系,而不仅仅是句法关系,因为它旨在保证层次结构中类型的语义互操作性,尤其是对象类型。 Barbara Liskov 和 Jeannette Wing 在 1994 年的一篇论文中简明扼要地描述了该原理如下...

实际上是指当前代码:

    public ActionResult Index()
    {
        var movies = GetMovies();

        return View(movies);
    }

    private IEnumerable<Movie> GetMovies()
    {
        return new List<Movie>
        {
            new Movie {Id = 1, Name = "Shrek"},
            new Movie {Id = 2, Name = "LotR"}
        };
    }

可以改为:

public class MoviesController : Controller
{
    private readonly IMovieDb _movieDb;
    // Dependency Injecting access to movies
    public MoviesController(IMovieDb movieDb)
    {
        _movieDb = movieDb;
    }

    public ActionResult Index()
    {
        var movies = _movieDb .GetMovies();

        return View(movies);
    }
    // ....


public interface IMovieDb
{
    IEnumerable<Movie> GetMovies();
}

现在我们不知道如何检索电影......只要合约/接口满足我们的数据需求,我们就不应该关心。

【讨论】:

  • 哇,这个答案花了很多精力。在上一个示例中,您注入了movieDb,但随后使用了iocDA。错字?
  • 这是很多信息@Erik,非常感谢您的努力!稍后我肯定会再次回到您的答案,作为重要的知识来源。目前,您的回答和拉西尔的回答对这个主题做出了巨大的澄清!
【解决方案2】:

IEnumerable&lt;&gt; 是与迭代 List&lt;&gt; 相关的接口。返回List&lt;&gt; 将立即向GetMovies() 的使用者公开更多操作,而不是严格必要的操作——例如从集合中添加或删除——这可能更容易引入错误。选择IEnumerable&lt;&gt; 而不是List&lt;&gt; 没有技术上的理由,因为在引擎盖下它的行为是相同的。这个决定纯粹是实际的。

【讨论】:

    【解决方案3】:

    使用 IEnumerable 会削弱耦合,这使得它可以在实现发生变化时与其他具体类型向前兼容。可以在不改变界面的情况下进行以下所有更改:

    //Original
    private IEnumerable<Movie> GetMovies()
    {
        return new List<Movie>
        {
            new Movie {Id = 1, Name = "Shrek"},
            new Movie {Id = 2, Name = "LotR"}
        };
    }
    
    //Using an array
    private IEnumerable<Movie> GetMovies()
    {
        return new Movie[]
        {
            new Movie {Id = 1, Name = "Shrek"},
            new Movie {Id = 2, Name = "LotR"}
        };
    }
    
    //From EF
    private IEnumerable<Movie> GetMovies()
    {
        return dbContext.Movies.Where( m => Name == "Shrek" || Name == "Lotr );
    }
    
    
    //Covariant
    class NerdMovie : Movie {}
    
    private IEnumerable<Movie> GetMovies()
    {
        return new List<NerdMovie>
        {
            new NerdMovie {Id = 1, Name = "Shrek"},
            new NerdMovie {Id = 2, Name = "LotR"}
        };
    }
    
    
    //Custom type
    class MovieList : List<Movie> { }
    
    private IEnumerable<Movie> GetMovies()
    {
        return new MovieList
        {
            new Movie {Id = 1, Name = "Shrek"},
            new Movie {Id = 2, Name = "LotR"}
        };
    }
    
    //Using yield
    private IEnumerable<Movie> GetMovies()
    {
        yield return new Movie {Id = 1, Name = "Shrek"};
        yield return new Movie {Id = 2, Name = "LotR"};
    }
    

    【讨论】:

    • 这仅在接收方(即视图)是正确的,而不是在给予方(即GetMovies 函数)。您可以将 List 从函数返回到期望 IEnumerable 的视图,并且仍然具有完全相同的解耦。有关详细信息,请参阅我的答案。
    • 我不同意;这不是完全相同的脱钩。通过在方法定义中公开一个列表,服务承诺提供一个列表,这可能很重要,例如如果该方法是接口或合同的一部分,并且需要前向兼容性。有时明智的做法是只做出您需要的承诺,仅此而已。感谢您的反馈。
    • 是的,这正是我在回答中所解释的。但是对于问题中的代码,这些条件都不适用。该类未实现接口,该函数返回一个列表。在这种特定情况下,将接口用作返回类型绝对没有任何好处,并且不会以任何方式改变耦合/解耦。
    • 我认为我们就语言的工作方式达成了一致。对我来说,令人困惑的是您讨论“有问题的代码”耦合的方式。耦合是一个超出所讨论代码的概念,因为它与一件事的更改是否需要另一件事的更改有关。在我的回答中,我展示了六种方法来更改方法,如果您使用具体类型,这些方法也需要您更改调用代码。所以说耦合没有区别是不正确的。
    • 仔细阅读问题。 OP 没有询问调用代码(即视图)。该类型在那里定义为IEnumerable,因此它将接受您答案中的所有这些方法。 OP 询问了 GetMovies 函数返回的类型。我是说在问题的特定代码中,将返回类型从IEnumerable 更改为List 不会以任何方式影响耦合。该视图仍在接受IEnumerable,您的所有功能都可以使用它。函数返回类型唯一不同的是函数是否实现了接口。
    【解决方案4】:

    我无法说出特定代码背后的开发人员的确切推理,但通常我们使用最通用的可接受类型(通常是接口)进行输入,就像在视图中所做的那样。这使我们能够从不同的资源中获取数据并在它们之间进行切换。

    至于返回输出,返回泛型接口而不是实际类型没有任何好处,除非函数根据某些条件返回不同的子类型,而您的示例中并非如此。或者,如果类本身正在实现一个接口,那么接口返回最通用的可接受类型是很有用的,原因与上述相同。这也不是你的情况,所以在你的情况下,返回 IEnumerable 没有任何好处。

    【讨论】:

    • 哇,谢谢拉西尔在我的问题中的这个和其他答案。你在这里给了我很好的知识!顺便说一句,所以使用 IEnumerable 不会加快应用程序的速度,是吗(我在想这可能是原因)?
    • 不,即使返回类型是IEnumerable&lt;&gt;,代码仍然返回List&lt;&gt;。唯一的区别是消费者只能看到IEnumerable&lt;&gt;暴露的方法,直到它被转换为另一种类型。
    • 不是为了退货,不是。在视图中,循环通过IEnumerable 可能会有所不同(更快或更慢),具体取决于它是如何完成的。但是返回 List 而不是 IEnumerable 不会以任何可衡量的方式影响性能。
    【解决方案5】:

    IEnumerable 是接口所以它更灵活,如果你使用 List ,你会强制接收方法期待 List。

    【讨论】:

    • 谢谢,我给你+,但卢克塞缪尔的答案比较复杂,所以我会标记他的答案。
    • 这是不正确的。您可以完美地将 List 返回到期望 IEnumerable 的视图。
    • 感谢@RacilHilan 的澄清。
    • @RacilHilan: 期待 Array 的方法怎么样?
    • 我不明白你的问题。怎么样?评论不是提问和回答问题的好地方。如果您希望我们给您好的答案,您可以发布带有代码的问题。或者至少解释一下期望数组的方法是什么意思。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-10-25
    • 1970-01-01
    • 1970-01-01
    • 2014-09-21
    • 1970-01-01
    • 1970-01-01
    • 2011-02-18
    相关资源
    最近更新 更多