【问题标题】:C# LINQ query Select projection, Function programmingC# LINQ 查询选择投影,函数编程
【发布时间】:2022-01-22 07:50:49
【问题描述】:
public class Factory
{
    public string Number { get; set; }
    public List<OrganizationUnit> Units;
}

public class OrganizationUnit
{
    public string Name { get; set; }
    public string Id { get; set; }
    public bool ?IsActive { get; set; }
}

public static  class LocalisationRepo  
{
    public static List<Factory> GetFactories()
    {
        List<Factory> fa = new List<Factory>();
        Factory factory = new Factory() { Number="F10", Units = new List<OrganizationUnit> { 
            new OrganizationUnit() 
            { Id = "001", Name = "Quality", IsActive = false}, 
            new OrganizationUnit() { Id = "002", Name = "Line 3", IsActive=null } ,
            new OrganizationUnit { Id="003", Name="IT", IsActive=true } } };

                  Factory factory2 = new Factory()
            {
                Number = "F11",
                Units = new List<OrganizationUnit> {
                new OrganizationUnit()
                { Id = "001", Name = "Quality", IsActive = true},
                new OrganizationUnit() { Id = "002", Name = "Line 3", IsActive=true } ,
                new OrganizationUnit { Id="003", Name="IT", IsActive=true } }
            };

            fa.Add(factory);
            fa.Add(factory2);
        
        return fa;
    }

}

有一个 linq 查询

var factories = LocalisationRepo.GetFactories();

var fa = factories.SelectMany(f => f.Units.Where(u => (u.IsActive ?? false) == false), (f, u) => f).Distinct();

foreach (var item in fa)
{
    Console.WriteLine(item.Number);
}

第一部分f =&gt; f.Units.Where(u =&gt; (u.IsActive ?? false) == false)
给我们IEnumerable&lt;OrganizationUnit&gt; 第二个参数(f, u) =&gt; f(应用于中间序列每个元素的变换函数) "将序列的每个元素投影到IEnumerable&lt;Out T&gt;"

我的问题是当第一个参数的“输出”为IEnumerable&lt;OrganizationUnit&gt; 时,此选择器/委托如何从IEnumerable&lt;OrganizationUnit&gt; 转换为IEnumerable&lt;Factory&gt; 应该如何考虑/理解?

我们知道 f 是一个工厂,但“中间结果”是 OrganizationUnit 所以......如何?关于函数编程的一些理论?

我想要具有非活动 OrganisationUnits IsActive=false 的工厂; 但我不是在问如何查找结果,因为我的示例运行良好。 我想知道它是如何工作的,以及为什么......

同微软示例https://docs.microsoft.com/en-US/dotnet/api/system.linq.enumerable.selectmany?view=net-6.0

我们可以查询 变种查询 = 宠物主人 .SelectMany(po => po.Pets.Where(p => p.StartsWith("S")), (po, p) => po);

【问题讨论】:

  • 你到底想做什么?我不认为您以您认为的方式使用SelectMany。您是否尝试将项目投射到新的工厂列表中,并且只使用那些处于活动状态的单位?
  • 工厂方法应该是Func&lt;TInput, TOutput&gt;。从我所看到的,我什至不明白你想要达到什么目的。 OrganizationUnit 应该如何翻译成Factory 类?
  • 我修改了示例。所以你现在可以检查一下,我想要具有非活动组织单位 IsActive=false 的工厂会更清楚;但我不是在询问如何查找结果,因为我的示例运行良好。我想知道它是如何工作的,以及为什么......
  • “我的例子工作正常”你确定吗?您是否只想要具有符合条件的单位的工厂的 ID,或者您正在寻找其他东西?

标签: c# linq


【解决方案1】:

我认为这里的混淆与(不必要的)使用SelectMany 来过滤工厂有关。

在此声明中:

var fa = factories.SelectMany(f => f.Units.Where(u => (u.IsActive ?? false) == false), (f, u) => f);

(f, u) =&gt; f参数基本上是忽略selectmany选什么(也就是u),反正选原厂f

所以最后,你的陈述相当于: ``` var fa = factory.Select(f => f); ``` 这基本上是一个空操作。

所以上述情况并非完全如此。事实证明,您的实际查询相当于

var fa = factories.Where(f => f.Units.Any(u => (u.IsActive ?? false) == false);

这是因为,对于每个工厂,SelectMany 重载将选择您提供的每个匹配的Units,将其作为第三个参数的 func 参数交给您,然后等待您对其进行转换随心所欲。因此它希望您将(Factory,Unit) 对转换为some T。您决定避免转换,而是选择第一个元素 Factory f。然后,SelectMany 会将所有此类 lambda 执行的结果组合成一个最终序列,在您的情况下,仅包含工厂。如果您列表中的任何工厂包含多个禁用单元,它将在最终结果中出现多次,这就是为什么您必须向其添加 Distinct 以使查询“有意义”。

如果你想过滤“至少有一个非活动单位的工厂”,你可以这样做:

var fa = factories.Where(f => f.Units.Any(u => (u.IsActive ?? false) == false);

如果你想要“所有单位都处于非活动状态的工厂”,你可以这样做:

var fa = factories.Where(f => f.Units.All(u => (u.IsActive ?? false) == false);

我们知道 f 是一个工厂,但“中间结果”是 OrganizationUnit 所以......如何?关于函数编程的一些理论?

“中间结果”不是OrganizationUnit:有了这个重载,中间结果实际上是一对(Factory,OrganizationResult)正在“转换”成只需Factory 选择f 作为结果。没什么特别的。

SelectMany 应该用于展平结果,您似乎不需要。

【讨论】:

  • @Artur 你能准确地说明我的查询是如何不起作用的吗?您确定您使用的是Where 而不是SelectWhere 不会改变返回类型,所以它会保持 IEnumerable&lt;Factory&gt; 这就是你原来的 factories 的样子。
  • 如果没有 SelectMany 和 (f, u) => f) 在我的查询中,您将得到 IEnumerabe> (list with IsActive=false) 所以 SelectMany “扁平化”了它。您的查询工作正常,但我想知道我的工作情况
  • @Artur 好的,我看到你现在编辑了。我的解释对你有意义吗?基本上,您会以非常迂回的方式获得相同的结果。它有效,但它不是实现您想要的结果的最干净的方法。
  • 谢谢,我现在知道的更多了;)var fa = factories.SelectMany(f =&gt; f.Units.Where(u =&gt; (u.IsActive ?? false) == false), (f, u) =&gt; new { FactoryName = f.Number, OuId = u.Id, OuName = u.Name, IsActive = u.IsActive });
【解决方案2】:

SelectMany 运算符用于从称为嵌套集合的集合中选择元素。

SelectMany 从嵌套集合返回单个结果 您可以使用 SelectMany 从工厂获取每个 OrganizationUnit 行。

var fa = factories.SelectMany(f =&gt; f.Units.Where(u =&gt; !(u.IsActive??false)), (factory, unit) =&gt; unit);

你正试图反方向。

重要不要使用 .Distinct() 来修复重复项。一开始就不应该出现重复项。

解决方案:

var fa = factories.Where( factory =&gt; factory.Units.Any(unit =&gt; (unit.IsActive?? false) == false));

【讨论】:

    【解决方案3】:

    应该如何考虑/理解?

    您正在对工厂列表执行 SelectMany,并且您告诉 SM 您希望它扁平化每个工厂中的单元。第二个参数表示一个函数,它接受当前正在迭代的工厂和正在迭代的单元之一。

    listOfFactories.SelectMany(aFactory => aFactory.listOfTheUnits, (theCurrentFactory, oneOfTheUnitsOfTheCurrentFactory) => ...)
    

    或者在非委托术语中

    listOfFactories.SelectMany(GetUnitsFromAFactory, DoSomethingWithAFactoryAndUnitAndGetAnOutput)
    

    SelectMany 在概念上只是一对嵌套的 foreach 循环;用这些术语来说可能是这样的:

    var output = List<SomeOutput>();
    
    foreach(var aFactory in listOfFactories){
      var theCurrentFactory = aFactory;
      var someEnumerable = GetUnitsFromAFactory();
    
      foreach(var oneOfTheUnitsOfTheCurrentFactory in someEnumerable){
        output.Add(DoSomethingWithAFactoryAndUnitAndGetAnOutput(theCurrentFactory, oneOfTheUnitsOfTheCurrentFactory);
      }
    }
    
    //methods 
    IEnumerable<OrganizationUnit> GetUnitsFromAFactory(Factory f){
      return f.listOfTheUnits;
    }
    
    SomeOutput DoSomethingWithAFactoryAndUnitAndGetAnOutput(Factory f, Unit u) {
      ...
    }
    

    您的代码令人困惑的是,您只从“DoSomethingWithAFactoryAndUnitAndGetAnOutput”返回工厂,因此如果您有 3 个工厂,分别有 10、11 和 12 个单位,您最终会得到一个包含 33 个工厂的列表,10 次重复第一个工厂,第二个工厂重复 11 次,第三次重复 12 次。现在我们(希望)很高兴 SelectMany 是一个嵌套循环对,让我们补充一点关于单元不活动的内容

    var out = new List<Factory>
    foreach(var f in factories)
      foreach(var u in f.Units)
        if(unit.IsActive ?? false == false)
          out.Add(f);
    

    你看,你已经多次添加了一个工厂,每一个不活动的单元就添加一次

    然后您区分这些以折叠它们,因此整个 SelectMany 是一种浪费的练习,您可以改为只要求具有非活动单元的工厂。用简单的循环术语可能看起来像:

    var out = new List<Factory>
    foreach(var f in factories)
      foreach(var u in f.Units)
        if(unit.IsActive ?? false == false){
          out.Add(f);
          break;
        }
    

    在添加一个工厂后添加中断(遇到第一个非活动单元)。在渐进式添加 LINQ 术语中,这更像是在做

    foreach(var f in factories)
      if(f.Units.Any(unit => unit.IsActive ?? false == false))
        out.Add(f);
         
    

    这是

    factories.Where(f => f.Units.Any(unit => unit.IsActive ?? false == false))
    

    【讨论】:

      【解决方案4】:

      我不认为SelectMany 正在做你认为它正在做的事情。我认为您认为它正在返回带有单元的工厂,这些单元仅通过从单元集合返回工厂的某种投影过滤到 IsActivefalse 的那些。这是不正确的。

      SelectMany 的第二个选择器是您传递的 resultSelector

       (f, u) => f
      

      其中fu 是源集合的项目,以及由前一个参数投影的集合的项目(您的Where 结果)。这意味着对于满足 Where 条件 (u) 的任何单元,它返回包含它的工厂 (f)。这就是为什么你必须打电话给Distinct,因为你实际上得到了工厂每个匹配的单元。如果一家工厂有两个符合标准的设备,那么您将把同一家工厂退回两次。 Unique 从结果中删除了那些重复的工厂,但工厂本身(即其中单元的连接)没有改变。

      为了“过滤”子集合,您需要重新投影工厂:

      var fa = factories.Select(f => new Factory(
              {
                Number = f.Number, 
                Units = f.Units.Where(u => (u.IsActive ?? false) == false).ToList();
              }
            )
          );
      

      或者,或者,循环遍历工厂并删除IsActivetrue的单元。

      【讨论】:

      • 这是关于“在 linq 中选择”的练习的一部分,谢谢。
      • 好的,希望这能向您展示第二个选择器在做什么 - 不涉及任何魔法或“功能性”诡计。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-04-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-07
      相关资源
      最近更新 更多