【问题标题】:What magic is this?这是什么魔法?
【发布时间】:2013-02-04 15:15:26
【问题描述】:

我最近看到了臭名昭著的 Jon Skeet 关于使用 LINQ to XML 的帖子。这段特殊的 sn-p 代码引起了我的注意:

// Customers is a List<Customer>
XElement customersElement = new XElement("customers",
    customers.Select(c => new XElement("customer", //This line is "magic"
        new XAttribute("name", c.Name),
        new XAttribute("lastSeen", c.LastOrder)
        new XElement("address",
            new XAttribute("town", c.Town),
            new XAttribute("firstline", c.Address1),
            // etc
    ));

我决定在我的应用程序中自己测试它,我设置了一个 foreach 循环,如下所示:

foreach (var kvp in m_jobs) {    //m_jobs is a Dictionary<string,Job>
    m_xmlDoc.Root.Element("SCHED_TABLE").Add(
        kvp.Value.GenerateXmlNode())
    );
}

我修改为:

m_xmlDoc.Root.Element("SCHED_TABLE").Add(
    m_jobs.Select(job => job.Value.GenerateXmlNode())
};

GenerateXmlNode() 是一种为特定作业项生成适当 XML 标记的方法。我不确定会发生什么,但你瞧,它的工作原理与我的 foreach 循环完全一样。我不太明白的是为什么?!另外,这被认为是 LINQ 的“滥用”还是“功能”?

为清楚起见进行编辑:我知道 .Select 将返回一个 IEnumerable,其中包含我所要求的内容,但我没有明确列举它。我了解 .Add 的工作原理,因为它接受可变数量的参数,但同样,我没有明确列举传递这些参数。那么......它仍然如何工作?

【问题讨论】:

  • 为什么什么?考虑到 XElement.Add 的广告用途,这段代码有什么令人惊讶的地方?
  • @JoelEtherton 实际上,这令人惊讶。方法重载是Add(Object),而不是Add(IEnumerable&lt;object&gt;)。你怎么知道它需要一个序列或单个项目,而不仅仅是单个项目?
  • @PaulBellora 谢谢,完成。
  • @JoelEtherton 你的意思是什么?通常,从方法的公共 API 中应该可以清楚地看出它是接受序列还是单个项目。有一个object 类型的参数来确定该类型是否实现IEnumerable 并在实现时采取不同的行为,这是非常 奇怪和意外的。我知道我没想到,我预计 pubic void Add(IEnumerable objects) 的方法会额外重载。
  • @Servy:我很抱歉。我粘贴了错误的链接。这个链接描述了为什么它并不令人惊讶:msdn.microsoft.com/en-us/library/bb943882.aspx。如果你 RTM,你会看到它表明可以传入复杂的内容:Any type that implements IEnumerable

标签: c# xml linq


【解决方案1】:

XElement.Add 方法在底层看起来是这样的:

public void Add(object content)
{
    if (content is IEnumerable)
    {
        foreach (object child in (IEnumerable)content)
            Add(child);
    }
    else
    {
        //process individual element
    }
}

因此,虽然从 Add 的公共接口中并不清楚,但您可以将一系列项目或单个项目传递给它,它会在运行时确定它是哪个并采取相应的行动。

【讨论】:

  • 有趣,所以这只是因为 Add() 在后台支持它,我不应该期望它在其他领域也能工作?
  • @Kittoes,是的,因为这个类中的 Add 非常特别。请尽量不要根据这种情况自己编写这样的代码 - 绝对没有办法根据方法签名来确定这种行为。
  • @AlexeiLevenkov 正是我的想法,如果我在方法签名中看到 IEnumerable,那么发生的事情将非常明显。
【解决方案2】:

没有魔法; Add 方法接受 objectparams object[] - 在内部它只是检查每个输入的一系列常见场景,包括 IEnumerable 等。然后它只是展开序列,添加子元素/属性它发现。 LINQ 从Select 返回(在这种情况下)一个IEnumerable 序列,这使得它完全可用。

【讨论】:

    【解决方案3】:

    Select 是一个LINQ extension method,可以应用于任何实现IEnumerable&lt;T&gt; 的类型。作为参数,它接受委托或lambda expression(此处为 lambda 表达式)。这个 lambda 表达式定义了一个 ad-hoc 函数,该函数应用于集合的每个元素。这些元素在这里用c 表示。 Select 产生一个 IEnumerable&lt;U&gt;,其中 U 是 lambda 表达式返回的项目的类型。换句话说,Select 通过 lambda 表达式(这里是 Customer to XElements)将 T 类型的元素转换为 U 类型的元素。

    由于XElement 构造函数的第二个参数也接受枚举,因此这种“魔法”是可能的。

    public XElement(
        XName name,
        Object content
    )
    

    content 可以是 IEnumerable&lt;XElement&gt; 等等。 System.Xml.Linq 命名空间非常灵活。还有一个从 stringXName 的隐式转换,允许您将字符串作为第一个参数传递。

    【讨论】:

    • "Since the second argument to the XElement constructor accepts enumerations as well, this "magic" is possible." 他不是在调用 XElement 构造函数,而是在调用 Add 方法,该方法不接受枚举作为参数。
    • 问题是关于Add的,没有接受枚举的构造函数(params,但不是IEnumerable)。
    • “魔法”发生在第一个代码 sn-p 中,没有调用 AddXElement 有一个构造函数重载:public XElement( XName name, Object content)。这个content 可以是IEnumerable&lt;XElement&gt;
    • @OlivierJacot-Descombes 第二个和第三个 sn-p 与第三个完全无关。我专门在.Add 语句中使用.Select,尽管Add() 没有接受IEnumerable 的重载,但代码运行得很好。在 dotPeek 中反编译 System.Xml.Linq 后,我可以看到 XContainer 对象包含 Add() 函数,该函数的工作方式与上述答案中的描述完全相同。它检查传递的对象是可枚举的还是数组(以及其他一些东西)。
    • 好的,评论'This line is "magic"' 应该在第三个sn-p 然后而不是第一个。我描述了标有“魔法”的那一行的“魔法”。
    【解决方案4】:

    这个问题其实由三个为什么组成。在魔法最大的秘密终于揭开之前,我们首先需要破解魔法师密码,这样我们才能看穿问题:

    Func<Customer, XElement> selector=
        c => {
            var xe=new XElement("address",
                new XAttribute("town", c.Town),
                new XAttribute("firstline", c.Address1)
                // , etc
                );
    
            return
                new XElement("customer", // This line is a part of the "magic"
                    new XAttribute("name", c.Name),
                    new XAttribute("lastSeen", c.LastOrder),
                    xe
                    );
        };
    
    XElement customersElement=new XElement("customers", customers.Select(selector)); // This line is another part of the "magic"
    

    Customer 类假定在 LastOrderAddress1TownName 的字段或属性中。以下是分开的问题和答案:

    Q1:看了你的sn-p代码,为什么IEnumerable中元素的成员不用你明确列举就可以访问?

    A1:元素通过lambda表达式传递。也就是说,传递给Select 的参数是一个委托。因此,您可以访问使用c 传递的元素的成员。我们在委托中调用的构造函数是XElement(XName name, params object[] content) 的重载,你可以将对象传递给XAttributeXElement

    Q2:在代码的两种不同语法中使用 sn-ps,为什么带有 Select 的投影枚举的 Add 语句的工作方式就像调用 XElement 的构造函数一样一个选定的可枚举,产生与代码使用 foreach?

    相同的结果

    A2IEnumerableIEnumerable&lt;T&gt; 作为对象传递给继承自 XContainerXElementAdd 方法的重载构造函数。

    Q3:根据Q2,为什么IEnumerable在已经作为对象传递的情况下仍然可以枚举?

    A3:一个类的实例总是是它的类的一个(n)实例,即使你用更大的类型、泛型类型或接口传递它。虽然在这里我们没有遇到界面问题,但我建议看一下 [this answer] 来了解一下。在XContainer.Add(object content) 的代码中,IEnumerable 的传递参数将使用以下代码进行处理:

    IEnumerable enumerable=content as IEnumerable;
    
    if(enumerable!=null) {
        foreach(object element in enumerable) {
            this.Add(element);
        }
    }
    else {
        this.AddString(GetStringValue(content));
    }
    

    但是,如果content 是一个数组,而不是as IEnumerable,它将使用以下代码进行处理(也许是为了获得性能):

    object[] objArray=content as object[];
    
    if(objArray!=null) {
        foreach(object element in objArray) {
            this.Add(element);
        }
    }
    

    你可能想知道为什么即使传递一个数组也会调用XContainer.Add(object content),这是因为Add重载了数组是:

    public void Add(params object[] content) {
        this.Add(content);
    }
    

    现在,您知道它是如何完成的了。

    【讨论】:

      猜你喜欢
      • 2012-05-14
      • 2016-05-15
      • 2023-03-18
      • 1970-01-01
      • 2013-09-24
      • 1970-01-01
      • 2020-07-01
      • 1970-01-01
      相关资源
      最近更新 更多