代码之家  ›  专栏  ›  技术社区  ›  snowcode

当对象不可变并引用其父对象时,如何使用Linq创建嵌套对象图

  •  0
  • snowcode  · 技术社区  · 8 年前

    在我尝试编写更多无副作用的代码(使用不可变类)时,当使用linq查询数据源并选择父关系和子关系以创建由不可变对象组成的新图形时,我遇到了一些困难。

    当我有一个子对象(例如invoiceLineItem对象)需要在其构造函数中向其传递对父对象的引用时,这个问题就会出现,因此 .Parent 属性将引用父级,例如。 lineItem[2].Parent 参考 Invoice .

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

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

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

    带有不可变类的示例(the???)我需要解决的是,如何在该点将引用传递给包含类实例?

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

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

    2 回复  |  直到 8 年前
        1
  •  1
  •   gnud    8 年前

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

    就我个人而言,我想我会完全删除与家长的链接。你什么时候真正需要它?

    如果一条线可以在没有父线的情况下存在,则可以使用 null 父项,然后在发票构造函数内使用父项创建新项目。

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

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

    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中完全删除父属性。

        2
  •  0
  •   snowcode    8 年前

    我很惊讶Linq不支持这一点,因为对象中的双向导航对于许多图形类型和内存查询都是必不可少的。如果你看 Table 对象,您将看到许多这种类型的双向关系正在建立。我认为我的错误是将类称为DTO,这是一个超负荷的词。

    因此,假设双向性是一个要求,并且在特定的设计中有很多原因,那么问题仍然存在。

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

    最后我能想到的最好的方法与@gnud上面所描述的非常相似,但有一点扭曲,通过添加一个 AttachParent 方法,并且只允许调用一次,请参见以下内容:

    我做了以下更改

    • 补充 public Invoice Parent { get; private set; } 到行项目,但使用专用setter。
    • 补充 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; } 至行项目。cs公司
    • 最后,将父级更改为由构造函数内的LineItem附加,但不必克隆传入的项。

    我认为,最后一点对于团队来说确实是一个风格问题,以决定他们想要在不变性方面走多远。在我的项目中,这并不是什么具体的东西。在我的用例中,我实际上要处理数十亿个图形节点,发票和项目示例是我所能想到的演示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投影,一次完成漂亮而干净的操作。:) 谢谢大家的反馈和快速回答,你们真是棒极了!