【问题标题】:How would you refactor this LINQ code?您将如何重构此 LINQ 代码?
【发布时间】:2010-09-08 11:18:39
【问题描述】:

我有很多看起来像这样的丑陋代码:

if (!string.IsNullOrEmpty(ddlFileName.SelectedItem.Text))
    results = results.Where(x => x.FileName.Contains(ddlFileName.SelectedValue));
if (chkFileName.Checked)
    results = results.Where(x => x.FileName == null);

if (!string.IsNullOrEmpty(ddlIPAddress.SelectedItem.Text))
    results = results.Where(x => x.IpAddress.Contains(ddlIPAddress.SelectedValue));
if (chkIPAddress.Checked)
    results = results.Where(x => x.IpAddress == null);

...etc.

results 是一个IQueryable<MyObject>
这个想法是,对于这些无数的下拉列表和复选框中的每一个,如果下拉列表中选择了某些内容,则用户希望匹配该项目。如果选中该复选框,则用户特别想要该字段为空或空字符串的那些记录。 (UI 不允许同时选择两者。)这一切都添加到了 LINQ 表达式中,在我们添加了所有条件之后,它会在最后执行。

似乎应该有某种方法可以提取一个或两个Expression<Func<MyObject, bool>>,这样我就可以将重复的部分放入一个方法中,然后传入更改的内容。我在其他地方做过这个,但是这组代码让我受阻。 (另外,我想避免使用“Dynamic LINQ”,因为如果可能的话,我想保持类型安全。)有什么想法吗?

【问题讨论】:

    标签: c# .net linq linq-to-sql


    【解决方案1】:

    我会将其转换为单个 Linq 语句:

    var results =
        //get your inital results
        from x in GetInitialResults()
        //either we don't need to check, or the check passes
        where string.IsNullOrEmpty(ddlFileName.SelectedItem.Text) ||
           x.FileName.Contains(ddlFileName.SelectedValue)
        where !chkFileName.Checked ||
           string.IsNullOrEmpty(x.FileName)
        where string.IsNullOrEmpty(ddlIPAddress.SelectedItem.Text) ||
           x.FileName.Contains(ddlIPAddress.SelectedValue)
        where !chkIPAddress.Checked ||
           string.IsNullOrEmpty(x. IpAddress)
        select x;
    

    并没有更短,但我发现这个逻辑更清晰。

    【讨论】:

      【解决方案2】:

      在那种情况下:

      //list of predicate functions to check
      var conditions = new List<Predicate<MyClass>> 
      {
          x => string.IsNullOrEmpty(ddlFileName.SelectedItem.Text) ||
               x.FileName.Contains(ddlFileName.SelectedValue),
          x => !chkFileName.Checked ||
               string.IsNullOrEmpty(x.FileName),
          x => string.IsNullOrEmpty(ddlIPAddress.SelectedItem.Text) ||
               x.IpAddress.Contains(ddlIPAddress.SelectedValue),
          x => !chkIPAddress.Checked ||
               string.IsNullOrEmpty(x.IpAddress)
      }
      
      //now get results
      var results =
          from x in GetInitialResults()
          //all the condition functions need checking against x
          where conditions.All( cond => cond(x) )
          select x;
      

      我刚刚明确声明了谓词列表,但是可以生成这些,例如:

      ListBoxControl lbc;
      CheckBoxControl cbc;
      foreach( Control c in this.Controls)
          if( (lbc = c as ListBoxControl ) != null )
               conditions.Add( ... );
          else if ( (cbc = c as CheckBoxControl ) != null )
               conditions.Add( ... );
      

      您需要某种方法来检查您需要检查的 MyClass 的属性,为此您必须使用反射。

      【讨论】:

        【解决方案3】:

        你见过LINQKit吗? AsExpandable 听起来像是您所追求的(尽管您可能想阅读 TomasP.NET 上的 Calling functions in LINQ queries 帖子以获得更深入的了解)。

        【讨论】:

          【解决方案4】:

          如果 LINQ 会影响可读性,请不要使用它。将各个测试分解为布尔方法,可用作您的 where 表达式。

          IQueryable<MyObject> results = ...;
          
          results = results
              .Where(TestFileNameText)
              .Where(TestFileNameChecked)
              .Where(TestIPAddressText)
              .Where(TestIPAddressChecked);
          

          因此,各个测试是类上的简单方法。它们甚至可以单独进行单元测试。

          bool TestFileNameText(MyObject x)
          {
              return string.IsNullOrEmpty(ddlFileName.SelectedItem.Text) ||
                     x.FileName.Contains(ddlFileName.SelectedValue);
          }
          
          bool TestIPAddressChecked(MyObject x)
          {
              return !chkIPAddress.Checked ||
                  x.IpAddress == null;
          }
          

          【讨论】:

          • 请记住,这是 LINQ to SQL(我在问题中没有说,但它是标签之一)。我希望过滤发生在数据库端,而不是客户端。
          • 啊!然后在这种方法上为-1。
          【解决方案5】:
          results = results.Where(x => 
              (string.IsNullOrEmpty(ddlFileName.SelectedItem.Text) || x.FileName.Contains(ddlFileName.SelectedValue))
              && (!chkFileName.Checked || string.IsNullOrEmpty(x.FileName))
              && ...);
          

          【讨论】:

            【解决方案6】:

            到目前为止,这些答案都不是我想要的。为了举例说明我的目标(我也不认为这是一个完整的答案),我采用了上面的代码并创建了几个扩展方法:

            static public IQueryable<Activity> AddCondition(
                this IQueryable<Activity> results,
                DropDownList ddl, 
                Expression<Func<Activity, bool>> containsCondition)
            {
                if (!string.IsNullOrEmpty(ddl.SelectedItem.Text))
                    results = results.Where(containsCondition);
                return results;
            }
            static public IQueryable<Activity> AddCondition(
                this IQueryable<Activity> results,
                CheckBox chk, 
                Expression<Func<Activity, bool>> emptyCondition)
            {
                if (chk.Checked)
                    results = results.Where(emptyCondition);
                return results;
            }
            

            这让我可以将上面的代码重构为:

            results = results.AddCondition(ddlFileName, x => x.FileName.Contains(ddlFileName.SelectedValue));
            results = results.AddCondition(chkFileName, x => x.FileName == null || x.FileName.Equals(string.Empty));
            
            results = results.AddCondition(ddlIPAddress, x => x.IpAddress.Contains(ddlIPAddress.SelectedValue));
            results = results.AddCondition(chkIPAddress, x => x.IpAddress == null || x.IpAddress.Equals(string.Empty));
            

            这并没有相当那么难看,但它仍然比我想要的要长。每组中的 lambda 表达式对显然非常相似,但我无法找到进一步压缩它们的方法……至少在不求助于动态 LINQ 的情况下是这样,这让我牺牲了类型安全。

            还有其他想法吗?

            【讨论】:

              【解决方案7】:

              @Kyralessa,

              您可以为接受 Control 类型参数加上 lambda 表达式并返回组合表达式的谓词创建扩展方法 AddCondition。然后,您可以使用流畅的接口组合条件并重用您的谓词。要查看如何实现它的示例,请参阅我对这个问题的回答:

              How do I compose existing Linq Expressions

              【讨论】:

                【解决方案8】:

                我会警惕这种形式的解决方案:

                // from Keith
                from x in GetInitialResults()
                    //either we don't need to check, or the check passes
                    where string.IsNullOrEmpty(ddlFileName.SelectedItem.Text) ||
                       x.FileName.Contains(ddlFileName.SelectedValue)
                

                我的推理是变量捕获。如果您立即执行一次,您可能不会注意到差异。但是,在 linq 中,评估不是立即的,而是在每次迭代发生时发生。代表可以捕获变量并在您想要的范围之外使用它们。

                感觉您的查询距离 UI 太近了。查询是向下的一层,而 linq 不是 UI 向下通信的方式。

                您最好执行以下操作。将搜索逻辑与表示分离——它更灵活和可重用——OO 的基础。

                // my search parameters encapsulate all valid ways of searching.
                public class MySearchParameter
                {
                    public string FileName { get; private set; }
                    public bool FindNullFileNames { get; private set; }
                    public void ConditionallySearchFileName(bool getNullFileNames, string fileName)
                    {
                        FindNullFileNames = getNullFileNames;
                        FileName = null;
                
                        // enforce either/or and disallow empty string
                        if(!getNullFileNames && !string.IsNullOrEmpty(fileName) )
                        {
                            FileName = fileName;
                        }
                    }
                    // ...
                }
                
                // search method in a business logic layer.
                public IQueryable<MyClass> Search(MySearchParameter searchParameter)
                {
                    IQueryable<MyClass> result = ...; // something to get the initial list.
                
                    // search on Filename.
                    if (searchParameter.FindNullFileNames)
                    {
                        result = result.Where(o => o.FileName == null);
                    }
                    else if( searchParameter.FileName != null )
                    {   // intermixing a different style, just to show an alternative.
                        result = from o in result
                                 where o.FileName.Contains(searchParameter.FileName)
                                 select o;
                    }
                    // search on other stuff...
                
                    return result;
                }
                
                // code in the UI ... 
                MySearchParameter searchParameter = new MySearchParameter();
                searchParameter.ConditionallySearchFileName(chkFileNames.Checked, drpFileNames.SelectedItem.Text);
                searchParameter.ConditionallySearchIPAddress(chkIPAddress.Checked, drpIPAddress.SelectedItem.Text);
                
                IQueryable<MyClass> result = Search(searchParameter);
                
                // inform control to display results.
                searchResults.Display( result );
                

                是的,它需要更多的打字,但是您阅读代码的次数比编写代码的次数多 10 倍左右。您的 UI 更清晰,搜索参数类自行处理并确保互斥选项不会发生冲突,并且搜索代码从任何 UI 中抽象出来,甚至根本不关心您是否使用 Linq。

                【讨论】:

                  【解决方案9】:

                  由于您想使用无数过滤器重复减少原始结果查询,您可以使用 Aggregate(),(对应于函数式语言中的 reduce())。

                  过滤器具有可预测的形式,由 MyObject 的每个成员的两个值组成 - 根据我从您的帖子中收集的信息。如果要比较的每个成员都是一个字符串,可能为 null,那么我建议使用扩展方法,它允许将 null 引用关联到其预期类型的​​扩展方法。

                  public static class MyObjectExtensions
                  {
                      public static bool IsMatchFor(this string property, string ddlText, bool chkValue)
                      {
                          if(ddlText!=null && ddlText!="")
                          {
                              return property!=null && property.Contains(ddlText);
                          }
                          else if(chkValue==true)
                          {
                              return property==null || property=="";
                          }
                          // no filtering selected
                          return true;
                      }
                  }
                  

                  我们现在需要在一个集合中安排属性过滤器,以允许迭代多个。它们表示为与 IQueryable 兼容的表达式。

                  var filters = new List<Expression<Func<MyObject,bool>>>
                  {
                      x=>x.Filename.IsMatchFor(ddlFileName.SelectedItem.Text,chkFileName.Checked),
                      x=>x.IPAddress.IsMatchFor(ddlIPAddress.SelectedItem.Text,chkIPAddress.Checked),
                      x=>x.Other.IsMatchFor(ddlOther.SelectedItem.Text,chkOther.Checked),
                      // ... innumerable associations
                  };
                  

                  现在我们将无数过滤器聚合到初始结果查询中:

                  var filteredResults = filters.Aggregate(results, (r,f) => r.Where(f));
                  

                  我在一个带有模拟测试值的控制台应用程序中运行了它,它按预期工作。我认为这至少说明了这个原则。

                  【讨论】:

                    【解决方案10】:

                    您可能会考虑通过消除复选框并在下拉列表中使用“&lt;empty&gt;”或“&lt;null&gt;”项来简化 UI。这将减少占用窗口空间的控件数量,不再需要复杂的“仅在未选中 Y 时启用 X”逻辑,并且会启用一个不错的每个查询字段一个控件。


                    继续您的结果查询逻辑,我将首先创建一个简单的对象来表示您的域对象上的过滤器:

                    interface IDomainObjectFilter {
                      bool ShouldInclude( DomainObject o, string target );
                    }
                    

                    您可以将过滤器的适当实例与您的每个 UI 控件相关联,然后在用户启动查询时检索它:

                    sealed class FileNameFilter : IDomainObjectFilter {
                      public bool ShouldInclude( DomainObject o, string target ) {
                        return string.IsNullOrEmpty( target )
                            || o.FileName.Contains( target );
                      }
                    }
                    
                    ...
                    ddlFileName.Tag = new FileNameFilter( );
                    

                    然后,您可以通过简单地枚举控件并执行关联的过滤器来概括您的结果过滤(感谢 hurst 的聚合想法):

                    var finalResults = ddlControls.Aggregate( initialResults, ( c, r ) => {
                      var filter = c.Tag as IDomainObjectFilter;
                      var target = c.SelectedValue;
                      return r.Where( o => filter.ShouldInclude( o, target ) );
                    } );
                    


                    由于您的查询非常有规律,您可以通过使用带有成员选择器的单个过滤器类来进一步简化实现:

                    sealed class DomainObjectFilter {
                      private readonly Func<DomainObject,string> memberSelector_;
                      public DomainObjectFilter( Func<DomainObject,string> memberSelector ) {
                        this.memberSelector_ = memberSelector;
                      }
                    
                      public bool ShouldInclude( DomainObject o, string target ) {
                        string member = this.memberSelector_( o );
                        return string.IsNullOrEmpty( target )
                            || member.Contains( target );
                      }
                    }
                    
                    ...
                    ddlFileName.Tag = new DomainObjectFilter( o => o.FileName );
                    

                    【讨论】:

                      猜你喜欢
                      • 1970-01-01
                      • 2019-06-07
                      • 1970-01-01
                      • 2010-10-09
                      • 1970-01-01
                      • 1970-01-01
                      • 2015-08-29
                      • 1970-01-01
                      • 1970-01-01
                      相关资源
                      最近更新 更多