第10章 LINQ to XML

第10章 LINQ to XML

10.1 架构概述——DOM 和 LINQ to XML 的 DOM

XML 文档可以用一棵对象树完整的表示,这称为“文档对象模型(document object model)”

LINQ to XML 由两部分组成:

  1. XML DOM,简称为 X-DOM
  2. 大约 10 个查询运算符

LINQ 也可以用于查询 W3C 标准的旧 DOM,不过 X-DOM 对 LINQ 查询更为友好:

  1. X-DOM 部分方法可以返回 IEnumerable​ 序列;
  2. X-DOM 的构造器支持通过 LINQ 构建对象树。

Tips

W3C 标准的 DOM 对应 C# 中的 XmlDocument​;X-DOM 对应 C# 中的 XDocument​ 等一系列类型。

更多内容见12 System.Xml 的使用

10.2 X-DOM 概览

XObject​ 是所有类型的基类,XElement​ 和 XDocument​ 是所有容器类型的基类

image

以如下代码为例,它对应的 X-DOM 如图:

image

string xml = @"
<customer id='123' status='archived'><firstname>Joe</firstname><lastname>Bloggs<!--nice name--></lastname>
</customer>
";XElement customer = XElement.Parse(xml);
XObject

XObject​ 为抽象类,是所有 XML 内容(XML Content)的 类,它内含一个指向 元素( Parent element)的属性、一个指向 XDocument ​ 的(可选)属性。

XNode

XNode​ 为抽象类,是大多数 XML 内容的基类(不含 XAttribute ​)。XNode​ 指向 元素(Element),不会指向 节点(Node)。

image

XContainer

我们在 XNode​ 提到,XNode​ 不会指向 节点。指向 节点的工作由它的派生类 XContainer​ 完成。

XContainer​ 为抽象类,用于处理子项。它也是 XElement ​ 和 XDocument ​ 的基类。

XElement

XElement​ 引入了诸如 Name​、Value​ 等成员,用于管理特性。多数 XElement​ 仅包含一个 XText ​ 节点,Value​ 用于快捷地 get、set 其内容。

XDocument

XDocument​ 封装了根节点的 XElement ​,添加了 XDeclaration ​、一系列处理指令及其他根元素+功能。

与 W3C DOM 不同,XDocument​ 是可选的,因此我们可以高效移动任意子节点至其他 X-DOM 中。

10.2.1 加载和解析

XElement​ 和 XDocument​ 提供了静态 Load ​ 和 Parse ​ 方法,用于从现有源建立 X-DOM 树。支持的源有:

  • Load ​ 方法:

    通过文件建立 X-DOM:URI、Stream​、TextReader​、XmlReader

    XDocument fromWeb = XDocument.Load ("http://albahari.com/sample.xml");
    XElement fromFile = XElement.Load (@"e:\media\somefile.xml");
    XElement config = XElement.Parse (@"
    <configuration><client enabled='true'><timeout>30</timeout></client>
    </configuration>");
    
  • Parse ​ 方法:

    通过字符串建立 X-DOM

Tips

XNode​ 也提供了一个静态方法 ReadFrom ​,从 XmlReader ​ 中实例化+填充任意类型的节点(node)。与 Load​ 不同,它每次仅读取一个完整节点,因此我们可以用它进行手动读取。

10.2.2 保存和序列化

任何 node 实例都可以通过其 ToString ​ 方法输出 XML 格式的字符串,通过 WriteTo ​ 方法将数据写入 XmlWriter ​ 中。

Tips

通过 ToString ​ 获得的字符串包含缩进、换行等格式化内容,可以通过传入 SaveOptions.DisableFormatting​ 参数关闭该特性。

注意:若原始 XML 内容包含格式化内容,即使传入 SaveOptions.DisableFormatting​ 参数,仍会保持原缩进样式。

XElement​ 和 XDocument​ 提供了 Save ​ 方法将 X-DOM 保存至 URI、Stream、TextWriter​、XmlWriter​ 中。该方法会自动添加 XML 声明(见10.7.2 XML 声明(declaration))。

10.3 实例化 X-DOM

10.3.0 构造器 + Add​ 方法

任意 XContainer ​ 的子类都可以使用构造器 + Add​ 方法创建 X-DOM 树,方法如下:

XElement lastName = new XElement ("lastname", "Bloggs");
lastName.Add (new XComment ("nice name"));XElement customer = new XElement ("customer");
customer.Add (new XAttribute ("id", 123));
customer.Add (new XElement ("firstname", "Joe"));
customer.Add (lastName);customer.Dump();
<customer id="123"><firstname /><lastname>Bloggs<!--nice name--></lastname>
</customer>

其中 Name ​ 参数必选, Value ​ 参数可选(可以在创建完成后再设置 Value​ 值)。Value​ 对应 XText​ 节点,它会被隐式创建。

10.3.1 函数式构建(Functional Construction)

X-DOM 支持“函数式”构建(源自函数式编程 functional programming),用法如下:

new XElement ("customer", new XAttribute ("id", 123),new XElement ("firstname", "joe"),new XElement ("lastname", "bloggs",new XComment ("nice name"))
)

Eureka

XElement​ 利用了 params​ 关键字实现该效果:

public XElement(XName name, params object?[] content)

优点有2:

  1. 和 XML 自身结构相似;

  2. 它可以使用 LINQ 的 select​ 语句。

    以如下代码为例,其中 Customers​ 为 EF Core 实例。

    new XElement ("customers",from c in Customers.AsEnumerable()select new XElement ("customer",new XAttribute ("id", c.ID),new XElement ("name", c.Name,new XComment ("nice name")))
    )
    

10.3.2 指定内容(Specifying Content)

实际上,10.3.1 函数式构建(Functional Construction)利用了 C# 中的可选参数数组,XElement​ 的构造器和 XContainer​ 的 Add​ 方法定义如下:

public XElement(XName name, params object?[] content)
public void Add (params object[] content)

XContainer​ 将可选参数数组的所有对象都转为了 NodeAttribute ,其处理逻辑如下:

  1. 忽略 null 对象;
  2. XNode​、XStreamingElement​ 对象,添加至 Node 集合中;
  3. XAttribute​ 对象,添加至 Attribute 集合中;
  4. string​ 对象,包装成 XText ​ 节点,添加至 Node 集合中;
  5. IEnumerable ​ 对象,遍历所有内容,按照 1~4 步处理;
  6. 其他:将对象转化为 string ​,按照步骤 4 处理。

Tips

object​ 包含 ToString​ 方法,所有 object​ 都可以转化为 XText​ 节点,因此不存在无效对象。

此外,XContainer​ 在调用 ToString​ 前会检查对象是否是如下类型,是则调用 XmlCovert​,保证序列化不受 CultureInfo​ 影响,符合 XML 格式规则:

float​、double​、decimal​、bool​、DateTime​、DateTimeOffset​、TimeSpan

10.3.3 自动深度克隆(Automatic Deep Cloning)

如XObject中提到,所有元素都包含 Parent Element 指针。当实例已有 Parent,将其赋值给其他 XContainer​ 时,将自动进行深克隆:

var address =new XElement ("address",new XElement ("street", "Lawley St"), new XElement ("town", "North Beach"));var customer1 = new XElement ("customer1", address);
var customer2 = new XElement ("customer2", address);customer1.Element ("address").Element ("street").Value = "Another St";
customer2.Element ("address").Element ("street").Value.Dump();    // 输出 Lawley St

Extra

因 X-DOM 深拷贝的特性,它的实例化没有任何副作用,这也是“函数式编程”的特点。

10.4 导航和查询(Navigating and Querying)

XNode​ 和 XContainer​ 定义了方法和属性用于游历 X-DOM 树。与常规 DOM 不同,这些函数返回单个值或 IEnumerable<T>​ 对象,而非 IList<T>​,因此需要通过 LINQ 进行查询。

Warn

在 X-DOM 中,Element 和 Attribute 的 name 是大小写敏感的,与 XML 一致。

10.4.1 导航至子节点

mindmap 子节点单一子节点FirstNodeLastNodeElement单层子节点NodesElements深层子节点DecendantsDecendantNodes

C 12 in a Nutshell The Definitive Reference.pdf - p547 - C 12 in a Nutshell The Definitive Reference-P547-20240426173112-6bd6490

Tips

*​ 的方法可以在序列(sequences)上使用(由 LINQ 支持)。

Info

本节用到的 XML 内容均为:

<bench><toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox><toolbox><handtool>Saw</handtool><powertool>Nailgun</powertool></toolbox><!--Be careful with the nailgun-->
</bench>

10.4.1.1 FirstNode()​、LastNode()​ 和 Nodes()

这三个方法(属性)用于操作 直接 子节点,Nodes()​ 返回 所有直接子节点序列(sequences )。以如下代码为例:

var bench =new XElement ("bench",new XElement ("toolbox",new XElement ("handtool", "Hammer"),new XElement ("handtool", "Rasp")),new XElement ("toolbox",new XElement ("handtool", "Saw"), new XElement ("powertool", "Nailgun")),new XComment ("Be careful with the nailgun"));bench.FirstNode.ToString(SaveOptions.DisableFormatting).Dump ("FirstNode");
bench.LastNode.ToString(SaveOptions.DisableFormatting).Dump ("LastNode");foreach (XNode node in bench.Nodes())Console.WriteLine (node.ToString (SaveOptions.DisableFormatting) + ".");
FirstNode:
<toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox>LastNode:
<!--Be careful with the nailgun-->Nodes():
<toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox>.
<toolbox><handtool>Saw</handtool><powertool>Nailgun</powertool></toolbox>.
<!--Be careful with the nailgun-->.

Tips

FirstNode​ 和 LastNode​ 的返回值类型为 XNode ​,Nodes​ 的返回值类型为 IEnumerable<XNode> ​。

10.4.1.2 检索 elements

Elements()​ 方法返回 XElement ​ 类型的单层子节点:

foreach (XNode node in bench.Elements("handtool"))Console.WriteLine(node.ToString(SaveOptions.DisableFormatting) + ".");
<toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox>.
<toolbox><handtool>Saw</handtool><powertool>Nailgun</powertool></toolbox>.

Elements()​ 可以返回指定名称的元素:

int toolboxCount = bench.Elements ("toolbox").Count();

Summary

从上面两个例子可以看出,Nodes()​ 与 Elements()​ 的区别:

  • Nodes() 支持寻找指定元素;
  • Elements()​ 只列出 XElement ​ 成员。
Elements()​ 与 LINQ
<bench><toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox><toolbox><handtool>Saw</handtool><powertool>Nailgun</powertool></toolbox><!--Be careful with the nailgun-->
</bench>

如下代码查询含有 Nailgun ​ 的 toolBox​:

var toolboxWithNailgun =from toolbox in bench.Elements()where toolbox.Elements().Any (tool => tool.Value == "Nailgun")select toolbox.Value;

如下代码查询所有 handtool ​:

var handTools =from toolbox in bench.Elements()from tool in toolbox.Elements()where tool.Name == "handtool"select tool.Value;

如下代码返回指定名称的元素:

var count = bench.Elements().Where(e => e.Name == "toolbox").Count();

等价于:

int toolboxCount = bench.Elements ("toolbox").Count();
Elements()​ 与 IEnumerable<T> where T : XContainer

XContainer.Elements()​ 方法的 LINQ 查询与 XContainer.Nodes()​ 方法的 LINQ 查询等价,之前的示例还可以写为:

from toolbox in bench.Nodes().ofType<XElement>()
where ...

但是 XContainer​ 有额外的扩展方法,XElement​ 作为它的子类同样可以用它处理元素序列。使用方式形下:

var handTools2 =from tool in bench.Elements ("toolbox").Elements ("handtool")select tool.Value.ToUpper();

上述查询,第一次调用的 Elements​ 方法绑定的是 XContainer​ 的实例方法,而第二次 Elements​ 方法则绑定到了扩展方法上。

Eureka

Nodes​ 方法的返回值类型是 IEnumerable<XNode>​,Elements​ 方法的返回值是 IEnumerable<XElements>​,而 LINQ 的 Elements​ 方法不支持 IEnumerable<XNode>​,因此无法对 Nodes​ 使用 Elements​ 方法。

10.4.1.3 检索单个元素(element)

Element()​ 方法等价于 LINQ 中的 FirstOrDefault ​,返回单层子节点匹配到的第一个元素,若元素不存在,返回 null。

Tips

Element("xyz").Value​ 调用在 xyz 元素不存在时将 抛出 NullReferenceExceptionXElement​ 为 string​ 类型定义了显式转换,可以通过强制类型避免此异常。即:

string xyz = (string)settings.Element ("xyz");

当然,我们也可以使用 ?.

10.4.1.4 获取子元素:Descendants​ 和 DescendantNodes

XContainer ​ 提供了 Descendants​ 方法和 DescendantNodes​ 方法,用于访问全部子元素(Element)或全部子节点(Node)(以至整棵树)。

Descendants ​ 方法可以接收一个元素名称,返回所有子元素 (XElement​ 对象)。

DescendantNodes ​ 方法不接收参数,返回所有类型的子节点(包括 XText​)。

以如下代码为例,输出内容如下:

/*输出 XElement 元素
<toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox>
<handtool>Hammer</handtool>
<handtool>Rasp</handtool>
<toolbox><handtool>Saw</handtool><powertool>Nailgun</powertool></toolbox>
<handtool>Saw</handtool>
<powertool>Nailgun</powertool>
*/
foreach (var node in bench.Descendants())Console.WriteLine(node.ToString(SaveOptions.DisableFormatting));
/* 输出全部节点
<toolbox><handtool>Hammer</handtool><handtool>Rasp</handtool></toolbox>
<handtool>Hammer</handtool>
Hammer
<handtool>Rasp</handtool>
Rasp
<toolbox><handtool>Saw</handtool><powertool>Nailgun</powertool></toolbox>
<handtool>Saw</handtool>
Saw
<powertool>Nailgun</powertool>
Nailgun
<!--Be careful with the nailgun-->
*/
foreach (XNode node in bench.DescendantNodes())Console.WriteLine (node.ToString (SaveOptions.DisableFormatting));

10.4.2 导航至父节点

XNode​ 及其子类(XDocument​ 除外)可以使用 AncestorXXX ​ 方法导航至父节点,父节点的类型必然是 XElement ​。

C 12 in a Nutshell The Definitive Reference.pdf - p550 - C 12 in a Nutshell The Definitive Reference-P550-20240427212207-l2o5u2p

Ancestors​ 返回一个序列,第一个元素是 Parent​,第二个元素是 Parent.Parent ​,直至根元素。

Tips

XDocument​ 不是任何节点的父节点,但是任何 XObject​ 都可以通过 Document ​ 属性访问 XDocument​。

Tips

可以使用 LINQ 查询根元素:

var root = bench.AncestorsAndSelf().Last();

上述代码不使用 Ancestors​ 方法,是因为 bench​ 本身可能就是根节点。

如果存在 XDocument​,也可以通过 XObject.Document.Root ​ 属性获取根节点。

10.4.3 导航至同级节点

C 12 in a Nutshell The Definitive Reference.pdf - p551 - C 12 in a Nutshell The Definitive Reference-P551-20240427214323-j9cnmbw

可以像链表一样使用 PreviousNode​ 和 NextNode​ 属性遍历节点。

Extra

事实上,节点在内部确实是以(单)链表的方式存储,因此 PreviousNode​ 属性的效率较低。

10.4.4 导航至节点的 Attribute

C 12 in a Nutshell The Definitive Reference.pdf - p551 - C 12 in a Nutshell The Definitive Reference-P551-20240427214634-4we23m4

Attribute ​ 方法接受 name​ 参数,返回 0~1 个元素的序列(一个 XML 元素不能包含同名 Attribute)。

Tips

上述是 XElement​ 中的方法,XAttribute​ 类型还提供了 Parent​ 属性、PreviousAttribute​ 和 NextAttribute​ 属性。

10.5 更新 X-DOM

mindmap 更新子节点XNodeAddBeforeSelfAddAfterSelfRemoveReplaceWithXContainerAddAddFirstRemoveNodesReplaceNodesXElementRemoveAttributesRemoveAllReplaceAttributesReplaceAllValue 属性SetValueSetElementValueSetAttributeValueXAttributeValue 属性SetValueRemoveLINQElements.RemoveDecendents.Remove

10.5.1 简单的值更新

C 12 in a Nutshell The Definitive Reference.pdf - p552 - C 12 in a Nutshell The Definitive Reference-P552-20240427215207-4o1xolr

SetValue​ 方法和 Value​ 属性用于替换/设置 Element 或 Attribute 的当前值。SetValue​ 方法接受 object ​ 类型的数据,Value​ 属性仅接受 string ​ 类型的数据。

二者赋值时,新值将替换所有子节点。

Tips

SetValue​ 方法内部实际调用的也是 Value​ 属性。

10.5.2 更新子节点(Node)和 Attribute

C 12 in a Nutshell The Definitive Reference.pdf - p552 - C 12 in a Nutshell The Definitive Reference-P552-20240427220005-v9s23fd

上述方法都用于更新当前节点。

10.5.2.1 SetElementValue ​ 方法和 SetAttributeValue ​ 方法

这两个方法将自动实例化 XElement​/XAttribute​ 对象,并作为 元素添加至 当前 元素中,若有同名 Element/Attribute 则进行覆盖:

XElement settings = new XElement ("settings");settings.SetElementValue ("timeout", 30);
settings.SetElementValue ("timeout", 60);
<settings><timeout>30</timeout>
</settings><settings><timeout>60</timeout>
</settings>

10.5.2.2 Add ​ 方法和 AddFirst ​ 方法

Add ​ 方法向内部节点的队尾插入节点; AddFirst ​ 向内部节点的排头插入节点。

Tips

Add​ 方法定义在 XContainer​ 中,AddAfterSelf​ 定义在 XNode​ 中。

10.5.2.3 RemoveNodes ​ 方法、 RemoveAttributes ​ 方法和 RemoveAll ​ 方法

RemoveNodes ​ 方法用于移除持有的全部节点, RemoveAttributes ​ 方法用于移除持有的全部 Attribute。 RemoveAll ​ 可以一次性将二者全部移除。

10.5.2.4 ReplaceXXX ​ 方法

等价于 RemoveXXX​ 方法 + Add​ 方法。

10.5.3 通过父节点更新子节点

C 12 in a Nutshell The Definitive Reference.pdf - p553 - C 12 in a Nutshell The Definitive Reference-P553-20240428123530-3pytrve

上述方法操作的是当前节点的父节点(Parent​),因此父节点不能为 null

AddBeforeSelf ​ 方法和 AddAfterSelf ​ 方法

用于在当前节点的前、后插入其他节点。

Remove ​ 方法

用于在父节点中移除当前节点。

ReplaceWith ​ 方法

用于在父节点中替换当前节点

10.5.3.1 移除节点或属性序列(LINQ)

System.Xml.Linq​ 提供了一系列扩展方法用于从父节点移除元素。后续代码对应的 XML 如下:

<contacts><customer name="Mary" /><customer name="Chris" archived="true" /><supplier name="Susan"><phone archived="true">012345678<!--confidential--></phone></supplier>
</contacts>
Elements().Remove()

从10.4.1.2 检索 elements可知,Elements​ 方法返回的是单层子节点,因此如下代码只会移除当前层的子节点:

contacts.Elements().Where (e => (bool?) e.Attribute ("archived") == true).Remove();
<contacts><customer name="Mary" /><supplier name="Susan"><phone archived="true">012345678<!--confidential--></phone></supplier>
</contacts>
Descendants().Remove()

从10.4.1.4 获取子元素:Descendants 和 DescendantNodes可知,Descendants​ 方法返回所有层次的子节点,因此如下代码会移除任何匹配到的子节点:

contacts.Descendants().Where (e => (bool?) e.Attribute ("archived") == true).Remove();
<contacts><customer name="Mary" /><supplier name="Susan" />
</contacts>
综合使用

以下代码移除了注释为“confidential”的联系人:

contacts.Elements().Where (e => e.DescendantNodes().OfType<XComment>().Any (c => c.Value == "confidential")).Remove();
<contacts><customer name="Mary" /><customer name="Chris" archived="true" />
</contacts>

10.6 使用 Value

10.6.1 设置 Value

如10.5.1 简单的值更新所述:

SetValue​ 方法和 Value​ 属性用于替换/设置 Element 或 Attribute 的当前值。SetValue​ 方法接受 object ​ 类型的数据,Value​ 属性仅接受 string ​ 类型的数据。

Warn

通过 Value​ 设置值时,DataTime​ 要使用 XmlConvert ​ 转化数据。

SetValue​ 和 XElement​/XAttribute​ 的构造器会自动调用 XmlConvert ​ 对数据格式化,保证了数据格式的正确性。

10.6.2 获得 Value

XElement​/XAttribute​ 内部定义了诸多显式转换(如下类型),因此可以直接通过自定义转换获取 Value。

  1. 标准数值类型
  2. string​、bool​、DateTime(Offset)​、TimeSpan​、Guid
  3. 上述值类型的 Nullable<>​ 版本。
XElement e = new XElement ("now", DateTime.Now);
DateTime dt = (DateTime) e;XAttribute a = new XAttribute ("resolution", 1.234);
double res = (double) a;

Suggestion

XML 的元素和 Attribute 不会记录数据的原始类型,上述显式转换可能执行失败。推荐将代码包裹在 try/catch 块中,并捕获 FormatException​ 异常。

10.6.2.1 XML 对象与空运算符

Element​ 方法和 Attribute​ 方法的返回值非常适合转化为 Nullable<> ​ 类型,以如下代码为例,程序不会因为“timeout”不存在而抛出异常:

int  timeout1 = (int)  x.Element ("timeout");
int? timeout2 = (int?) x.Element ("timeout");

配合空合并运算(??​)可以去除最终结果中的可空类型。如下代码在 resolution​ 属性不存在时返回 1.0:

double resolution = (double?) x.Attribute ("resolution") ?? 1.0;

10.6.3 值与混合内容节点

XML 是允许混合内容的,形式如下:

<summary>An XAttribute is <bold>not</bold> an XNode</summary>

要得到上述 X-DOM,需通过 XText ​ 节点:

XElement summary =new XElement ("summary",new XText ("An XAttribute is "),new XElement ("bold", "not"),new XText (" an XNode"));
<!--输出-->
<summary>An XAttribute is <bold>not</bold> an XNode</summary>

其中 summary​ 的 Value​ 如下,它拼接了各个子节点的 Value​:

An XAttribute is not an XNode

Tips

实际传入 string 也是可以的,构造器内部会隐式转为 XText​:

XElement summary =new XElement ("summary","An XAttribute is ",new XElement ("bold", "not")," an XNode");

10.6.4 自动连接 XText​ 节点

XElement​ 中添加简单内容(字符串)时,X-DOM 会将内容附加至现有 XText ​:

// 1 个 XText节点
var e1 = new XElement ("test", "Hello"); e1.Add ("World");
e1.Nodes().Count().Dump ();    // 输出 1
// 1 个 XText节点
var e2 = new XElement ("test", "Hello", "World");
e2.Nodes().Count().Dump ();    // 输出 1

如果显式创建、添加 XText​ 节点,则会得到多个子节点:

// 2 个 XText节点
var e3 = new XElement ("test", new XText ("Hello"), new XText ("World"));
e3.Nodes().Count().Dump ();    // 输出 2

XElement​ 不会连接这两个 XText​ 节点,节点对象的标识均得到保留。即便如此,其 ToString​ 输出的内容仍是拼接的:

<test>HelloWorld</test>

10.7 文档和声明

10.7.1 XDocument

XDocument​ 可接受的内容包括:

XElement XDeclaration XDocumentType XProgressingInstruction XComment
数量 1 1 1 多个 多个
是否必选

其中 XElement​ 作为 X-DOM 的根节点。

XDocument​ 未定义 XDeclaration ​,调用 XDocument.Save​ 时,会自动添加默认的 XML 声明:

// 未定义 XDeclaration
var value = new XDocument (new XElement("test", "data")
);
<!--Save 方法生成的内容:-->
<?xml version="1.0" encoding="utf-16"?>
<test>data</test>

10.7.2 XML 声明(declaration)

10.7.2.1 XML 声明的作用

XDeclaration​ 对象主要用于指导 XML 的序列化进程,影响的内容有二:

  1. 文本编码标准
  2. 声明中的 encoding 和 standalone 如何定义

XDeclaration​ 构造器接受三个参数:version、encoding 和 standalone。

ExtraNotice

XML 写入器(writer)会忽略指定的 version 信息,总是写入“1.0”。

XML 声明中的编码方式必须使用 IETF 编码方式书写,例如“utf-16”。

10.7.2.2 XElement​ 和 XDocument​ 遵循的声明规则

XML 声明用于保证文件被阅读器(reader)正确解析(parse)并理解。XElement​ 和 XDocument​ 都遵循以下声明规则:

  1. 调用 Save​ 方法将内容写入文件,总是 会自动 写入 XML 声明。
  2. 调用 Save​ 方法将内容写入 XmlWriter​ 时,除非 XmlWriter​ 特别指定,否则 写入 XML 声明。
  3. ToString​ 方法 不会 生成 XML​ 声明。

Tips

如果不想让 XmlWriter​ 生成 XML 声明,可以设置 XmlWriterSettings​ 对象的 OmitXmlDeclaration​ 和 ConformanceLevel​ 属性。

另见11.2.0 XmlWriterSettings

Notice

XNode​ 的 WriteTo​ 方法向 XmlWriter​ 写入, 也会 添加 XML 声明。

10.7.2.3 将 XML 声明输出为字符串

若要将 XDocument​ 序列化为 string​,且包含声明,需使用 Save​ 方法:

var doc =new XDocument (new XDeclaration ("1.0", "utf-8", "yes"),new XElement ("test", "data"));var output = new StringBuilder();
var settings = new XmlWriterSettings { Indent = true };using (XmlWriter xw = XmlWriter.Create (output, settings))doc.Save (xw);
<?xml version="1.0" encoding="utf-16" standalone="yes"?>
<test>data</test>

Warn

上述代码即使我们设置编码格式为 utf-8,实际输出的是 utf-16。因为我们输出的对象是 StringBuilder​,编码必然是 utf-16。XmlWriter​ 会自动判断实际输出编码格式,这有效避免了编码格式错误导致的异常。

正因此,为避免输出错误的编码格式,XDocument.ToString​ 不会包含 XDeclaration​ 内容:

var doc =new XDocument (new XDeclaration ("1.0", "utf-8", "yes"),new XElement ("test", "data"));
doc.ToString().Dump();
输出:
<test>data</test>

10.8 名称(Name)和 命名空间(namespace)

XML 的 namespace 用于避免 命名 冲突。例如 nil 可能有多种含义,但在 http://www.w3.org/2001/xmlschema-instance 命名空间下,表示 C# 中的 null。

10.8.1 XML 中的命名空间

XML 中的 namespace 通过 Attribute 声明:

<customer xmlns="http://domain.com/xmlspace"><address><postcode>02138</postcode></address>
</customer>

上述 XML 中,address 和 postcode 属于 http://domain.com/xmlspace​ 命名空间。若不希望子节点继承父节点的命名空间,需显式的令子节点 namespace 为

<customer xmlns="http://domain.com/xmlspace"><address xmlns=""><postcode>02138</postcode></address>
</customer>

当然,我们也可以按照10.8.1.1 前缀(namespace 别名)中的方式,为父节点分配前缀。

Info

关于专门设为空的 namespace,我仅在 XAML 中见过一次这样的应用。见x:XData

10.8.1.1 前缀(namespace 别名)

以如下 XML 为例,一次性完成了两步操作(定义和使用):

  1. xmlns:nut​ 定义了前缀 nut;
  2. nut:customer​ 将前缀分配至 当前 元素。
<nut:customer xmlns:nut="http://domain.com/xmlspace"/>

Notice

拥有前缀的元素,它的子元素 不会 自动使用相同的 namespace。在如下 XML 中,firstname 的 namespace 分别为 nut

<nut:customer xmlns:nut="http://domain.com/xmlspace"><firstname>Joe</firstname>
</nut:customer>
<nut:customer xmlns:nut="http://domain.com/xmlspace"><nut:firstname>Joe</nut:firstname>
</customer>

在 XAML 中我们会同时引入多个 namespace,此时可以通过前缀区分不同 namespace 下的成员:

<Window x:Class="WpfApp1.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:local="clr-namespace:WpfApp1"mc:Ignorable="d"Title="MainWindow" Height="450" Width="800"><Grid></Grid>
</Window>

10.8.1.2 Attribute 与 namespace

XML 中的 Attribute 若要标记 namespace,必须通过前缀。如:

<customer xmlns:nut="OReilly.Nutshell.CSharp" nut:id="123" />

Warn

未用前缀限定的 Attribute 默认使用 的 namespace,它不从父元素继承默认 namespace。

一般来说,Attribute 是元素的本地特征,不需要 namespace。通用 Attribute、元数据 Attribute 例外,譬如之前提到的 W3C 中的 nil 代表了 C# 中的 null:

<customer xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><firstname>Joe</firstname><lastname xsi:nil="true" />
</customer>

10.8.2 在 X-DOM 中指定 namespace

为 X-DOM 添加 namespace 的方法有二:

本地名称前用 大括号 指定

以如下代码为例:

new XElement ("{http://domain.com/xmlspace}customer", new XAttribute("{http://domain.com/xmlspace}id", "123"),"Bloggs"
);
<customer p1:id="123"xmlns:p1="http://domain.com/xmlspace"xmlns="http://domain.com/xmlspace">Bloggs
</customer>
使用 XNamespace

使用方式如下:

XNamespace ns = "http://domain.com/xmlspace";
new XElement(ns + "data",new XAttribute(ns + "id", 456),"123"
);
<data p1:id="123"xmlns:p1="http://domain.com/xmlspace"xmlns="http://domain.com/xmlspace">123
</data>

XNamespace​ 和 XName​ 都定义了与 string ​ 类型的隐式转换;XNamespace​ 还重载了 +​ 运算符,返回类型为 XName ​。

X-DOM 的所有构造器和方法,都使用 XName ​ 类型作为 Element/Attribute 的名称参数,因此我们可以使用 XNamespace + string​ 的方式传入参数。

10.8.3 X-DOM 和默认 namespace

在 X-DOM 中,不存在“继承 namespace”的概念,若想继承父项 namespace,每个成员都需要 显式指定 。而 X-DOM 在读取或输出 XML 时,若父子 namespace 相同,将 自动缺省子项的 namespace

XNamespace ns = "http://domain.com/xmlspace";var data =new XElement (ns + "data",new XElement (ns + "customer", "Bloggs"),new XElement (ns + "purchase", "Bicycle"));
<data xmlns="http://domain.com/xmlspace"><customer>Bloggs</customer><purchase>Bicycle</purchase>
</data>

若父项指定了 namespace,子项未指定,子项的 namespace 会标记为

XNamespace ns = "http://domain.com/xmlspace";var data =new XElement (ns + "data",new XElement ("customer", "Bloggs"),new XElement ("purchase", "Bicycle"));
<data xmlns="http://domain.com/xmlspace"><customer xmlns="">Bloggs</customer><purchase xmlns="">Bicycle</purchase>
</data>

Warn

当成员的 namespace 不为 ,查找元素时传入的 Name 需包含 namespace 信息,例如:

XElement x = data.Element (ns + "customer");   // OK
XElement y = data.Element ("customer");        // null

Suggest

上述指定 namespace 的方式显然很麻烦,我们可以在后期统一指定 namespace:

foreach (XElement e in data.DescendantsAndSelf())if (e.Name.Namespace == "")e.Name = ns + e.Name.LocalName;

10.8.4 添加前缀

namespace 在 XML 中本质是 Attribute ,因此我们可以通过 XAttribute ​ 为成员添加前缀。该 Attribute 的 Name​ 为 XNamespace.Xmlns + 别名 ​,Value​ 为对应的 namespace。以如下 X-DOM 为例:

<data xmlns="http://domain.com/space1"><element xmlns="http://domain.com/space2">value</element><element xmlns="http://domain.com/space2">value</element><element xmlns="http://domain.com/space2">value</element>
</data>

插入前缀方式为:

<ns1:data xmlns:ns1="http://domain.com/space1" xmlns:ns2="http://domain.com/space2"><ns2:element>value</ns2:element><ns2:element>value</ns2:element><ns2:element>value</ns2:element>
</ns1:data>
XNamespace ns1 = "http://domain.com/space1";
XNamespace ns2 = "http://domain.com/space2";var mix =new XElement (ns1 + "data",new XElement (ns2 + "element", "value"),new XElement (ns2 + "element", "value"),new XElement (ns2 + "element", "value"));
// 插入 namespace
mix.SetAttributeValue (XNamespace.Xmlns + "ns1", ns1);
mix.SetAttributeValue (XNamespace.Xmlns + "ns2", ns2);
// 或
mix.Add(new XAttribute(XNamespace.Xmlns + "ns1", ns1));
mix.Add(new XAttribute(XNamespace.Xmlns + "ns2", ns2));

前缀对于 Attribute 同样有效:

XNamespace xsi = "http://www.w3.org/2001/XMLSchema-instance";
var nil = new XAttribute (xsi + "nil", true);var cust =new XElement ("customers",//new XAttribute (XNamespace.Xmlns + "xsi", xsi),new XElement ("customer",new XElement ("lastname", "Bloggs"),new XElement ("dob", nil),new XElement ("credit", nil)));
cust.SetAttributeValue(XNamespace.Xmlns + "xsi", xsi);
<customers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><customer><lastname>Bloggs</lastname><dob xsi:nil="true" /><credit xsi:nil="true" /></customer>
</customers>

Tips

前缀的引入不会影响 X-DOM 内部的实际结构,它仅在输入、输出时才会用到(如序列化和反序列化)。

Error

虽然 namespace 的声明方式和 XAttribute 极其相似,但不可以用如下方式声明:

var mix = new XElement("data", new XAttribute("xmlns", "{http://domain.com/space1}"));

xmlns​ 是 xml 中的特殊关键字,上述代码 mix​ 在使用时将抛出 XmlException​ 异常。

xmlns​ 特性是 XML 中的一个特殊 特性 ,专门用于声明 namespace。

10.9 注解(Annotations)

注解用于存放私有数据,可以附加在任何的 XObject​ 上,X-DOM 将其视为黑盒。有如下方法操作注解对象:

// 添加或移除
public void AddAnnotation (object annotation)
public void RemoveAnnotations<T>()     where T : class
// 检索
public T Annotation<T>()               where T : class
public IEnumerable<T> Annotations<T>() where T : class
public T Annotation<T>()               where T : class
public IEnumerable<T> Annotations<T>() where T : class

注解使用 Type 作为键(必须是引用类型)。用法如下:

XElement e = new XElement ("test");e.AddAnnotation (new CustomData { Message = "Hello" } );
e.Annotations<CustomData>().First().Message.Dump();e.RemoveAnnotations<CustomData>();
e.Annotations<CustomData>().Count().Dump();class CustomData { internal string Message; }

Error

在10.3.3 自动深度克隆(Automatic Deep Cloning)中我们提到,XObject​ 如果有父项,该节点赋值给其他父项时会进行深拷贝。但注解不参与该拷贝,它所在的节点进行拷贝时,新节点的注解为空。

10.10 将数据映射到 X-DOM #delay#​ 用不到,看不懂,剩余内容推迟再看

我们可以使用 LINQ 将数据从数据源映射至 X-DOM 中,只要该数据源支持 LINQ 查询。

例如我们要通过LINQ查询得到形如下方的 XML:

<customers><customer id="1"><name>Sue</name><buys>3</buys></customer>...
</customers>
var customers =new XElement ("customers",new XElement ("customer", new XAttribute ("id", 1),new XElement ("name", "Sue"),new XElement ("buys", 3))
);

在新版 EF 上的操作如下:

var customers =new XElement ("customers",from c in Customers.AsEnumerable()select new XElement ("customer", new XAttribute ("id", c.ID),new XElement ("name", c.Name),new XElement ("buys", c.Purchases.Count)));
// or
var sqlQuery =from c in Customers.AsEnumerable()select new XElement ("customer", new XAttribute ("id", c.ID),new XElement ("name", c.Name),new XElement ("buys", c.Purchases.Count));var customers = new XElement ("customers", sqlQuery);
<customers><customer id="1"><name>Tom</name><buys>3</buys></customer><customer id="2"><name>Harry</name><buys>2</buys></customer>...
</customers>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/861080.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

不同充电协议的 iPhone 无线充电器对比分析 All In One

不同充电协议的 iPhone 无线充电器对比分析 All In One不同充电协议的 iPhone 无线充电器对比分析 All In OneiPhone 12 Pro 使用 7.5W 无线充电器,从 0% ~ 100% 充满需要多少时间MagSafe 充电器 RMB 329https://www.apple.com.cn/shop/product/MWQX3CH/AMagicSafe iPhone 16…

第8章 LINQ 查询

第8章 LINQ 查询 8.2 流式语法 8.2.2 使用 Lambda 表达式 常用运算符 Where() 筛选器 Order() 排序器 Select() 映射器 Take() 获取前 x 个元素 Skip() 跳过前 x 个元素 Reverse() 反转所有元素 First() 获取第一个元素 Last() 获取最后一个元素 ElementAt() 获取第 x 个元素 C…

团队作业4—项目冲刺

这个作业属于哪个课程 计科22级34班这个作业要求在哪里 作业要求这个作业的目标 项目冲刺,进行为期7天的敏捷开发团队成员:姓名 学号张嘉敏 3222004893张嘉乐 3122004544赵衍锴 3122004502唐学鹏 3119005703各个成员在Alpha阶段认领的任务成员 任务唐学鹏 需求分析,调整系统…

又写了一个大一新生的期末作业

#include <stdio.h> #include <string.h> #include <math.h>// 定义学生结构体 struct Student {char id[20]; // 学号char className[20]; // 班级char name[20]; // 姓名int startHour; // 上机开始时间(小时)int startMinute; // 上机…

word中自带插入公式,实现换行和对齐

word自带公式输入很是难用,尤其是不能在公式内回车换行。网上有方法说用(shift+回车)方法,亲测不可用。通过多方查找资料,终于找到如何实现word自带公式的换行和对齐的解决方法。先看看最终效果。首先,我们需要观测到word公式输入的模式,在插入公式操作中,公式工具左上…

INFINI Console 指标采集优化

前言 在 Easysearch / Elasticsearch / Opensearch 管理系统中,对于不同集群不同指标数据进行采集是一个常规任务。但是采集过程中不仅会对采集系统 CPU 和访问性能造成不少压力,也会对 Easysearch / Elasticsearch / Opensearch 集群造成资源消耗,从而影响集群本身的健康运…

费心劳神但又收获满满——软件工程个人总结作业

学期回顾 回顾对于软件工程课程的想象 在学期初时刚上软件工程这门课程时,问我本以为它和以前的专业课一样,以理论为主,并不会占用自己很多时间。但之后这个想法就改变了,我发现软件工程这门课程理论与实践紧密结合,有着各种任务,每个任务背景几乎是之前没有了解过的,需要…

终于结束啦!

一、学期回顾 1.1 回顾你对于软件工程课程的想象 这学期的软工课程,我一开始既充满了期待,也充满了不安。期待是因为这门课程能够让我实在地编写代码并开发项目,而不安同样也来自于此。我的代码能力并不强,我很害怕在这门课的作业中做不出能够通过的东西。不过这些问题都在…

2024-2025-1 20241406刘书含 第十四周学习总结

一、教材学习内容 (一)第十四章模拟、图形学、游戏以及其他应用 《计算机科学概论》第十四章主要探讨了模拟、图形学、游戏以及其他应用。以下是该章节的总结: 模拟: 模拟是计算的一个重要领域,它涉及为复杂系统构建计算机模型,并用模型进行实验以观察结果。模型是对真实…

再战博客园美化(九)

被拉回来赶工了我圆角没了,不过更烦的是里面的forFlow没有背景难受。 圆角改下顺序就好了。好吧不行,用了第一个!important forFlow背景待我去小窝吸一下配色。我测,最丑的一集。但是里面不错。 再去看看两位佬怎么做的 emm,看懂了,有卡片不弄forFlow,里面才弄forFlow。改…

Samba远端共享文件夹映射到本地硬盘

背景: Samba用于将远端Linux服务器的共享文件夹映射到本地磁盘,方便在windows环境下访问,同时可以在Linux环境下编译运行。 操作步骤 Ubantu侧: 1.安装Samba软件包 sudo apt-get install samba samba-common2.修改Samba配置文件,配置共享信息 sudo vim /etc/samba/smb.con…

C++关键字

C++关键字 static 类和结构体外的static被static修饰后只在内部发生链接,其作用域只在其所在的文件中。类和结构体内的staticstruct Entity {static int x, y; 静态成员变量static void Print(){std::cout << x << "," << y << std::endl;}…