如何在对象不可变时使用Linq创建嵌套对象图并引用它们的父对象

时间:2018-05-15 20:54:31

标签: c# linq immutability

在我尝试编写更多副作用免费代码(使用不可变类)时,当使用linq查询数据源并选择父和子关系来创建新图时,我碰到了一点砖墙由不可变对象组成。

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

当类不可变时,我无法看到如何使用linq完成此操作。 Linq和不可变类在C#中是如此重要的概念我相信我必须遗漏一些明显的东西。

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

更新:17.05.18我觉得很难相信这是不可能的,因为如果是的话,那么在我看来这是语言中的设计缺陷,语言受到相当严格的审查,......更多可能我只是遗漏了一些东西。

使用Immutable类(???)的示例是我需要修复的,如何将该引用传递给包含的类实例?

    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是可变的,我们能够通过首先创建父项,然后是子项,然后通过附加现在具有的子项来改变父状态来解决传入引用的问题。对父集的引用。我需要能够使用Immutable类来实现这一点,但是如何?

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; }
}

我希望我错过了一些明显的东西,任何帮助都会受到最高的赞赏。 谢谢 艾伦

2 个答案:

答案 0 :(得分:1)

&#39;发票&#39;在构造函数运行之后,对象才会存在。在为构造函数创建参数时,无法引用它。这与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属性。

答案 1 :(得分:0)

我很惊讶Linq不支持这一点,因为对象中的双向导航对于许多图形类型和内存查询至关重要。如果从Linq2Sql甚至实体框架中查看Table对象,您会看到很多这种双向关系正在设置中。我认为我的错误是把类作为DTO,这是一个大量重载的词。

因此,假设双向性是一个要求,并且在特定设计中有很多理由需要它,问题仍然存在。

我希望我错过了一些东西,一些聪明的人会说,嘿,只是这样做,Linq有这个功能指向(这个)正在创建的对象。这不会让我感到惊讶。

最终我能想出的最好的结果与@gnud上面所描述的非常相似,但通过添加{{1}来略微改变意图(以及缺乏语言支持)更明显方法,只允许调用一次,见下文:

我做了以下更改

  • AttachParent添加到订单项,但使用了私有设置器。
  • public Invoice Parent { get; private set; }添加到LineItem.cs
  • 最后,在构造函数中更改了由LineItem附加的父级,但不必克隆传入的项目。

我认为最后一点是团队决定他们想要在多大程度上采用不变性的风格问题。这不是我的项目中具体的内容。在我的用例中,我正在处理数十亿个图节点,Invoice和Item示例纯粹是我能想到的用于演示Linq问题的最小代码。需要克隆所有对象以在我的用例中设置其父对象将是昂贵的,并不一定导致更安全的代码。

而不是.AsReadOnly我只是简单地使用数组,因为代码只会由我使用,而我在项目中使用的约定是数组表示只读集合,而数组使代码成为代码超级清洁,更容易扫描,并立即识别签名等中的任何属性。

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;
        }

更新:上午10点16分 如果你想完全放下不可变类的兔子洞,那么使用数组是一个很大的禁忌。请在底部查看我的上一条评论。这个问题的真正焦点不是编写不可变类,而是如何创建一个嵌套对象,当使用linq时它指向它的父级,并且这些类恰好是不可变的,即你想用单个LINQ投影做到这一点一气呵成,干净利落。 :) 感谢大家的反馈和快速解答,你们摇滚!