一、作业问题
1、问:listbox1.items[i]返回的object是指的字符串吗?
答:items是真正的对象集合,在Add时加的是Person对象p,则里面的item就是Person对象p。
但是,在listbox1显示框中显示的,并不是p,而是p.ToString()。若不加ToString()
会默认使用该方法。若不重写将是"命名空间.类型名"。因此一般在Person类中重写字符串
方法,以符合程序设计方式。
因此,item或items[index]直接作原加入的对象进行使用,必要时进行显式转换。
2、问:退出方法,窗体,程序有哪些方法,有什么区别?
答:(1)This.Close()
此方法是窗体对象的一个成员方法,用于关闭当前窗体。它会触发窗体的FormClosing
和FormClosed事件,并且在关闭前会执行窗体上的一些清理工作。
注意,使用this.Close()只能关闭当前窗体,如果想退出整个应用程序,还需要关闭其
他可能存在的窗体。
(2)Application.Exit()
这个方法是System.Windows.Forms.Application类的一个静态方法,用于退出应用程序。
它会退出应用程序的消息循环,并且在退出前触发ApplicationExit事件。
使用Application.Exit()可以确保关闭所有打开的窗体,并且退出应用程序。
(3)return语句
在事件处理方法中使用return语句可以提前结束该方法的执行。如果事件处理方法是一
个事件的最后一个处理方法,并且没有其他事件侦听该事件,那么使用return语句可以达到
关闭窗体或退出应用程序的效果。但如果有其他事件侦听该事件,return语句只会结束当前
事件处理方法的执行,并不会关闭窗体或退出应用程序。
(4)Environment.Exit(0)
这个方法是System.Environment类的一个静态方法,用于立即终止应用程序并返回指定
的退出代码。它不会触发任何清理工作,也不会触发任何事件。通常这种方式向系统或其它
调用程序传递退出代码,用0表示正常退出,用非零表示异常退出。
一般情况下,建议使用Application.Exit()而不是Environment.Exit()。
(5)this.Hide()
这个方法用于隐藏当前窗体,但窗体实际上并没有关闭。如果需要再次显示窗体,可以
使用this.Show()方法。
3、问:List<T>能保存二维数据吗?例如List<T,Z>。
答:不能.
在C#, List<T> 类型只能保存一维数据,即T类型的单个值。它不支持直接保存二维数
据。如果你需要保存二维数据,可以考虑使用多维数组或者嵌套的集合类型。
(1)多维数组
int[,] twoDimensionalArray = new int[3, 3];twoDimensionalArray[0, 0] = 1;twoDimensionalArray[0, 1] = 2;
(2)嵌套的集合类型
嵌套的集合类型是指将一个集合作为另一个集合的元素来构建更复杂的数据结构。这可
以通过嵌套使用标准集合类或自定义集合类来实现。
(2.1)List<List<T>>
这是一个嵌套的列表,用于存储具有多个层级的元素。例如,List<List<int>> 表示一
个二维整数数组。它适用于表示矩阵或二维表格等结构。
使用场景:当需要使用二维或多维的数据结构时,可以使用嵌套的列表。例如,表示棋
盘上的方格、存储多个学生的分数等。
注意:当访问嵌套列表中的元素时,需要使用两个索引来指定元素的位置。例如,访问
二维数组中的元素可以使用 list[i][j] 的方式。在添加或删除元素时,需要小心对嵌套列
表的索引进行管理,以避免出现越界或逻辑错误。
List<List<int>> list = new List<List<int>>();List<int> list0 = new List<int>() { 0, 1, 2 };List<int> list1 = new List<int>() { 3, 4, 5 };List<int> list2 = new List<int>() { 6, 7, 8 };list.Add(list0);list.Add(list1);list.Add(list2);Console.WriteLine(list[1][1].ToString());//4
一般设计时内层list最好设计为等长,以便循环获取或设置。否则,有时可能越界异常
(2.2)Dictionary<T1, List<T2>>
这是一个将键值对中的值作为列表的字典。它适用于需要按键进行分组的场景。
使用场景:当需要对数据进行分组,并且每个组可以包含多个元素时,可以使用这样的
嵌套字典。
注意:当添加新的元素时,需要先检查字典中是否存在对应的键,如果不存在则创建一
个新的列表,并将元素加入到列表中。当访问嵌套字典中的元素时,先访问键的值(即列
表),然后再通过索引访问列表中的元素。
Dictionary<string, List<int>> grades = new Dictionary<string, List<int>>();List<int> tom = new List<int>() { 88, 89, 90 };List<int> john = new List<int>() { 90, 93, 78 };grades.Add("Tom", tom);grades.Add("John", john);Console.WriteLine(grades["Tom"][1]);//89
上面是存储学生语数成绩的结构。注意唯一性与长度。
(2.3)HashSet<HashSet<T>>
这是一个嵌套的哈希集合,用于存储唯一的元素集合。
使用场景:当需要存储多个唯一的集合,并且每个集合可能包含多个唯一元素时,可以
使用嵌套的哈希集合。
注意:当添加新的元素时,需要先检查嵌套的哈希集合中是否存在对应的集合,如果不
存在则创建一个新的集合,并将元素加入到集合中。当访问嵌套哈希集合中的元素时,先访
问外层集合,然后再通过迭代方式访问内层集合中的元素。
HashSet<HashSet<int>> set = new HashSet<HashSet<int>>();HashSet<int> set0 = new HashSet<int>() { 0, 1, 2 };HashSet<int> set1 = new HashSet<int>() { 3, 4, 5 };HashSet<int> set2 = new HashSet<int>() { 6, 7, 7 };//aset.Add(set0);set.Add(set1);set.Add(set2);Console.WriteLine(set.ElementAt(2).ElementAt(1));//7HashSet<List<int>> hs = new HashSet<List<int>>();List<int> list0 = new List<int>() { 0, 1, 2 };List<int> list1 = new List<int>() { 3, 4, 5 };hs.Add(list0);hs.Add(list1);Console.WriteLine(hs.ElementAt(1)[1]);//4
上面a不会报错,凡是重复的,只会存储一个样本。若set1也为{0,1,2}则上面实际是只有
set0存储了,set1因重复不会报错也不会存储,set1为{2,1,0}也是重复的。若为{0,1,2,3}则
不是重复的。
由于嵌套带来了复杂性,需要注意:
a.嵌套集合的性能:
嵌套集合可能会带来额外的性能开销,特别是在插入和删除元素时。要谨慎使用嵌套集
合,并注意性能考虑。
b.数据一致性:
当修改嵌套集合中的元素时,需要确保数据保持一致。即使在嵌套的集合中进行了更改,
也应该反映在外层集合中。
c.错误处理:
在使用嵌套集合时,需要小心处理索引、边界等可能导致错误的情况,以避免出现异常
或逻辑错误。
所以,嵌套的集合类型可以用于构建复杂的数据结构,但使用它们时需要小心处理,注
意数据一致性和性能问题,并小心处理可能导致错误的情况。
4、问:listbox1.items[listbox1.selectedindex]与listbox1.selecteditem有什么区别?
答:(1)访问方式:
前者是通过索引来访问 Items 集合,需要指定索引位置。
后者直接返回当前选中的项对象,无需指定索引。
(2)可读写性:
前者是可读写的。可以读取或修改 ListBox 中特定索引位置的项。
后者是只读属性,只能读取当前选中的项对象,无法直接修改其引用。
因此在修改listbox1中选中项时,使用前者。
二、Xpath路径表达式
1、XPath路径表达式(类似正则表达式)
XPath 是一种用于在 XML 文档中定位节点的表达式语言。在 C# 的 XmlDocument(或
XElement)中,可以使用 XPath 表达式来选择并提取符合条件的节点。
XPath 表达式由一系列路径和条件组成,用于描述节点的层次结构和属性值。
(1)路径表达式:
/:从根节点开始选择。
例: /AAA,/AAA/BBB,/AAA/BBB/CCC
//:选择任意位置的节点。
例://BBB,//BBB/CCC,//CCC/DDD/EEE
elementName:选择指定名称的元素节点。例:上面的元素名AAA之类
*:选择任意名称的元素节点。
例:/AAA/BBB/*,//CCC/*,/*/*/*/BBB三个层次后面的BBB元素节点
//*所有元素节点,//AA所有AA元素节点,
/AAA/BBB[1]节点AAA下BBB所有元素节点的第一个。
/AAA/BBB[last()]节点AAA下BBB所有元素节点的最后一个。
注意:XML区分大小写,因此xpath也是区别大小写的
(2)条件表达式:
[@attributeName='value']:选择具有指定属性名和属性值的节点。
[@attributeName]:选择具有指定属性名的节点。
注意:加上@表示属性,不加表示元素。
例://@style选择所有属性为style的结点(属性也是结点,又如//@id)
//BBB[@id]所有含有id属性的BBB元素结点(注意重点是元素,上一行重点是属性)
//BBB[@name]所有含有name属性的BBB元素结点(重点是元素)
//BBB[@*]所有含有属性的BBB元素结点(无属性的排除)
//BBB[not(@*)]所有不能有任何属性的BBB元素结点
//BBB[@id='b1']所有且有属性id且值为'b1'的BBB元素结点
//BBB[normalize-space(@name)='bbb']去值两端空格类似trim
//*[count(BBB)=2]选择含有两个BBB子元素的元素(父元素)
//*[count(*)=2]只包含两个子元素的元素结点(父元素)
//*[count(*)=3].....三...
name()函数返回元素的名称;
starts-with(,)函数在该函数的第一个参数字符串是以第二个参数字符开始的情况返
回true;
contains(,)函数当其第一个字符串参数包含有第二个字符串参数时返回true。
//*[name()='BBB']所有名称为BBB的元素,等价=//BBB
//*[starts-with(name(),'B')]所有名称以B开头的元素
//*[contains(name(),'C')]所有名称中含有C的元素
string-length()函数返回字符串的字符数,你应该用<替代<,用>替代>
//*[string-length(name())=3]所有名字长度为3的元素
//*[string-length(name())<3]所有名字长度小于3的元素
//*[string-length(name())>3]所有名字长度大于3的元素
|多个路径进行合并。
//CCC|//BBB 所有CCC与BBB的元素合集
/AAA/EEE|//BBB 所有BBB与AAA下面EEE结点的合集
/AAA/EEE|//DDD/CCC|/AAA|//BBB 可以合并的路径数目没有限制
[index]表示同类节点的第index个(从1开始)
//CCC/p[1] 所有CCC结点后p结点中第一个p结点
注意:结点与节点都表示同样的意思node.
(3)轴轶函数:
ancestor:::选择当前节点的所有祖先节点。总是包含根节点,除非上下文节点就是根
节点。注意,是直系祖先,旁系不算,例如父亲的兄弟不算
parent:::选择当前节点的父节点。
child:::选择当前节点的所有子节点。
descendant:::选择当前节点的所有后代节点。
following-sibling:::包含上下文节点之后的所有兄弟节点.
注意:只是后面跟随的兄弟结点,前面的兄弟结点不包括。
preceding-sibling:::跟上面相对,是该结点之前的所有兄弟结点(不含本身)
following:::包含同一文档按文档顺序位于上下文节点之后的所有节点,除了祖先节点,
属性节点和命名空间节点。
preceding:::与上面相对,前面的兄弟及堂姪节点
descendant-or-self:::自身及其所有后代节点(不包含自身的兄弟结点)
ancestor-or-self:::自身及其所有祖先节点(不包含自身的兄弟结点
self:::自身
轴可以看作是一种在XML文档中沿着路径定位结点的方式,可以根据当前结点的关系找
到其他相关的结点。
可以把轴理解为路标,每一个关键处都有一个路标指明下一步走的方向。轴默认就是
child即当前节点的子节点,默认路标指向下一个子节点,如/AAA即/child::AAA表示根/节
点下面的子节点AAA,//BBB/parent::CCC表示当前BBB节点的父节点BBB。
child轴(axis)包含上下文节点的子元素,作为默认的轴,可以忽略不写。
/AAA 等价于/child::AAA
/descendant::*选择文档根元素的所有后代,即所有元素被选择
//DDD/parent::* 所有DDD元素的父节点合集(从DDD转向父节点)
/AAA/BBB/DDD/CCC/EEE/ancestor::* 所有祖先结点(从EEE转向祖先,不含EEE,不含
祖先中的兄弟元素)直系祖先
//fff/ancestor::* 所有FFF元素的祖先节点
/aaa/bbb/following-sibling::* 指定bbb后面的所有兄弟节点(不含bbb,跟随同级)
//ccc/following-sibling::* 所有ccc后面的所有兄弟结点(不含ccc)
//aaa/xxx/preceding-sibling::* xxx前面所有的兄弟结点
//ccc/preceding-sibling::*
//xxx/following::* xxx后面的所有兄弟节点及子节点(不含本身xxx)
//AAA/XXX/preceding::* XXX前面所有兄弟及其子节点
//aaa/xxx/descendant-or-self::* xxx自身及其所有后代节点
//fff/descendant-or-selft::* 所有fff结点及其所有后代节点
//zzz/yyy/xxx/sss/ancestor-or-self::*
//ggg/ancestor::*
//xxx/self::*
//GGG/ancestor::*|//GGG/descendant::*|GGG/following::*|//GGG/preceding::*
|//GGG/self::*
(4)运算函数
div运算符做浮点除法运算
mod运算符做求余运算
floor函数返回不大于参数的最大整数(趋近于正无穷)
ceiling返回不小于参数的最小整数(趋近于负无穷)
//BBB[position()mod 2 =0]选择偶数位置的BBB元素
注意:
在XPath中,斜杠(/)表示文档根节点,而不是文档本身。斜杠后面的节点表示文档根
节点的直接子节点。
如果你使用XPath表达式 "/aaa",它表示选择文档根节点下名为 "aaa" 的直接子节点。
这个节点可以是元素、属性、命名空间等。
请注意,XPath中的斜杠表示层级关系,而不是路径的开始或结束。因此,斜杠前面没有
节点时,它表示文档根节点。
如果你想选择文档本身,可以使用点(.)表示当前节点。例如,".//aaa" 表示选择文
档中所有名为 "aaa" 的节点,而不仅仅是文档根节点下的直接子节点。
问:xpath文档里常说"包含上下文",这是什么意思?
答:上下文是指在执行XPath表达式时所处的环境或位置。上下文通常是一个节点集合,可
以是整个文档、某个元素的子节点集合,或者是其他节点的集合。
在XPath表达式中,使用.表示当前节点,使用..表示当前节点的父节点。这些符号是
相对于上下文节点进行定位的。
2、XmlDocument有哪些方法来操作xpath?
XmlDocument中,可以使用SelectNodes()方法和SelectSingleNode()方法来执行XPath
查询并获取符合条件的节点。
XmlDocument xmlDoc = new XmlDocument();xmlDoc.Load("example.xml");XmlNodeList nodeList = xmlDoc.SelectNodes("//name");// 使用 XPath 选择所有 name 元素节点foreach (XmlNode node in nodeList){Console.WriteLine(node.InnerText);}// 使用 XPath 选择具有 id 属性值为 "I8" 的 element 元素节点XmlNode elementNode = xmlDoc.SelectSingleNode("//element[@id='I8']");if (elementNode != null){string name = elementNode.SelectSingleNode("name").InnerText;Console.WriteLine("Name: " + name);}
(1)SelectNodes方法:
该方法返回一个XmlNodeList对象,包含满足XPath表达式的所有节点。可以使用该方法
来定位多个元素。例如,使用XPath表达式"//book"可以定位所有的书籍元素。
(2)SelectSingleNode方法:
该方法返回一个XmlNode对象,包含满足XPath表达式的第一个节点。可以使用该方法来
定位单个元素。例如,使用XPath表达式"title"可以定位书籍元素下的标题元素。
例:xml文件:
<?xml version="1.0" encoding="utf-8" ?><school><student id="001" position="1"><name>许万里</name><age>18</age></student><student id="002" position="2"><name>刘万仁</name><age>19</age></student><student id="003" position="1"><name>晨微</name><age>20</age></student><student id="004" position="1"><name>郎吉祥</name><age>21</age></student></school>
利用xmldocument使用xpath:
XmlDocument xmldoc = new XmlDocument();xmldoc.Load("student.xml");XmlNodeList xmlns = xmldoc.SelectNodes("//student[@id][@position='1']");foreach (XmlNode n in xmlns){Console.WriteLine($"{n.Name},{n.Attributes["id"].Value}");}
3、XDocumetn中使用xpath的有哪些?
XDocument类提供了三个主要的方法来使用XPath表达式进行元素选择和查询:
XPathSelectElement方法:
使用XPath表达式选择单个元素。它返回满足XPath条件的第一个元素,如果没有匹配的
元素,则返回null。
XPathSelectElements方法:
使用XPath表达式选择多个元素。它返回一个IEnumerable<XElement>对象,其中包含满
足XPath条件的所有元素。
XPathEvaluate方法:
使用XPath表达式对文档进行求值。它返回一个object类型的结果,根据XPath表达式的
求值结果的类型可能会有所不同。
这些方法允许您在XDocument中使用XPath表达式来选择元素或执行XPath查询。请注意,
在使用这些方法之前,需要导入System.Xml.Linq和System.Xml.XPath命名空间。linq可以
进一步提供谓词、筛选、修改XML文档等功能。
例xml文件:
<library><book><title>Book 1</title><author>Author 1</author></book><book><title>Book 2</title><author>Author 2</author></book></library>
使用xpath操作
XDocument xdoc = XDocument.Load("book.xml");IEnumerable<XElement> xeles = xdoc.XPathSelectElements("//title");foreach (XElement n in xeles){Console.WriteLine($"{n.Name},{n.Value}");}XElement xele = xdoc.XPathSelectElement("/library/book");Console.WriteLine(xele.Element("title").Value);IEnumerable<object> s = (IEnumerable<object>)xdoc.XPathEvaluate("/library/book/author");//aforeach (XElement o in s){Console.WriteLine(o.Value);}
上面a处使用xpathevaluate()是获取值,根据已知的xml知道获取到的是两个结点,也就是返
回的是IEnumerable<XElement>类型,若xpath表达式为/library/book[1]/author,则指明是
返回一个结点的值,即"Author 1",这显示默认的object是一个string类型,那么后继的肯定
就应用string来处理。
实际中若的确无法确定时,可以根据is来确定它的类型:
var result = xdoc.XPathEvaluate("/library/book/author");if (result is IEnumerable<XElement> elements){var author = elements.FirstOrDefault()?.Value;Console.WriteLine(author);}else if (result is string str){Console.WriteLine(str);}else{Console.WriteLine("Unknown type");}
xpathevaluate()也可以用于属性,例如/library/book/@id,但要注意类型的转换与筛选。
XDocument xdoc = XDocument.Load("book.xml");//不能直接IEnumerable<XAttribute>IEnumerable<object> result = (IEnumerable<object>)xdoc.XPathEvaluate("/library/book/@id");foreach (XAttribute attribute in result)//a{Console.WriteLine(attribute.Value);}
XPathEvaluate若有多个结果需要枚举,只能使用IEnumerable<object>来接收结果。这
是因为XPath表达式可能匹配多种类型的节点,例如元素、属性、注释等,所以返回的结果
类型是多样的。虽然我们可以使用强制类型转换来尝试将结果转换为特定类型,但这种方式
并不总是可行的,因为我们无法预知XPath表达式最终会匹配到哪些类型的节点。
最后在a处,枚举时,再逐个隐式转换成对应结点。
问:/library/book/@id与/library/book[@id]有什么区别?
答:两者是两个不同的XPath表达式,区别在于匹配的节点类型和结果。
/library/book/@id:
这个XPath表达式匹配book元素下的id属性。它返回的是book元素的id属性的集合,
每个属性都是一个XAttribute对象。例如,如果有两个book元素,每个元素都有一
个id属性,那么这个表达式将返回两个XAttribute对象。
/library/book[@id]:
这个XPath表达式匹配具有id属性的book元素。它返回的是具有id属性的book元素的
集合,每个元素都是一个XElement对象。例如,如果有两个book元素,其中一个具
有id属性,另一个没有id属性,那么这个表达式将返回一个XElement对象。
/library/book/@id返回的是id属性,而/library/book[@id]返回的是具有id属性的book
元素。这两个表达式的结果类型不同,需要根据具体的需求选择使用哪个表达式。
三、对XML的增删查改
1、Xml增删查改+登录
思想:每次变化都是保存xml,然后重新加载xml,保持xml一直更新。
图01
2、以xmldocument操作
private void Form1_Load(object sender, EventArgs e){LoadToListView();}private void LoadToListView(){listView1.Items.Clear();XmlDocument xmldoc = new XmlDocument();xmldoc.Load("UserData.xml");XmlNodeList ns = xmldoc.SelectNodes("//user");foreach (XmlNode n in ns){ListViewItem item = new ListViewItem(n.Attributes["id"].InnerText);item.SubItems.Add(n.SelectSingleNode("name").InnerText);item.SubItems.Add(n.SelectSingleNode("password").InnerText);listView1.Items.Add(item);}button2.Enabled = false;}private void button1_Click(object sender, EventArgs e)//增加{string id = txtAddID.Text.Trim();if (id == "") return;XmlDocument xmldoc = new XmlDocument();xmldoc.Load("UserData.xml");XmlNodeList ns = xmldoc.SelectNodes("/Users/user[@id='" + id + "']");if (ns.Count > 0){MessageBox.Show("已经有该ID");return;}XmlElement root = xmldoc.DocumentElement;XmlElement user = xmldoc.CreateElement("user");user.SetAttribute("id", id);XmlElement name = xmldoc.CreateElement("name");name.InnerText = txtAddUser.Text.Trim();XmlElement pwd = xmldoc.CreateElement("password");pwd.InnerText = txtAddPwd.Text.Trim();user.AppendChild(name);user.AppendChild(pwd);root.AppendChild(user);xmldoc.Save("UserData.xml");LoadToListView();txtAddID.Text = "";txtAddUser.Text = "";txtAddPwd.Text = "";}private void button2_Click(object sender, EventArgs e)//修改{XmlDocument xmldoc = new XmlDocument();xmldoc.Load("UserData.xml");string id = txtEditID.Text.Trim();XmlNode n = xmldoc.SelectSingleNode("/Users/user[@id='" + id + "']");n.SelectSingleNode("name").InnerText = txtEditUser.Text.Trim();n.SelectSingleNode("password").InnerText = txtEditPwd.Text.Trim();xmldoc.Save("UserData.xml");LoadToListView();txtEditID.Text = "";txtEditUser.Text = "";txtEditPwd.Text = "";}private void listView1_SelectedIndexChanged(object sender, EventArgs e){if (listView1.SelectedItems.Count == 0) return;button2.Enabled = true;ListViewItem lvitem = listView1.SelectedItems[0];string id = lvitem.Text;txtEditID.Text = id;txtEditUser.Text = lvitem.SubItems[1].Text;txtEditPwd.Text = lvitem.SubItems[2].Text;}private void button4_Click(object sender, EventArgs e)//删除{if (listView1.SelectedItems.Count == 0) return;string id = listView1.SelectedItems[0].Text;XmlDocument xmldoc = new XmlDocument();xmldoc.Load("UserData.xml");XmlNode n = xmldoc.SelectSingleNode("/Users/user[@id='" + id + "']");xmldoc.DocumentElement.RemoveChild(n);xmldoc.Save("UserData.xml");LoadToListView();txtEditID.Text = "";txtEditUser.Text = "";txtEditPwd.Text = "";}private void button3_Click(object sender, EventArgs e)//登录{string use = txtLogUser.Text.Trim();string pwd = txtLogPwd.Text.Trim();XmlDocument xmldoc = new XmlDocument();xmldoc.Load("UserData.xml");XmlNodeList ns = xmldoc.SelectNodes("//name[.='" + use + "']");if (ns.Count == 0){MessageBox.Show("用户名错误");return;}else{bool log = false;foreach (XmlNode n in ns){if (n.NextSibling.InnerText == pwd){MessageBox.Show("登录成功");log = true;txtLogUser.Text = "";txtLogPwd.Text = "";break;}}if (!log){MessageBox.Show("密码错误");}}}
3、以xdocument进行操作
private void LoadToListView(){listView1.Items.Clear();XDocument xdoc = XDocument.Load("UserData.xml");var eles = xdoc.XPathSelectElements("//user");foreach (XElement ele in eles){ListViewItem item = new ListViewItem(ele.Attribute("id").Value);item.SubItems.Add(ele.Element("name").Value);item.SubItems.Add(ele.Element("password").Value);listView1.Items.Add(item);}button2.Enabled = false;}private void button1_Click(object sender, EventArgs e)//增加{string id = txtAddID.Text.Trim();if (id == "") return;XDocument xdoc = XDocument.Load("UserData.xml");var eles = xdoc.XPathSelectElements("/Users/user[@id='" + id + "']");if (eles.Count() > 0){MessageBox.Show("已经有该ID"); return;}//XElement ele = xdoc.Root.Elements("user").Single(x => x.Attribute("id").Value == id);//if (ele != null)//{// MessageBox.Show("已经有该ID"); return;//}XElement user = new XElement("user");user.SetAttributeValue("id", id);user.SetElementValue("name", txtAddUser.Text.Trim());user.SetElementValue("password", txtAddPwd.Text.Trim());xdoc.Root.Add(user);xdoc.Save("UserData.xml");LoadToListView();txtAddID.Text = "";txtAddUser.Text = "";txtAddPwd.Text = "";}private void button2_Click(object sender, EventArgs e)//修改{XDocument xdoc = XDocument.Load("UserData.xml");string id = txtEditID.Text.Trim();XElement user = xdoc.XPathSelectElement("/Users/user[@id='" + id + "']");user.SetElementValue("name", txtEditUser.Text.Trim());user.SetElementValue("password", txtEditPwd.Text.Trim());xdoc.Save("UserData.xml");LoadToListView();txtEditID.Text = "";txtEditUser.Text = "";txtEditPwd.Text = "";}private void listView1_SelectedIndexChanged(object sender, EventArgs e){if (listView1.SelectedItems.Count == 0) return;button2.Enabled = true;ListViewItem lvitem = listView1.SelectedItems[0];string id = lvitem.Text;txtEditID.Text = id;txtEditUser.Text = lvitem.SubItems[1].Text;txtEditPwd.Text = lvitem.SubItems[2].Text;}private void button4_Click(object sender, EventArgs e)//删除{if (listView1.SelectedItems.Count == 0) return;string id = listView1.SelectedItems[0].Text;XDocument xdoc = XDocument.Load("UserData.xml");XElement user = xdoc.XPathSelectElement("/Users/user[@id='" + id + "']");user.Remove();xdoc.Save("UserData.xml");LoadToListView();txtEditID.Text = "";txtEditUser.Text = "";txtEditPwd.Text = "";}private void button3_Click(object sender, EventArgs e)//登录{string user = txtLogUser.Text.Trim();string pwd = txtLogPwd.Text.Trim();XDocument xdoc = XDocument.Load("UserData.xml");var eles = xdoc.XPathSelectElements("//name[.='" + user + "']");if (eles.Count() == 0){MessageBox.Show("用户名错误");return;}else{bool log = false;foreach (XElement ele in eles){var bros = ele.ElementsAfterSelf("password");foreach (XElement bro in bros){if (bro.Value == txtLogPwd.Text.Trim()){MessageBox.Show("登录成功!");log = true;txtLogUser.Text = "";txtLogPwd.Text = "";break;//只能退出当前循环,需要在外层循环中根据log再退出}}}if (!log){MessageBox.Show("密码错误");}}}
推荐使用xdocument,一是本身比较简洁,二是里面可以使用lambdar表达式
问:xdoc.Root.Elements("user")与xdoc.Elements("user")有什么区别?
答:区别在于它们选择元素的起始位置不同.elements()只会对直接子元素搜索,不会对后
代所有元素搜索。
因此前面搜索的是根元素下面的子元素,而后者搜索的是仅是根元素。
4、问:本地名称(LocalName)和可选的命名空间(Namespace)有什么区别?
答:在 XML 中,每个元素和属性都有一个名称,名称由本地名称和命名空间组成。
(1)本地名称(LocalName):
表示元素或属性的名称的本地部分,即不包含命名空间前缀的部分。本地名称是元素或
属性在其所在命名空间中的唯一标识符。例如,在 <book>元素中,本地名称是 book。
(2)命名空间(Namespace):
用于标识和区分 XML 元素或属性的命名空间。命名空间提供了一种在不同 XML 文档之
间进行唯一标识的方式。它通常以 URI(Uniform Resource Identifier)的形式表示。命
名空间可以与元素或属性的本地名称结合,形成该元素或属性的完整名称。例如,
在 <ns:book>元素中,ns 是命名空间前缀,表示该元素属于以 ns 前缀定义的命名空间。
在 XML 中,命名空间的目的是避免元素和属性名称的冲突。它允许不同来源的 XML
数据在同一个文档中存在并区分开来。区别:
<root><book xmlns="http://example.com/books"><title>The Adventures of Tom Sawyer</title><author>Mark Twain</author></book><ns:book xmlns:ns="http://example.com/novels"><ns:title>The Catcher in the Rye</ns:title><ns:author>J.D. Salinger</ns:author></ns:book></root>
上面有两个 <book>元素,但它们位于不同的命名空间中。在第一个 <book>元素中,
内部元素的本地名称是title 和author,并且默认命名空间为http://example.com/books。
而在第二个 <ns:book>元素中,内部元素的本地名称是 title 和 author,并且命名空间
为 http://example.com/novels。
通过这种方式,两个 <book>元素及其内部元素虽然具有相同的本地名称,但由于位于
不同的命名空间中,可以被区分开来。因此,本地名称和命名空间一起描述了 XML 元素或
属性的唯一标识符,并在避免冲突的同时提供了更丰富的语义和上下文信息。
问:xml使用命名空间的原因?
答:使用命名空间的主要目的之一是确保在处理来自不同来源(如不同网站)的 XML 数据
时,能够将它们正确区分开来并避免名称冲突。
XML 是一种通用的数据交换格式,用于在不同系统和平台之间传递数据。不同的组织、
应用程序或网站可能定义了具有相同名称的元素和属性,但它们在语义上可能具有不同的
含义。
通过使用命名空间,可以将相同的元素或属性名称与特定的命名空间关联起来,从而
将其作为唯一标识符。这样,在处理包含来自不同命名空间的 XML 数据的文档时,可以确
保元素和属性的语义正确并且不会混淆。
例如,假设有两个网站 A 和 B,它们都使用了名称为 “book” 的元素来描述书籍的信
息。如果没有命名空间,当将来自网站 A 和网站 B 的 XML 数据合并到同一个文档中时,
就无法区分它们,并且会出现名称冲突。
通过为网站 A 定义命名空间 http://www.example.com/siteA,为网站 B 定义命名空
间 http://www.example.com/siteB,每个网站的 <book> 元素就能够在合并的文档中保持
其唯一性和上下文。这样,在处理合并文档时,可以根据命名空间将元素正确区分为来自
网站 A 还是网站 B,并根据其命名空间进行相应的处理。
因此,使用命名空间可以确保在处理来自不同来源的 XML 数据时,元素和属性能够正
确识别和解释,并避免名称冲突和歧义。
四、隐式类型转换与显式类型转换
1、隐匿类型转换
隐式类型转换是指在编译时自动将一种类型转换为另一种类型,而无需显式地进行类型
转换操作。这种类型转换是安全的,因为它只允许从一种类型到另一种类型的转换,而不会
丢失数据或引发异常。隐式类型转换通常发生在以下情况下:
(1)当将一个小范围的整数类型赋值给一个大范围的整数类型时,如将int类型赋值给
long类型。
(2)当将一个派生类的实例赋值给一个基类类型时,如将一个派生类的对象赋值给一
个基类的引用变量。
(3)当将一个浮点数类型赋值给一个更高精度的浮点数类型时,如将float类型赋值给
double类型。
int num1 = 10;long num2 = num1; // 隐式将int类型转换为long类型string str = "123";int num3 = Convert.ToInt32(str); // 显式将string类型转换为int类型float f = 3.14f;double d = f; // 隐式将float类型转换为double类型Animal animal = new Dog();
注意:隐式类型转换只能在类型之间存在继承关系或者存在内置的转换规则时才能进行。
如果类型之间没有直接的转换关系,那么就需要使用显式类型转换来进行转换操作。
2、implicit隐式类型转换
使用implicit关键字定义的隐式转换操作符允许在不进行显式转换的情况下,将一个类
型隐式转换为另一个类型。这种转换是自动进行的,不需要显式调用转换方法。
public class Distance{public double Meters { get; }public Distance(double meters){Meters = meters;}public static implicit operator double(Distance distance){return distance.Meters;}}Distance distance = new Distance(10.5);// 定义一个Distance类型的变量double meters = distance;// 将Distance类型隐式转换为double类型Console.WriteLine(meters); // 输出: 10.5
上面定义了一个Distance类,其中包含一个Meters属性和一个隐式转换操作符。该隐式
转换操作符允许将Distance类型隐式转换为double类型。
注意:
(1)隐式转换操作符必须定义为公共静态方法,并且返回目标类型的值。
(2)隐式转换操作符只能定义在类或结构体中。
(3)隐式转换操作符只能定义一个参数,该参数是要转换的源类型。
(4)隐式转换操作符必须是从源类型到目标类型的转换,不能反过来定义。
(5)隐式转换操作符应该是安全的,不应该导致数据丢失或引发异常。
上面5个注意的原因:
(1)因为隐式转换是一种自动进行的转换,不需要显式调用转换方法。将隐式转换操作
符定义为公共静态方法可以确保在需要进行隐式转换时,可以访问和调用该方法。同时,让
右端类型与左端类型一致,就必须返回目标类型。
(2)隐式转换是一种类型之间的转换关系,需要在类型的定义中进行定义和实现。只有
类或结构体才能定义方法,因此隐式转换操作符只能定义在类或结构体中。
(3)隐式转换是从源类型到目标类型的转换。在进行隐式转换时,只需要提供源类型的
值,就可以自动进行转换。因此,隐式转换操作符只需要一个参数,即要转换的源类型。
(4)隐式转换是一种自动进行的转换,不需要显式调用转换方法。在进行隐式转换时,
编译器会自动查找匹配的隐式转换操作符进行转换。如果允许反过来定义隐式转换操作符,
可能会导致歧义和不确定性。
(5)为了确保转换的正确性和可靠性。隐式转换是一种自动进行的转换,如果转换不是
安全的,可能会导致数据丢失或引发异常。因此,为了保证程序的正确性和可靠性,隐式
转换操作符应该是安全的。如果需要进行不安全的转换,应该使用显式转换操作符来明确
指定转换。
3、Explicit显式类型转换
显式类型转换(explicit)主要应用的场景是防止不必要的隐式转换。在某些情况下,
隐式转换可能会导致代码可读性降低或意外类型转换,因此需要使用显式转换来明确表达
转换的意图。例如,在类的构造函数中,为了避免意外的隐式转换,最好尽可能多地使用
显式转换。
explicit是一个关键字,用于定义显式转换操作符。显式转换操作符允许程序员在不
同类型之间进行显式转换。
使用explicit关键字定义的显式转换操作符必须是公共静态方法,并且必须返回目标
类型。它们通常被用于将一个较大的类型转换为一个较小的类型,以避免数据丢失。
public class Distance{private double meters;public Distance(double meters){this.meters = meters;}public static explicit operator int(Distance distance){return (int)distance.meters;}}class Program{static void Main(string[] args){Distance distance = new Distance(1000);int meters = (int)distance; // 使用显式转换操作符将Distance类型转换为int类型Console.WriteLine(meters); // 输出:1000}}
上面Distance类定义了一个显式转换操作符,将Distance类型转换为int类型。在
Main方法中,我们创建了一个Distance对象,并使用显式转换操作符将其转换为int类型,
并将结果赋给meters变量。注意:
(1)explicit关键字只能用于定义显式转换操作符,而不能用于隐式转换操作符。
(2)显式转换操作符必须定义在类或结构体中。
(3)显式转换操作符必须是公共的静态方法。
(4)显式转换操作符必须返回目标类型。
(5)显式转换操作符通常用于将一个较大的类型转换为一个较小的类型,以避免数据
丢失。因此,在进行显式转换时,需要注意可能会发生数据截断的情况。
下面是华氏温度与温度的自定义显式转换定义:
public class Temperature{private double celsius;public Temperature(double celsius){this.celsius = celsius;}public static explicit operator Fahrenheit(Temperature temperature){double fahrenheit = (temperature.celsius * 9 / 5) + 32;return new Fahrenheit(fahrenheit);}}public class Fahrenheit{private double temperature;public Fahrenheit(double temperature){this.temperature = temperature;}public override string ToString(){return $"{temperature}°F";}}class Program{static void Main(string[] args){Temperature temperature = new Temperature(25);Fahrenheit fahrenheit = (Fahrenheit)temperature; // 使用显式转换操作符将Temperature类型转换为Fahrenheit类型Console.WriteLine(fahrenheit); // 输出:77°F}}
注意:
(1)在显式转换操作符中,我们可以根据需要执行任意的转换逻辑。
(2)显式转换操作符可以在任意两个自定义类型之间进行定义,只要转换逻辑是可行的。
(3)在进行显式转换时,需要确保目标类型能够接受转换后的值,否则可能会引发异常
或产生不可预期的结果。
(4)显式转换操作符可以用于自定义类型的转换,以便在不同的类型之间进行数据转换
和操作。
4、Operator运算符重载
可以通过运算符重载来重定义已经存在的运算符的行为。这意味着我们可以为自定义类
型定义与标准运算符(如算术运算符、关系运算符等)相关的操作。
例如,我们可以重载加法运算符(+)以实现自定义类型的相加操作,或者重载相等运
算符(==)以实现自定义类型的相等性比较。
运算符重载的目的是为了提供一种直观和自然的方式来操作自定义类型对象,使其行为
与内置类型对象类似。这样,我们可以使用操作符来操作自定义类型,使代码更具可读性和
简洁性。
注意,在进行运算符重载时,我们只能重载已经存在的运算符,无法创建新的运算符。
这是由C#语言规范所决定的。可以通过运算符重载来自定义行为的常见运算符:
一元运算符:+, -, !, ++, –
算术运算符:+, -, *, /, %, ++, –
关系运算符:==, !=, >, <, >=, <=
逻辑运算符:&&, ||, !
位运算符:&, |, ^, ~, <<, >>
索引器:[]
通过定义相应的重载方法,我们可以修改这些运算符在自定义类型上的操作行为。
注意下面写法:public static 返回类型 operator 符号(参数)
(1)算术运算符重载:
我们可以重载算术运算符,如加法运算符(+)、减法运算符(-)、乘法运算符(*)
和除法运算符(/)。例如,假设我们有一个自定义的Vector类,我们可以重载加法运算
符来执行向量的加法操作。
private static void Main(){Vector v1 = new Vector() { X = 1, Y = 2 };Vector v2 = new Vector() { X = 4, Y = 3 };Vector v3 = v1 + v2;Console.WriteLine(v3.X);Console.ReadKey();}public class Vector{public int X { get; set; }public int Y { get; set; }public static Vector operator +(Vector v1, Vector v2){return new Vector() { X = v1.X + v2.X, Y = v1.Y + v2.Y };}}
(2)比较运算符重载:
我们可以重载比较运算符,如相等运算符(==)、不等运算符(!=)、大于运算符
(>)和小于运算符(<)。例如,假设我们有一个自定义的Person类,我们可以重载相
等运算符以比较两个人的年龄是否相等。
private static void Main(){Person p1 = new Person() { Name = "Tom", Age = 18 };Person p2 = new Person() { Name = "John", Age = 18 };Console.WriteLine(p1 == p2);Console.WriteLine(p1.Equals(p2));Console.ReadKey();}public class Person{public string Name { get; set; }public int Age { get; set; }public static bool operator ==(Person p1, Person p2){return p1.Age == p2.Age;}public static bool operator !=(Person p1, Person p2){return p1.Age != p2.Age;}}
问题一:
重载==后必须同时重载!=。
问题二:
重载==后,equals仍然是按原来进行引用相等性比较(即比较两个对象是否指向相同
的内存地址),而不是基于你重载的运算符进行的值相等性比较。
如果你希望Object.Equals(Person p1, Person p2)按照你重载的运算符来进行相等
性比较,你可以通过重写Object.Equals(object o)方法来实现。事实上,当前面的重载
写上时,vs2022就会提醒
“重载==与!=后,提示Person定义运算符==或!=,但不重写object.equals(object o)”
也就是说equals的结果可能与定义==的结果不一样。若更改为一样,则在类内重写:
public override bool Equals(object o){if(o== null || GetType()==o.GetType()) return false;Person obj=(Person)o;return Name== obj.Name && Age==obj.Age;}
operator重载时应该注意:
运算符重载的参数数量和类型是根据相应的运算符来确定的。例如,对于二元运算符
(如加法运算符),重载方法的参数包括两个操作数,而对于一元运算符(如递增运算
符),重载方法的参数只包括一个操作数。
运算符重载方法必须被声明为公共(public)和静态(static)。这是因为运算符重
载方法与类型本身而不是特定的实例相关联,所以它们必须是静态的。同时,为了允许外
部代码直接使用运算符,重载方法也必须声明为公共的。
在使用运算符重载时,需要注意以下几点:
(1)编写清晰和可读性强的代码:运算符重载是为了让代码更易读和理解,所以应该
遵循良好的命名习惯,并确保代码对其他开发人员易于理解。
(2)保持一致性:运算符重载的行为应与内置运算符的行为保持一致。例如,在重载
加法运算符时,它应该执行通常意义上的加法操作,而不是执行其他奇怪或不相关的操作。
(3)谨慎地选择重载运算符:应该谨慎选择需要重载的运算符,以避免引起混淆或意
外的行为。不应该过度使用运算符重载,只在有必要的情况下使用。
(4)重载运算符的对称性:运算符重载应该具有对称性。例如,如果重载了相等运算
符(==),则还应该同时重载不等运算符(!=),并且它们的行为应该是对称的。
(5)考虑运算符的预期用途:在重载运算符时,应该考虑到它们的预期用法和语义。
这样,其他人员在使用重载的运算符时可以有一个直观的期望,并且代码会更具可读性。
五、序列化
1、XML序列化
下面对一个List<Person>进行序列化。
private static void Main(){List<Person> list = new List<Person>(){new Person(){Name="Tom",Age=18},new Person(){Name="John",Age=19},new Person(){Name="Jack",Age=20}};XmlSerializer ser = new XmlSerializer(typeof(List<Person>));//typeof(Person)将报错using (FileStream fs = File.OpenWrite(@"D:\22.xml")){ser.Serialize(fs, list);}Console.WriteLine(File.ReadAllText(@"D:\22.xml"));Console.ReadKey();}public class Person{public int Age { get; set; }public string Name { get; set; }}
如果不需要写到文件中,则只需要用内存流即可:
internal class Program//a{private static void Main(){List<Person> list = new List<Person>(){new Person(){Name="Tom",Age=18},new Person(){Name="John",Age=19},new Person(){Name="Jack",Age=20}};using (MemoryStream ms = new MemoryStream()){XmlSerializer xml = new XmlSerializer(typeof(List<Person>));xml.Serialize(ms, list);ms.Position = 0;//b关键,否则游标在末尾无法读出内容.每次读完游标在末尾。string s = new StreamReader(ms).ReadToEnd();//XDocument xdoc = XDocument.Load(ms);//string s = xdoc.ToString(); //c不会包括声明部分,需要手动添加//若用xmldocument就是全部,但由于是outerxml,不会是标准的格式显示//XmlDocument xmldoc = new XmlDocument();//xmldoc.Load(ms);//s = xmldoc.OuterXml;//dConsole.WriteLine(s); // 输出序列化后的字符串}Console.ReadKey();}}public class Person{public int Age { get; set; }public string Name { get; set; }}
用using可以自行释放ms,using块内定义的超出块外,GC会自动回收。
ms也可以不用管,GC也可以自动回收。也可以手动设置:
ms.SetLength(0);//设置内存流长度为0,也即变相释放了。
或者,直接创建新的对象来清空内存流:
ms=new MemoryStream();
总之,MemoryStream 会自动管理内存的释放,但在特定情况下,手动管理内存流可以
帮助避免潜在的内存问题。
另外:
xml需要公共方法,person类必须public,它的外界也必须是public。例如把person纳
入Program时,Program也必须为public(a处)。
每一次内存流读后游标在末尾,若想再次读取,须置开头(b处)
xdoc与xmldoc取得xml内容有差异。xdoc取的是除声明外的xml。xmldoc虽然取得的是全
部xml,但再没有回车table等格式方便眼睛识别格式。
2、问:为什么xml序列化时不用加[serializable],而二进制序列必须加?
答:对于二进制序列化,需要使用[serializable]特性标记类,以便将类的定义信息(如
字段、属性等)包含在序列化流中。如果不标记为[serializable],则无法进行二进制序
列化。这是因为二进制序列化需要将类的定义信息作为序列化的开端,然后按照对象的实
际状态进行序列化。
而对于XML序列化,不需要显式地标记类为[serializable]。这是因为XML序列化使用
的是公共语言规范(Common Language Specification,CLS),该规范要求所有公共类都
必须是可序列化的。因此,在默认情况下,所有公共类都可以进行XML序列化。
注意,即使不需要显式地标记类为[serializable],但如果类中包含非公共字段或属
性,则仍然无法进行XML序列化。因为这些字段或属性无法被序列化过程访问。
在C#中,只有公共实例字段、公共静态字段、公共可读属性、公共只读属性、公共可
访问的事件和公共可访问的方法才能被序列化。私有成员不在此范围内,因此无法直接进
行序列化。
然而,有一种方法可以实现私有成员的序列化,即使用反射(Reflection)。通过反
射,我们可以访问私有成员并读取其值,然后将其序列化。但是,这种方法需要额外的代
码实现,并且可能会影响性能。
3、下面写写自定义xml序列
提示:
XML序列化只会将对象的公有字段和属性序列化为XML,而忽略对象的方法、事件和索
引器等。这是因为XML序列化的目的是保存对象的状态,而方法、事件和索引器等通常表示
对象的行为和功能,不是对象的状态的一部分。如果您希望将方法、事件和索引器等内容
一起序列化,可以考虑使用其他序列化方法,如Binary序列化或Json序列化。
internal class Program{private static void Main(){List<Person> list = new List<Person>(){new Person() { Name = "Tom", Age = 18 },new Person() { Name = "John", Age = 19 },new Person() { Name = "Jack", Age = 20 }};MySerializer(list, typeof(List<Person>));//Person p = new Person() { Name = "Tom", Age = 18 };//MySerializer(p, typeof(Person));Console.ReadKey();}private static void MySerializer(object obj, Type type){//主要针对List<T>及Object情况if (obj is ICollection)//是否为集合{//List<T>情况ICollection c = (ICollection)obj;XDocument xdoc = new XDocument();XElement xRoot = new XElement("List");xdoc.Add(xRoot);foreach (var o in c){XElement item = new XElement("item");PropertyInfo[] ps = o.GetType().GetProperties();//先属性foreach (PropertyInfo p in ps){item.SetElementValue(p.Name, p.GetValue(o, null));}FieldInfo[] fs = o.GetType().GetFields(); //后字段foreach (FieldInfo f in fs){item.SetElementValue(f.Name, f.GetValue(o));}xRoot.Add(item);}xdoc.Save(@"E:\22.xml");}else{//Object情况XDocument xdoc = new XDocument();XElement xRoot = new XElement(type.Name);xdoc.Add(xRoot);PropertyInfo[] ps = type.GetProperties(); //先属性foreach (PropertyInfo p in ps){xRoot.SetElementValue(p.Name, p.GetValue(obj, null));//a}FieldInfo[] fs = type.GetFields();//后字段foreach (FieldInfo f in fs){xRoot.SetElementValue(f.Name, f.GetValue(obj));}xdoc.Save(@"E:\22.xml");}}}public class Person{private string id;public string school = "xz";public int Age { get; set; }public string Name { get; set; }public void SayHi(string s){id = "001";Console.WriteLine(s);}}
上面主要针对list<T>与person情况写,有字段有属性,下面看一下特征,简化为只属性。
问:上面a处p.GetValue(obj, null)表示什么意思?
答:p和类是一个级别,是概念级。如同佛家说的灵魂层面,类与属性就是灵魂层面,而
它们的实际对象或变量的值,就是肉身。灵魂永久不变,但可以轮回变化肉身(实例或值)
因此,p是属性,是概念,GetValue是取值,obj是实例,这样才灵魂与肉身相合,取得
具体的值,第二个参数是索引值,因此有些属性是索引器,是一组值,不只一个值,需要
指定索引。如果不是索引器,就直接用null代表即可。
4、Attribute特性
简言之:特性实质就是一个标记,方便后续任务的识别和处理。
特性(Attribute)是一种用于在代码中添加元数据的机制。它们提供了一种在编译时
为代码添加附加信息的方式,以便在运行时可以通过反射机制来获取这些信息。
特性的主要用途是为代码提供额外的元数据,以便在运行时可以根据这些元数据执行
不同的操作。它们可以用于以下场景:
代码注释和文档:特性可以用于为代码添加注释、文档和说明。例如,可以使用特性
来标记某个方法的用途、参数的含义等。
运行时行为修改:特性可以用于在运行时修改代码的行为。通过读取特性中的元数据,
可以根据不同的条件执行不同的逻辑。
自定义属性:特性可以用于为类、方法、属性等添加自定义属性。这些属性可以用于
标记特定的行为或功能,以便在运行时进行处理。
特性提供了一种灵活的方式来为代码添加额外的元数据,并且可以通过反射机制在运
行时获取和处理这些元数据。
特性在C#中可以看作是一种标记,用于为代码添加附加的元数据信息。这些特性可以
在编译时被读取和处理,以实现不同的功能或行为。
特性可以用于标记类、方法、属性、字段等代码元素,并且可以为它们提供额外的信
息。在运行时,可以使用反射机制来读取代码中的特性,并根据特性中的元数据执行相应
的逻辑。
问:[XmlIgnore]是什么意思?
答:[XmlIgnore] 是一个属性应用程序,用于指示在序列化和反序列化过程中忽略特定的
字段或属性。
当一个类被序列化成XML时,所有公共的字段和属性都将被默认包括在XML中。然而,
有时候我们可能希望某些字段或属性在序列化过程中被忽略掉,以避免将不必要的数据暴
露给外部。这时,我们可以在需要忽略的字段或属性上使用[XmlIgnore]属性。例如:
public class Person{public string Name { get; set; }[XmlIgnore]public int Age { get; set; }}
Age属性被标记为[XmlIgnore],因此在序列化Person对象时,Age将被忽略掉。
注意,[XmlIgnore]属性只能应用于公共的字段和属性。私有的字段或属性将不受影
响,仍然会被序列化。另外,[XmlIgnore]属性还可以应用于基类中的字段或属性,以便
在派生类中忽略它们。
5、针对上面,进行为人忽略的标记特性的设置和使用
internal class Program{private static void Main(){Person p = new Person() { Name = "Tom", Age = 18 };MySerializer(p, typeof(Person));Console.ReadKey();}private static void MySerializer(object obj, Type type){//主要针对List<T>及Object情况if (obj is ICollection)//是否为集合{//List<T>情况}else{//Object情况XDocument xdoc = new XDocument();XElement xRoot = new XElement(type.Name);xdoc.Add(xRoot);PropertyInfo[] ps = type.GetProperties(); //先属性foreach (PropertyInfo p in ps){object[] objs = p.GetCustomAttributes(typeof(MyIgnoreAttribute), false);//aif (objs.Length == 0)//b 指定的特性若没有,不用忽略{xRoot.SetElementValue(p.Name, p.GetValue(obj, null));}else//c 忽略{continue;}}xdoc.Save(@"E:\22.xml");}}}public class Person{[MyIgnore]//epublic int Age { get; set; }public string Name { get; set; }}public class MyIgnoreAttribute : Attribute//d{}
a处:获取自定义特性集合,第一参数指定自定义特性的类型,第二参数指定是否来自父类
继承。实际返回的应该是MyIgnoreAttribute类型,为了简化用了var,同时也不必进行
强制转换类型。
b处:集体是否有值,>0有值,说明该属性有这个特性,就跳过继续c处,否则必须处理。
d处:因为特性就是一个标志,无须更多的说明,所以内部一般空代码。
e处:特性可以直接忽略后面的Attribute字样而正常使用,但为了简化,一般在d处定义时
后缀就不要再加Attribute了。
六、浅拷贝与深拷贝
1、什么是浅拷贝,什么是深拷贝
浅拷贝和深拷贝是对象复制的两种不同方式。
浅拷贝是指创建一个新对象,并将原对象的成员值复制到新对象中。如果对象包含引用
类型的成员,那么浅拷贝只会复制引用,而不会复制引用对象本身。这意味着新对象和原对
象的引用类型成员将指向同一个对象。修改其中一个对象的引用类型成员,会影响到另一个
对象的相应成员。
浅拷贝通常使用`MemberwiseClone`方法或对象的复制构造函数来实现。
深拷贝是指创建一个新对象,并将原对象的成员值复制到新对象中,包括引用类型的成
员。这样,新对象和原对象的引用类型成员都会指向各自独立的对象。修改其中一个对象的
引用类型成员,不会影响到另一个对象的相应成员。深拷贝通常通过手动复制对象的成员
,或者使用序列化和反序列化来实现。
区别和用途:
浅拷贝适用于对象的成员都是值类型或不可变类型,并且没有引用类型成员的情况。它
可以快速创建对象的副本,但是修改其中一个对象的引用成员会影响到另一个对象。
深拷贝适用于对象包含引用类型成员的情况。它可以创建一个独立的对象副本,修改其
中一个对象的成员不会影响到另一个对象。
实现深拷贝时,可以使用以下方法:
(1)使用自定义的拷贝构造函数或拷贝方法,手动复制对象的成员,包括引用类型的成
员。
(2)使用序列化和反序列化,将对象序列化为字节流,然后反序列化为一个新的对象。
这样可以实现对象的完全复制,包括引用类型的成员。
在实现深拷贝时,需要注意:
对象及其所有引用类型成员都必须实现深拷贝,以确保每个对象都有自己的独立副本。
如果对象的引用类型成员是可变类型(如集合),则需要确保在深拷贝过程中也复制
了这些成员的元素,以避免共享相同的元素。
对象及其引用类型成员的成员也可能需要实现深拷贝,以确保整个对象图都被正确复制。
internal class Program{private static void Main(){Person p1 = new Person() { Name = "Tom", Age = 18, Bike = new Bike() { Brand = "永久" } };Person p2 = p1;//p2指向了p1,并没发生拷贝//下面是浅拷贝Person p3 = new Person();p3.Name = p1.Name;p3.Age = p1.Age;p3.Bike = p1.Bike;//关键,区别在于是否指向同一对象//下面是深拷贝Person p4 = new Person();p4.Name = p1.Name;p4.Age = p1.Age;p4.Bike = new Bike() { Brand = p1.Bike.Brand };Console.ReadKey();}}public class Person{public string Name { get; set; }public int Age { get; set; }public Bike Bike { get; set; }}
2、实现浅拷贝与深拷贝
(1)浅拷贝:使用MemberwiseClone(),返回类型是object,注意类型转换。
(2)深拷贝:使用二进制序列化和反序列化。
或者先浅拷贝,再手动对引用进行重新赋值。
internal class Program{private static void Main(){Person p1 = new Person() { Name = "Tom", Age = 18, Bike = new Bike() { Brand = "永久" } };Person p2 = p1.QianCopy();p1.Name = "John";Console.WriteLine(p2.Name);Console.ReadKey();}}[Serializable]public class Person{public string Name { get; set; }public int Age { get; set; }public Bike Bike { get; set; }public Person QianCopy(){return this.MemberwiseClone() as Person;}public Person DeepCopy(){BinaryFormatter bf = new BinaryFormatter();using (MemoryStream ms = new MemoryStream()){bf.Serialize(ms, this);ms.Position = 0; //areturn bf.Deserialize(ms) as Person;//b}}}[Serializable]public class Bike{public string Brand { get; set; }}
上面a处,因为写入ms后,游标在末尾需要重置到开头。
b处,因为反序列化返回的是object需要强制转换。
问:string也是引用类型,为什么在浅拷贝时无需关心它?
答:string在C#中被特殊对待,具有不可变性。这意味着一旦创建了一个string对象,它
的值就不能被修改。任何对string对象的修改实际上都会创建一个新的string对象。
当我们对一个string对象进行修改时,实际上是创建了一个新的string对象,而不是
修改原始的string对象。由于string是不可变的,它的值在创建后就不能被修改。
所以,在浅拷贝中,当我们复制一个对象的string属性时,实际上是复制了一个指向
原始string对象的引用。但当我们修改其中一个对象的string属性时,实际上是创建了一
个新的string对象,并将新的string对象赋值给属性,而不会修改原始的string对象。因
此,另一个对象的string属性不会受到影响。
因此,这个规律暗合值类型的变化规律,所以在浅拷贝中把它"当作"值类型。