替代Foreach扩展的更优雅的LINQ替代品

时间:2018-07-19 22:24:42

标签: c# .net asp.net-mvc entity-framework linq

这纯粹是为了提高我的技能。我的解决方案可以很好地完成主要任务,但并非“整洁”。我目前正在研究带有实体框架项目的.NET MVC。我只知道多年来已经足够的基本奇异LINQ函数。现在,我想学习如何看待。

所以我有两个模型

public class Server
{
    [Key]
    public int Id { get; set; }
    public string InstanceCode { get; set; }
    public string ServerName { get; set; }
}

public class Users
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; }
    public int ServerId { get; set; } //foreign key relationship
}

在我的一个视图模型中,我被要求提供一个下拉列表,用于在创建新用户时选择服务器。下拉列表中填充文本和值ID作为IEnumerable 这是我的服务器下拉列表的原始属性

public IEnumerable<SelectListItem> ServerItems
{
    get { Servers.ToList().Select(s => new selectListItem { Value = x.Id.ToString(), Text = $"{s.InstanceCode}@{s.ServerName}" }); }
}

有关需求的更新,现在我需要显示与每个服务器选择相关的用户数。好的,没问题。这就是我从头顶上写下的内容。

public IEnumerable<SelectListItem> ServerItems
{
    get 
    {
        var items = new List<SelectListItem>();
        Servers.ToList().ForEach(x => {
            var count = Users.ToList().Where(t => t.ServerId == x.Id).Count();
            items.Add(new SelectListItem { Value = x.Id.ToString(), Text = $"{x.InstanceCode}@{x.ServerName} ({count} users on)" });
        });

        return items;
    }
}

这使我的结果说“ localhost @ rvrmt1u(8个用户)”,仅此而已。 如果我想按用户数对该下拉列表进行排序怎么办。我要做的就是字符串中的另一个变量。

TLDR ...我确信某个地方的人可以教我一两个关于将其转换为LINQ查询并使它看起来更好的东西。另外,由于知道如何对列表进行排序以首先显示具有最多用户的服务器,所以可以得到加分。

2 个答案:

答案 0 :(得分:16)

好的,我们有这样的麻烦:

    var items = new List<SelectListItem>();
    Servers.ToList().ForEach(x => {
        var count = Users.ToList().Where(t => t.ServerId == x.Id).Count();
        items.Add(new SelectListItem { Value = x.Id.ToString(), Text = $"{x.InstanceCode}@{x.ServerName} ({count} users on)" });
    });
    return items;

进行一系列小的,仔细的,显然正确的重构,以逐步改进代码。

开始于:让我们将那些复杂的操作抽象为它们自己的方法。

请注意,我已将无用的x替换为有帮助的server

int UserCount(Server server) => 
  Users.ToList().Where(t => t.ServerId == server.Id).Count();

为什么地球上ToList上有Users?看起来不对。

int UserCount(Server server) => 
  Users.Where(t => t.ServerId == server.Id).Count();

我们注意到有一个内置的方法可以同时执行这两个操作:

int UserCount(Server server) => 
  Users.Count(t => t.ServerId == server.Id);

同样,用于创建项目:

SelectListItem CreateItem(Server server, int count) => 
  new SelectListItem 
  { 
    Value = server.Id.ToString(), 
    Text = $"{server.InstanceCode}@{server.ServerName} ({count} users on)" 
  };

现在我们的财产为:

    var items = new List<SelectListItem>();
    Servers.ToList().ForEach(server => 
    {
        var count = UserCount(server);
        items.Add(CreateItem(server, count);
    });
    return items;

好多了。

如果您只是要传递lambda正文,请不要使用ForEach作为方法!语言中已经有一个内置的机制可以做得更好!您只需写items.Foreach(item => {...});就没有理由写foreach(var item in items) { ... }。它更易于理解和调试,并且编译器可以更好地对其进行优化。

    var items = new List<SelectListItem>();
    foreach (var server in Servers.ToList())
    {
        var count = UserCount(server);
        items.Add(CreateItem(server, count);
    }
    return items;

好多了。

为什么ToList上有Servers?完全没有必要!

    var items = new List<SelectListItem>();
    foreach(var server in Servers)
    {
        var count = UserCount(server);
        items.Add(CreateItem(server, count);
    }
    return items;

变得更好。我们可以消除不必要的变量。

    var items = new List<SelectListItem>();
    foreach(var server in Servers)
        items.Add(CreateItem(server, UserCount(server));
    return items;

嗯。这使我们了解到CreateItem可以自己进行计数。让我们重写它。

SelectListItem CreateItem(Server server) => 
  new SelectListItem 
  { 
    Value = server.Id.ToString(), 
    Text = $"{server.InstanceCode}@{server.ServerName} ({UserCount(server)} users on)" 
  };

现在我们的道具身是

    var items = new List<SelectListItem>();
    foreach(var server in Servers)
        items.Add(CreateItem(server);
    return items;

这应该看起来很熟悉。我们已经重新发明了SelectToList

var items = Servers.Select(server => CreateItem(server)).ToList();

现在,我们注意到可以用方法组替换lambda:

var items = Servers.Select(CreateItem).ToList();

并且我们已将整个混乱情况简化为清晰明确的样子的一行。它有什么作用?它为每个服务器创建一个项目,并将它们放在列表中。 代码应该读起来像它所做的一样,而不是它的执行方式

仔细研究我在这里使用的技术

  • 将复杂的代码提取为辅助方法
  • 用实际循环替换ForEach
  • 消除不必要的ToList
  • 当您意识到有待改进的地方时,请重新考虑以前的决定
  • 在重新实现简单的辅助方法时识别
  • 不要停止一项改进!每种改进都可以做另一种。

  

如果我想按用户数量对下拉列表进行排序怎么办?

然后按用户数量排序!我们将其抽象为一个辅助方法,因此可以使用它:

var items = Servers
  .OrderBy(UserCount)
  .Select(CreateItem)
  .ToList();

我们现在注意到,我们叫UserCount 两次。我们在乎吗?也许。两次调用它可能是一个性能问题,或者令人恐惧,它可能不是幂等的!如果有任何一个问题,那么我们需要撤销之前做出的决定。在理解模式下而不是在流利模式下更容易处理这种情况,所以让我们将其重写为一个理解:

var query = from server in Servers
            orderby UserCount(server)
            select CreateItem(server);
var items = query.ToList();

现在我们回到之前的内容:

SelectListItem CreateItem(Server server, int count) => ...

现在我们可以说

var query = from server in Servers
            let count = UserCount(server)
            orderby count
            select CreateItem(server, count);
var items = query.ToList();

,我们每个服务器只调用一次UserCount

为什么要回到理解模式?因为在流利模式下执行此操作会造成混乱:

var query = Servers
  .Select(server => new { server, count = UserCount(server) })
  .OrderBy(pair => pair.count)
  .Select(pair => CreateItem(pair.server, pair.count))
  .ToList();

看起来有点难看。 (在C#7中,您可以使用元组而不是匿名类型,但是想法是相同的。)

答案 1 :(得分:1)

LINQ的窍门就是键入return并从那里开始。不要创建列表并向其中添加项目;通常有一种一次性选择所有内容的方法。

public IEnumerable<SelectListItem> ServerItems
{
    get 
    {
        return Servers.Select
        (
            server => 
            new 
            {
                Server = server,
                UserCount = Users.Count( u => u.ServerId = server.Id )
            }
        )
        .Select
        (
            item =>
            new SelectListItem
            {
                Value = item.Server.Id.ToString(),
                Text = string.Format
                (
                    @"{0}{1} ({2} users on)" ,
                    item.Server.InstanceCode,
                    item.Server.ServerName, 
                    item.UserCount
                )
            }
        );
    }
}

在此示例中,实际上有两个Select语句-一个用于提取数据,另一个用于格式化。在理想情况下,将这两个任务的逻辑划分为不同的层,但这是可以的。