一、作业问题
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)多维数组
cs
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] 的方式。在添加或删除元素时,需要小心对嵌套列
表的索引进行管理,以避免出现越界或逻辑错误。
cs
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>>
这是一个将键值对中的值作为列表的字典。它适用于需要按键进行分组的场景。
使用场景:当需要对数据进行分组,并且每个组可以包含多个元素时,可以使用这样的
嵌套字典。
注意:当添加新的元素时,需要先检查字典中是否存在对应的键,如果不存在则创建一
个新的列表,并将元素加入到列表中。当访问嵌套字典中的元素时,先访问键的值(即列
表),然后再通过索引访问列表中的元素。
cs
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>>
这是一个嵌套的哈希集合,用于存储唯一的元素集合。
使用场景:当需要存储多个唯一的集合,并且每个集合可能包含多个唯一元素时,可以
使用嵌套的哈希集合。
注意:当添加新的元素时,需要先检查嵌套的哈希集合中是否存在对应的集合,如果不
存在则创建一个新的集合,并将元素加入到集合中。当访问嵌套哈希集合中的元素时,先访
问外层集合,然后再通过迭代方式访问内层集合中的元素。
cs
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 };//a
set.Add(set0);
set.Add(set1);
set.Add(set2);
Console.WriteLine(set.ElementAt(2).ElementAt(1));//7
HashSet<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
查询并获取符合条件的节点。
```cs
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
通过定义相应的重载方法,我们可以修改这些运算符在自定义类型上的操作行为。
注意下面写法:public static 返回类型 operator 符号(参数)
(1)算术运算符重载:
我们可以重载算术运算符,如加法运算符(+)、减法运算符(-)、乘法运算符(*)
和除法运算符(/)。例如,假设我们有一个自定义的Vector类,我们可以重载加法运算
符来执行向量的加法操作。
cs
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类,我们可以重载相
等运算符以比较两个人的年龄是否相等。
cs
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的结果可能与定义==的结果不一样。若更改为一样,则在类内重写:
cs
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>进行序列化。
cs
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; }
}
如果不需要写到文件中,则只需要用内存流即可:
cs
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;//d
Console.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序列化。
cs
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]属性。例如:
cs
public class Person
{
public string Name { get; set; }
[XmlIgnore]
public int Age { get; set; }
}
Age属性被标记为[XmlIgnore],因此在序列化Person对象时,Age将被忽略掉。
注意,[XmlIgnore]属性只能应用于公共的字段和属性。私有的字段或属性将不受影
响,仍然会被序列化。另外,[XmlIgnore]属性还可以应用于基类中的字段或属性,以便
在派生类中忽略它们。
5、针对上面,进行为人忽略的标记特性的设置和使用
cs
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);//a
if (objs.Length == 0)//b 指定的特性若没有,不用忽略
{
xRoot.SetElementValue(p.Name, p.GetValue(obj, null));
}
else//c 忽略
{
continue;
}
}
xdoc.Save(@"E:\22.xml");
}
}
}
public class Person
{
[MyIgnore]//e
public 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)使用序列化和反序列化,将对象序列化为字节流,然后反序列化为一个新的对象。
这样可以实现对象的完全复制,包括引用类型的成员。
在实现深拷贝时,需要注意:
对象及其所有引用类型成员都必须实现深拷贝,以确保每个对象都有自己的独立副本。
如果对象的引用类型成员是可变类型(如集合),则需要确保在深拷贝过程中也复制
了这些成员的元素,以避免共享相同的元素。
对象及其引用类型成员的成员也可能需要实现深拷贝,以确保整个对象图都被正确复制。
cs
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)深拷贝:使用二进制序列化和反序列化。
或者先浅拷贝,再手动对引用进行重新赋值。
cs
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; //a
return 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属性不会受到影响。
因此,这个规律暗合值类型的变化规律,所以在浅拷贝中把它"当作"值类型。