【问题标题】:How to create nested object graphs using Linq when the objects are immutable and reference their parent当对象不可变并引用其父对象时,如何使用 Linq 创建嵌套对象图
【发布时间】:2018-05-18 05:51:49
【问题描述】:

在我尝试编写更多无副作用的代码(使用不可变类)时,我在使用 linq 查询数据源并选择父子关系以创建一个由不可变对象。

当我有一个子对象时问题就出现了,例如invoiceLineItem 对象需要在其构造函数中传递对父对象的引用,以便.Parent 属性将引用父对象,例如lineItem[2].Parent 引用 Invoice

当类是不可变的时,我看不出如何使用 linq 来做到这一点。 Linq 和不可变类在 C# 中是如此重要的概念,我相信我一定遗漏了一些明显的东西。

我将展示一些演示问题的代码,首先我将展示使用不可变类似乎没有使用 Linq 的解决方案,然后在下面我将展示如何使用可变类来做到这一点,我当然会这样做不想做。

更新 : 17.05.18 我很难相信这是不可能的,因为如果是这样,那么在我看来那是语言的设计缺陷,并且该语言受到了相当严格的审查,... 更多可能我只是错过了一些东西。

我需要修复不可变类的示例( ??? ),如何在该点将引用传递给包含的类实例?

    void Main()
    {
        var nums = new[]{
                new { inv = 1, lineitems =new [] {new { qty = 1, sku = "a" }, new { qty = 2, sku = "b" }}},
                new { inv = 2, lineitems =new [] { new { qty = 3, sku = "c" }, new { qty = 4, sku = "d" }}},
                new { inv = 3, lineitems =new [] { new { qty = 5, sku = "e" }, new { qty = 5, sku = "f" }}}
        };

        // How do I pass in the reference to the newly being created Invoice
        // below to the Item constructor?

        var invoices = nums.Select(i => 
            new Invoice(i.inv, i.lineitems.Select(l => 
                new Item(l.qty, l.sku, ??? )
        )));

        invoices.Dump();
    }

    public class Invoice 
    {
        public int Number { get; }
        public IEnumerable<Item> Items { get; }
        public Invoice(int number, IEnumerable<Item> items) {
            Number = number;
            Items = items;
        }
    }

    public class Item 
    {
        public Invoice Parent { get; }
        public int Qty { get; }
        public string SKU { get; }
        public Item(int qty, string sku, Invoice parent) {
            Parent = parent;
            Qty = qty;
            SKU = sku;
        }
    }

相同的类,但这次 DTO 是可变的,我们能够通过首先创建父级,然后是子级,然后通过附加现在具有对父集的引用。我需要能够使用不可变类来做到这一点,但是如何做到这一点?

void Main()
{
    var nums = new[]{
            new { inv = 1, lineitems =new [] {new { qty = 1, sku = "a" }, new { qty = 2, sku = "b" }}},
            new { inv = 2, lineitems =new [] { new { qty = 3, sku = "c" }, new { qty = 4, sku = "d" }}},
            new { inv = 3, lineitems =new [] { new { qty = 5, sku = "e" }, new { qty = 5, sku = "f" }}}
    };

    var invoices = nums.Select(i => 
    {
        var invoice = new Invoice() 
        { 
            Number = i.inv
        };
        var items = from item in i.lineitems select new Item() 
        { 
            Parent = invoice, Qty = item.qty, SKU = item.sku 
        };
        invoice.Items = items;
        return invoice;
    });

    invoices.Dump();
}

public class Invoice
{
    public int Number { get; set; }
    public IEnumerable<Item> Items { get; set; }
}

public class Item
{
    public Invoice Parent { get; set; }
    public int Qty { get; set; }
    public string SKU { get; set; }
}

我希望我错过了一些明显的东西,任何帮助将不胜感激。 谢谢你 艾伦

【问题讨论】:

  • 鉴于您对自己的限制,我认为没有办法做您想做的事。您有两个对象“同时”需要彼此的实例。
  • 顺便说一句,您不应该将 IEnumerable 存储在这样的属性中。滥用的可能性很大。您不能确定是否可以枚举一个 ienumerable 两次。将其转换为某种只读集合。

标签: c# linq immutability


【解决方案1】:

'Invoice' 对象在构造函数运行之前不存在。在为构造函数创建参数时无法引用它。这与 LINQ 无关。

就个人而言,我认为我会完全放弃指向父级的链接。你什么时候真正需要它?

如果 Line 可以在没有父级的情况下存在,您可以使用 null 父级创建它们,然后在 Invoice 构造函数中使用父级创建一个新项目。

public class Invoice
{
    public int Number { get; }
    public IReadOnlyList<Item> Items { get; }
    public Invoice(int number, IEnumerable<Item> itemsWithoutParent)
    {
        Number = number;
        Items = itemsWithoutParent
           .Select(x => new Item(x.Qty, x.SKU, this))
           .ToList().AsReadOnly();
    }
}

如果您不希望 Line 没有父对象,那么 Invoice 构造函数必须采用一系列 Line 工厂函数,而不是 Line 对象。

public class Invoice
{
    public int Number { get; }
    public IReadOnlyList<Item> Items { get; }
    public Invoice(int number, IEnumerable<Func<Invoice,Item>> items)
    {
        Number = number;
        Items = items
          .Select(x => x(this))
          .ToList().AsReadOnly();
    }
}   
/* usage */
var invoices = nums.Select(i =>
        new Invoice(i.inv, i.lineitems.Select(l => 
            (Func<Invoice,Item>)(parent => new Item(l.qty, l.sku, parent))))
    ).ToList();

重复一遍,我更愿意从您的 DTO 中完全删除 Parent 属性。

【讨论】:

  • 要求链接到父级是 OP 的要求,也是问题的核心。没有这个要求,就没有问题。在对象图中提供双向导航性是一项常见要求。 OP 为使用误导性词 DTO 道歉 :)
【解决方案2】:

我很惊讶 Linq 不支持这一点,因为对象中的双向导航对于许多图形类型和内存查询来说是必不可少的。如果您查看来自 Linq2Sql 甚至 Entity Framework 的 Table 对象,您会看到很多这种类型的双向关系正在设置中。我认为我的错误是将类称为 DTO,这是一个严重超载的词。

因此,假设双向性是一项要求,并且有很多原因您希望在特定设计中这样做,问题仍然存在。

我希望我遗漏了一些东西,一些聪明的人会过来说,嘿,就这样做吧,Linq 有这个 func 的东西,它指向正在创建的对象的(this)。这不会让我感到惊讶。

我最终能想到的最好的方法与@gnud 上面描述的非常相似,但是通过添加AttachParent 方法,稍微改变了意图(以及缺乏语言支持) ,并且只允许调用一次,见下文:

我做了以下更改

  • 已将 public Invoice Parent { get; private set; } 添加到订单项,但使用私有设置器。
  • 已将public void AttachParent(Invoice parent) { if(parent!=null) throw new InvalidOperationException("Class is immutable after parent has already been set, cannot change parent."); Parent = parent; } 添加到 LineItem.cs
  • 最后,将父项更改为由构造函数内的 LineItem 附加,但不必克隆传入的项。

我认为最后一点实际上是团队的风格问题,以决定他们希望在不变性方面走多远。这不是我项目中具体的东西。在我的用例中,我正在处理数十亿个图形节点,Invoice 和 Item 示例纯粹是我能想到的用于演示 Linq 问题的最小代码。需要克隆所有对象以在我的用例中设置它们的父对象会很昂贵,并且不一定会导致更安全的代码。

我只是使用数组而不是 .AsReadOnly,因为代码仅供我使用,而我在项目中使用的约定是数组表示只读集合,而数组使代码超级干净,更容易扫描并立即识别签名等中哪些属性是只读的。

    void Main()
    {
        var nums = new[]{
                new { inv = 1, lineitems =new [] {new { qty = 1, sku = "a" }, new { qty = 2, sku = "b" }}},
                new { inv = 2, lineitems =new [] { new { qty = 3, sku = "c" }, new { qty = 4, sku = "d" }}},
                new { inv = 3, lineitems =new [] { new { qty = 5, sku = "e" }, new { qty = 5, sku = "f" }}}
        };

        // How do I pass in the reference to the newly being created Invoice below to the Item constructor?

        var invoices = nums.Select(i => 
            new Invoice(i.inv, i.lineitems.Select(l => 
                new Item(l.qty, l.sku )
            ).ToArray()
        ));

        invoices.Dump();
    }

    public class Invoice 
    {
        public int Number { get; }
        public Item[] Items { get; }
        public Invoice(int number, Item[] items) {
            Number = number;
            Items = items;
            foreach(var item in Items) item.AttachParent(this);
        }
    }

    public class Item 
    {
        public Invoice Parent { get; private set; }
        public int Qty { get; }
        public string SKU { get; }
        public Item(int qty, string sku) {
            Qty = qty;
            SKU = sku;
        }

        public void AttachParent(Invoice parent) {
            if(Parent!=null) throw new InvalidOperationException("Class is immutable after parent has already been set, cannot change parent.");
            Parent = parent;
        }
    }

更新时间:5 月 16 日上午 10 点 如果您想完全进入不可变类兔子洞,那么使用数组是一个很大的禁忌。请在底部查看我的最后一条评论。这个问题的真正重点不是编写不可变类,而是如何在使用 linq 时创建一个指向它的父对象的嵌套对象,而这些类恰好是不可变的,即你想在哪里使用单个 LINQ 投影,一气呵成,干净整洁。 :) 感谢大家的反馈和快速回答,你们摇滚!

【讨论】:

  • fyi ...所有代码都可以剪切并粘贴到Linqpad中并运行。
  • 这段代码没有运行,你有一个错字,parent -> ParentAttachParent 方法中。但我真正想警告你的是在不可变代码中使用数组。数组真的不是一成不变的。如果你想要数组,你必须在每次使用它们时复制它们。最好使用只读集合包装器,例如 IReadOnlyList.
  • 完全同意,好点,我只是登录以将 Gnud 的答案标记为正确。我正在重新阅读我的笔记,并意识到我的数组约定非常危险。没有人会阅读我的代码并假设我的数组始终是副本。为了完整起见,我的问题确实特别要求关注不变性,所以我同意。我自己辩护,您不应该修改作为参数传递给您的数组中的项目。 (无副作用/功能编码)等
  • Parent 属性上很受欢迎。 :)
  • 读了一点,术语“冻结”(或可冻结对象)被提及或者是描述行为的“域”方式。在上面的代码示例中,与其说class is immutable after attaching parent,不如说class is frozen after attaching parent。小点,但有助于理解即使在主流接受的最佳实践库中,例如System.Collections.immutable 某些类可以是临时可变的,例如在构造函数中。
猜你喜欢
  • 2017-03-05
  • 2018-07-24
  • 2020-03-20
  • 2016-12-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-02
  • 2019-02-20
相关资源
最近更新 更多