@



- 从命令行读取和输入的例子:

- 类、结构的默认修饰符是internal。
- 类中所有的成员默认修饰符是private。
- 接口默认修饰符是internal。
- 接口的成员默认修饰符是public。
- 命名空间、枚举类型成员默认修饰符是public。
- 委托的默认修饰符是internal。
从某一个类型模板创建实际的对象,就称为实例化该类型。
一个简单的变量声明至少需要一个类型和一个名称,如int a,其中int为类型,a为名称。

虽然常量成员表现得像一个静态量,但不能将常量声明为静态static,如:static const int a=3.14这句语法是错误的。
1. this指代当前实例
2. this用作扩展方法
3. this用作索引器:
虽然派生类不能删除它继承的任何成员,但可以用与基类成员名称相同的成员来屏蔽基类成员,这也是继承的主要功能之一,很实用。派生类屏蔽基类成员的关键字是new,在派生类中屏蔽一些基类成员的一些要点如下:
(1)要屏蔽一个继承的数据成员,需要声明一个新的相同类型的成员,并使用相同的名称。
(2)通过在派生类中声明新的带有相同签名的函数成员,可以隐藏或屏蔽继承的基类的函数成员。注意:签名由名称和参数列表组成,不包括返回类型。
(3)要让编译器知道你在故意屏蔽继承的成员,实用new修饰符。否则,程序会成功编译,但会警告你隐藏了一个继承的成员。
(4)也可以屏蔽静态成员。
当使用基类引用访问派生类对象时,得到的是基类的成员。虚方法可以使基类的引用访问“升至”派生类级别。可以使用基类引用调用派生类的方法,只需要满足以下条件:
(1)派生类的方法和基类的方法拥有相同的签名和返回类型;
(2)基类的方法使用virtual标注;
(3)派生类的方法使用override标注。
可对比以下两图:



如,一个简单的继承类程序:
而另外一种形式的构造函数初始化语句(通过this)可以让编译器使用该类中的其他构造函数。
readonly字段只可以在构造函数中初始化,如果在其他方法中初始化一个readonly字段(即使这个方法只被构造函数调用),会得到一个编译错误。

- 必须在定义时初始化。必须是var a=“abc”的形式,不能是var a; a=“abc”的形式;
- 只能用于本地变量,不能用于字段;
- 只能在变量声明中包含初始化时使用;
- 一旦编译器推断除变量的类型,它就是固定且不能更改的。



静态类中所有的成员都是静态的,静态类用于存放不受实例数据影响的函数或数据。静态类的一个常见用途就是创建一个包含一组数学方法和值的数学库。
关于静态类需要注意的几个地方:
(1)类本身必须被标记为static;
(2)类的所有成员必须是静态的;
(3)类可以有一个静态构造函数,但不能有实例构造函数,不能创建该类的实例;
(4)静态类是隐式封装的,也就是说,不能继承静态类。

static修饰的静态字段被类的所有实例共享,所有实例都访问同一内存位置。如果该内存位置的值被一个实例改变了,这种变化对于所有的实例都改变。


1.delegate委托
先看一代码:
(2)使用该委托类型声明一个委托变量;
(3)创建委托类型的变量,把它赋值给委托变量。新的委托对象包含指向某个方法的引用,这个方法和第一步定义的签名和返回类型保持一致;
再看一段代码,稳固下委托的使用步骤:
2.Action委托
Action委托签名不提供返回类型(也就是说Action委托是没有返回值的),它具有Action、Action<T1,T2>、Action<T1,T2,T3>……Action<T1,……T16>多达16个的重载,其中传入参数均采用泛型中的类型参数T,涵盖了几乎所有可能存在的无返回值的委托类型。
代码示例:

3.Func委托
Func具有Func

4.Predicate委托
- Predicate是返回bool型的泛型委托;
- Predicate
表示传入参数为int,返回bool的委托; - Predicate有且只有一个参数,返回值固定为bool;

5.这几种委托的区别:
- Delegate至少0个参数,至多32个参数,可以无返回值,也可以指定返回值类型;
- Func可以接受0个至16个传入参数,必须具有返回值;
- Action可以接受0个至16个传入参数,无返回值;
- Predicate只能接受一个传入参数,返回值为bool类型;

(5)再如代码举例:

1.什么是接口
接口是使用interface关键字声明的数据类型。
2.接口的作用是是什么
- 作为一个客观的规范
比如,如果把接口比做一个合同,这个合同规定了你能做什么事情,但是没有规定你怎么做。那么实现了这个接口的人就相当于履行合同的人,这个人必须按照合同的规定去做事情,但是不同的人在做这些事情时可以有不同的实现。
- 实现多态
3.接口的特点是什么
- 接口是抽象的行为,规定了能做什么,没规定怎么做。
- 实现了接口必须实现接口的所有成员。
4.接口如何使用
- 类继承接口 Class A : interface B,interface C
类需要实现继承接口中所有的方法,支持多继承。
- 接口继承接口 interface A :interface B,interface C
接口不能实现继承接口的任何方法,支持多继。
6.接口和抽象类的区别
- 接口的主要作用是定义类型之间的契约,实现了接口的类必须按照接口定义的契约来实现它的成员,从而可以实现不同类型之间的通用性和互换性。抽象类的主要作用是为了实现多态性,它可以为子类提供一组基础功能,并要求子类必须实现一些具体的方法。抽象类可以包含一些具体的实现,但同时也可以包含一些抽象的成员,子类必须实现这些抽象成员。
- 使用接口的主要场景是当你需要定义一组通用的规范或契约时。比如说,你可以定义一个 IDisposable 接口,规定实现该接口的类型必须实现 Dispose 方法,用于释放资源。又比如,你可以定义一个 IComparer 接口,规定实现该接口的类型必须实现 Compare 方法,用于比较两个对象的大小。使用抽象类的主要场景是当你需要为子类提供一组基础功能,并要求子类必须实现一些具体的方法时。比如说,你可以定义一个 Animal 抽象类,规定所有动物都必须具有 Eat 和 Sleep 方法,但对于不同的动物,它们的实现方法是不同的,因此你可以定义一个 Dog 类和一个 Cat 类,分别继承 Animal 类,并实现它的抽象方法。
- 接口和抽象类都可以用来定义一组抽象的方法和属性,但它们的应用场景有所不同。当你需要定义一组通用的规范或契约时,应该使用接口;当你需要为子类提供一组基础功能,并要求子类必须实现一些具体的方法时,应该使用抽象类。
(1)装箱和拆箱
- 装箱:将值类型转换为引用类型的操作。
- 拆箱:相应地将引用类型转换成值类型。

(2)is运算符(用来检测转换是否成功,不像as操作符那样直接转换):
注意: is运算符只可用于引用、装箱、拆箱三种转换,不可用于用户自定义转换。
(3)as运算符:

(4)泛型方法:
泛型方法具有类型参数列表和可选的约束。
- 泛型方法有两个参数列表:
封闭在圆括号内的方法参数列表和封闭在尖括号内的类型参数列表。 - 要声明泛型方法,需要:在方法名称之后和方法参数列表之前防止类型参数列表,并在方法参数列表后放置可选的约束子句。
如图:

- 泛型方法的调用:

- 泛型方法举例:
代码:

(5)扩展方法和泛型类:
扩展方法可以和泛型类结合来使用,它允许我们将类中的静态方法关联到不同的泛型类上,还允许我们像调用类构造实例的实例方法一样来调用方法。

代码举例:

(6) 泛型结构


(8)泛型接口:
代码示例1:泛型类继承泛型接口

代码示例2:非泛型类继承泛型接口


如下面一段代码:



(7)join(联结)子句:
LINQ中的Join接受两个集合然后创建一个新的集合,每一个元素包含两个原始集合中的原始成员。
看一段代码:

再看一段代码:

(8)orderby子句:
orderby子句接受一个表达式并根据表达式顺序返回结果项。

(9)select ...group子句:

(10)group子句:
group子句按照一些标准进行分组。
(11)查询延续:into子句:
into子句可以接受查询的一部分结果并赋予另外一个名字,从而可以在查询的另一部中使用。

(12)再看一段代码:


1.进程和线程分别是什么?谈谈进程和线程的区别与联系。
- 默认情况下,一个进程只包含一个线程,从程序的开始一直执行到结束;
- 线程可以派生其他线程,因此在任意时刻,一个进程都可能包含不同状态的多个线程来执行程序的不同部分;
- 如果一个进程拥有多个线程,他们将共享进程的资源;
系统为处理器执行所规划的单元是线程,不是进程。
2.使用CancellationTokenSource 取消多线程

3. 异步编程
首先,一个描述很详细的博客链接: 异步编程
4.Task.Run 和 Task.Factory.StartNew 区别
不推荐他使用 Task.Factory.StartNew ,因为 Task.Run 是比较新的方法。
需要知道 是在 dotnet framework 4.5 之后才可以使用,但是 可以使用比 更多的参数,可以做到更多的定制。
可以认为 是简化的 的使用,除了需要指定一个线程是长时间占用的,否则就使用
对比说明如下:
1.创建新线程
下面来使用两个函数创建新的线程:
这时 foo 的创建就在另一个线程,需要知道 Task.Run 用的是线程池,也就是不是调用这个函数就会一定创建一个新的线程,但是会在另一个线程运行。
可以看到,两个方法实际上是没有差别,但是比较好看,所以推荐使用。
2.等待线程
创建的线程,如果需要等待线程执行完成再继续,那么可以使用 await 等待。
输出结果:
但是需要说的是这里使用 await 主要是给函数调用的外面使用,上面代码在函数里面使用 await 函数是 void 那么和把代码放在 task 里面是相同。
但是如果把 void 修改为 Task ,那么等待线程才有用。
除了使用 await 等待,还可以使用 WaitAll 等待。
使用 WaitAll 是在调用 WaitAll 的线程等待,也就是先在线程 1 运行,然后异步到线程3 运行,这时线程1 等待线程2运行完成再继续,所以输出结果:
3.长时间运行
两个函数最大的不同在于 可以设置线程是长时间运行,这时线程池就不会等待这个线程回收。
所以在需要设置线程是长时间运行的才需要使用 不然就使用
调用 就和使用下面代码一样:
实际上 可以认为是对 封装,使用简单的默认的参数。如果需要自己定义很多参数,就请使用 定义参数。
5.TaskCompletionSource的使用
1.TaskCompletionSource简介以及简单使用方法
MSDN链接:添加链接描述
TaskCompletionSource生成Task方法,使用TaskCompletionSource很简单,只需要实例化它即可。TaskCompletionSource有一个Task属性,可以对该属性暴露的task做操作,比如让它wait或者ContinueWith等操作。当然,这个task由TaskCompletionSource完全控制。
大多数时候,只在目标方法要调用基于事件API,又要返回Task的时候使用。比如下面的ApiWrapper方法,该方法要返回Task
再看一个例子:TaskCompletionSource中有一个SetResult方法,当该方法被调用后。就会让await等待的代码继续往下执行。
输出结果:
2.TaskCompletionSource 的 TrySetResult 是线程安全
在创建一个 TaskCompletionSource 期望让等待的逻辑只会被调用一次,而调用的是多线程,可以使用 TrySetResult 方法,这个方法是线程安全,只会让 TaskCompletionSource 被调用一次。
在多个线程调用 TaskCompletionSource 的 TrySetResult 方法,只有一个线程能进入设置,其他线程将会拿到返回 false 的值
测试代码如下:
输出结果:
6. c#判断代码是否执行超时的几种方式
1.使用Task
2.使用Thread
7.C# Monitor和Lock的定义及区别
1.Monitor对象
- Monitor.Enter(object)方法是获取锁,Monitor.Exit(object)方法是释放锁,这就是Monitor最常用的两个方法,当然在使用过程中为了避免获取锁之后因为异常,致锁无法释放,所以需要在try{} catch(){}之后的finally{}结构体中释放锁(Monitor.Exit())。
- Monitor的常用属性和方法:
- Enter(Object) 在指定对象上获取排他锁。
- Exit(Object) 释放指定对象上的排他锁。
- IsEntered 确定当前线程是否保留指定对象锁。
- Pulse 通知等待队列中的线程锁定对象状态的更改。
- PulseAll 通知所有的等待线程对象状态的更改。
- TryEnter(Object) 试图获取指定对象的排他锁。
- TryEnter(Object, Boolean) 尝试获取指定对象上的排他锁,并自动设置一个值,指示是否得到了该锁。
- Wait(Object) 释放对象上的锁并阻止当前线程,直到它重新获取该锁。
2.Lock关键字
- 如果在使用多线程时,在相同的时间内有多个线程同时执行相同的方法,也许就存在数据安全的问题,如多个线程之间对于相同的内存进行同时的读取和修改。为了让多线程每次只能有一个线程执行,可以使用的方法有很多。
在C#里面可以使用关键词lock加上一个对象作为锁定,在进入lock的逻辑,只能有一个线程获取锁,因此在lock里面的代码只能被一个线程同时执行。
lock锁的究竟是什么?是lock下面的代码块吗,不,是locker对象。我们想象一下,locker对象相当于一把门锁(或者钥匙),后面代码块相当于屋里的资源。哪个线程先控制这把锁,就有权访问代码块,访问完成后再释放权限,下一个线程再进行访问。注意:如果代码块中的逻辑执行时间很长,那么其他线程也会一直等下去,直到上一个线程执行完毕,释放锁。
Lock关键字实际上是一个语法糖,它将Monitor对象进行封装,给object加上一个互斥锁,A进程进入此代码段时,会给object对象加上互斥锁,此时其他B进程进入此代码段时检查object对象是否有锁?如果有锁则继续等待A进程运行完该代码段并且解锁object对象之后,B进程才能够获取object对象为其加上锁,访问代码段。
例如,以下代码就是标准的锁定方法的代码:
- Lock关键字封装的Monitor对象结构如下:
- 锁定的对象应该声明为private static object obj = new object();尽量别用公共变量和字符串、this、值类型。
3.Monitor和Lock的区别
- Lock是Monitor的语法糖。
- Lock只能针对引用类型加锁。
- Monitor能够对值类型进行加锁,实质上是Monitor.Enter(object)时对值类型装箱。
- Monitor还有其他的一些功能。
代码示例:
8.Thread.Abort()和Thread.ResetAbort()
先看一个问题,如下,要在Main方法中如何操作才能执行Foo方法finally中的语句?
答案之一:使用一个线程调用的方式,调用之后结束线程,此时就会输出
输出结果:
输出结果:
现在解释一下,在默认(不调用Thread.ResetAbort())的情况下, finally块后的代码是执行不到的,这是由于 ThreadAbortException这个异常非常特殊,它会在finally块的最后(如果没有finally块,则是在catch块的最后)重新扔出一个 ThreadAbortException异常。(不过这个异常在外部抓不到,它仅仅是为了退出线程用的)。
9.Task线程的开始、暂停、继续、取消
- MainWindow.xaml代码:
- MainWindowViewModel.cs代码:
- Book.cs代码:
- Data.cs代码:
10.Task的一些用法详解
Task是微软在.Net 4.0时代推出来的,Task看起来像一个Thread,实际上,它是在ThreadPool的基础上进行的封装,Task的控制和扩展性很强,在线程的延续、阻塞、取消、超时等方面远胜于Thread和ThreadPool,所以一经问世,基本ThreadPool就被取代了。
task有很多封装好的API,比如:
- WaitAll:等待提供的所有 System.Threading.Tasks.Task 对象完成执行过程。
- WaitAny:等待提供的任一 System.Threading.Tasks.Task 对象完成执行过程。
- ContinueWith:创建一个在目标 System.Threading.Tasks.Task 完成时异步执行的延续任务。
这里分别开启了两个线程t1、t2,在t1里面等待1秒,t2里面等待2秒,所以执行WaitAny时先等到ti完成,WaitAll时会等到t2完成.最终输出结果如下:
- Wait:等待 System.Threading.Tasks.Task 完成执行过程。
- Start:启动 System.Threading.Tasks.Task,并将它安排到当前的 System.Threading.Tasks.TaskScheduler中执行;
带返回值的使用方式:
11.Thread类中IsBackground属性
12.C# 前台线程和后台线程的区别(重要知识点)
前台线程和后台线程唯一区别:应用程序必须运行完所有的前台线程才会完全退出,若前台线程未执行完成,关闭应用程序后,应用程序并没有完全退出,在任务管理器中还存在此进程;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。
- 在任何时候我们都可以通过线程的IsBackground属性改变线程的前后台属性:
- thread.IsBackground=false;前台线程
- thread.IsBackground=true;后台线程
- 应用程序的主线程以及使用Thread构造的线程都默认为前台线程
线程池线程也就是使用 ThreadPool.QueueUserWorkItem()和Task工厂创建的线程都默认为后台线程。
线程由程序员创建,可是创建的方式不同,总体来说有两种,一种是个人构造,也就是使用thread类new线程对象创建,这一类线程是大部分程序员知道的,也叫专用线程;还有一种是由CLR创建,这一类线程主要存在于线程池中,也叫线程池线程。对于这两种线程的好坏,建议最好使用线程池线程,不要大量使用专用线程。
从回收的角度来看又可分为前台线程和后台线程:
- 后台线程:后台线程是可以随时被CLR关闭而不引发异常的,也就是说当后台线程被关闭时,资源的回收是立即的,不等待的,也不考虑后台线程是否执行完成,就算是正在执行中也立即被终止。【后台,存在于黑暗之中默默无闻,它的消亡和存在,别人也感受不到】
- 前台线程:前台线程是不会被立即关闭的,它的关闭只会发生在自己执行完成时,不受外在因素的影响。假如应用程序退出,造成它的前台线程终止,此时CLR仍然保持活动并运行,使应用程序能继续运行,当它的的前台线程都终止后,整个进程才会被销毁。
13.向线程里面传递数据
(1)字符串string类型的一些成员:
(2)Spilt方法:
该方法很有用,该方法会将一个字符串分割成若干个子字符串,并将他们以数组的形式返回。将一组按照预定位置分隔字符串的分隔符传给Spilt方法,就可以指定如何处理输出数组中的空元素(当然,原始字符串依然不会改变)。代码举例如下:

(3)StringBuilder类
StringBuilder类位于System.Text命名空间中,它可以帮助程序员动态、有效地产生字符串,并避免创建许多副本。代码举例如下:

(4)把字符串解析为数据值
解析允许我们接受表示值的字符串,并转化为实际的值,通过Parse静态方法。如:

但是,Parse方法有一个缺点,当转换不成功(即不能把string类型转化为其他类型)时会抛出一个异常,而在编程中要尽量避免异常。TryParse方法可以解决这个问题。


(1)List < T >是泛型集合,用法如下代码所示:

(2)List的一些常用属性和方法:

(3)可以用List

异:(1)类型不同:
类是引用类型,在堆上分配地址;
结构是值类型,在栈上分配地址;
(2)继承性不同:
类:是完全可扩展的,也可以继承其他类和接口,自身也能被继承;
结构:不能从另外一个结构或者类继承,本身也不能被继承(但能够继承接口,方法和类继承接口一样)。
(3)内部结构不同:
类: 有默认的构造函数和析构函数,可以使用访问修饰符 ,必须使用new 初始化;
结构: 没有默认的构造函数,但是可以添加构造函数,没有析构函数 ,可以不使用new 初始化。
同:基类型都是对象(object)
不同点:
不能直接实例化接口。
接口不包含方法的实现。
接口可以多继承,类只能单继承。
类定义可以在不同的源文件之间进行拆分。
相同点:
接口、类都可以从多个接口继承。
接口类似于抽象基类:继承接口的任何非抽象类型都必须实现接口的所有成员。
接口和类都可以包含事件、索引器、属性。

因为C#中所有的类都直接或者间接派生自Object类,因此Object类类是C#中唯一的非派生类。

注意:当出现"\n"时,结果是显示的是 ,而不会出现换行
(2)第二种C#转义字符方法是使用@

代码举例:

(1)抽象成员是抽象类中的成员,抽象成员是指设计为被覆写的函数成员,抽象成员具有以下特征:
- 抽象成员必须是函数成员,字段和常量不能为抽象成员;
- 抽象成员必须用Abstract标记;
- 抽象成员不能实现代码块;
- 抽象成员必须被子类用override关键字重写;
例如下图:

(2)虚成员和抽象成员的区别:
抽象方法是只有方法名称,没有方法体(也就是没有方法具体实现),子类必须用override关键字重写父类抽象方法。
虚函数有方法体,但是子类可以覆盖,也可不覆盖。
先看代码展示:

十二字口诀:“先静后构,静外到内,构内到外”;
先静后构:
一个类同时存在静态构造函数和普通构造函数,对象实例化后,先执行静态构造函数(先静),再执行普通构造函数(后构);
故上文对象A实例后输出的是SA baseA。
静外到内:
父类和子类都存在静态构造函数的时候,实例化子类后,先执行子类静态构造函数(静外),再执行父类静态构造函数(到内);
上文B继承了A,B和A同时存在静态构造函数但是由于一个静态构造函数在一个应用程序的完整生命周期中,最多只会被自动执行一次,因此基类的静态构造函数只会在基类被初始化的时候调用且调用一次,所以实例化B后,静态构造函数输出结果是是SB ,而不是SB SA。
构内到外:
实例化子类后,先执行父类的普通构造函数(构内),再执行子类的构造函数(到外);
上文B继承了A,实例化B后,普通构造函数输出结果是baseA baseB。
- 对于构造函数重载,先执行基类的构造函数,再执行子类的构造函数;
- 对于析构函数,先执行子类的析构函数,再执行基类的析构函数。




- 接口中不能声明常量和字段,抽象类中可以声明任何类成员;
- 在接口中只能定义成员,但不能具体实现,在抽象类中除了抽象方法外,其他成员允许有具体的实现;
- 接口中没有实例构造函数,也就是说没有构造函数,抽象类中有构造函数;
- 接口成员不能使用任何访问修饰符,抽象类中的类成员可以使用任意的访问修饰符;
- 继承接口的类或结构必须隐式或显式实现接口中的所有成员,否则需要将实现类定义为抽象类,并将接口中未实现的成员以抽象的方式实现,继承抽象类的类必须重写实现抽象类中的所有抽象方法,或者抽象类继承抽象类,可以重写部分抽象方法;
- 接口不能作为派生类继承,抽象类可以继承非抽象类或抽象类;
- 接口可以作为基类来多继承:接口、类和结构,抽象类可以作为基类只能实现单继承,只能让非抽象类或者抽象类继承。
输出结果:
发现了string类中一个特别好用的方法string.Join,先直接看效果:

看出来了,string.Join(",", arrays)这部分代码将arrays数组中每个成员用","分隔开,方便快捷。
下面是该方法原型:
public static String Join
- 摘要:
// 串联集合的成员,其中在每个成员之间使用指定的分隔符。 - 参数:
// separator:
// 要用作分隔符的字符串。只有在 values 具有多个元素时,separator 才包括在返回的字符串中。
// values:
// 一个包含要串联的对象的集合。
1.using指令
2.using别名
3.using语句
定义一个范围,在范围结束时处理(释放)对象。
注意:只有使用了IDisposible接口的对象才可以用using进行管理。
只在一定的范围内有效,出了这个范围时,自动调用IDisposable接口释放掉using语句块中的内容,当然并不是所有的类都适用,只有实现了IDisposable接口的类才可以使用。
using语句处理实现IDisposable的对象,并在作用域的末尾调用Dispose方法。
用Environment.GetEnvironmentVariable方法读取环境变量,并返回一个string类型的结果。
- 直接在C#文件中直接编写入口方法的代码,不用类,不用Main。经典写法仍然支持。反编译一下了解真相。
- 同一个项目中只能有一个文件具有顶级语句。
- 顶级语句中可以直接使用await语法,也可以声明函数,如:

sealed的英文意思就是密封,禁止的意思,故名思义,就是由它修饰的类或方法将不能被继承或是重写。在c#中sealed关键字可以用来修饰类和方法。
1.sealed关键字修饰类
2.sealed关键字修饰方法或属性
当sealed修饰方法时,表示该方法不能被重写。
1.通过接口注入
接口注入:
相比构造函数注入和属性注入,接口注入显得有些复杂,使用也不常见。具体思路是先定义一个接口,包含一个设置依赖的方法。然后依赖类,继承并实现这个接口。
2.通过属性访问器Settter注入
- UML图:

- 代码:

3.通过构造函数注入
- UML图:

- 代码:

C#中,yield关键字的作用是将当前集合中的元素立即返回,只要没有yield break,方法还是会继续执行循环到迭代结束。
- yield return是一次一个的返回,yield return例子:

通过单步调试即可发现,enumerableFuc方法每次被调用就会返回一个数据,第一次调用enumerableFuc方法会返回1,第二次调用会返回2,第三次调用会返回3。
- yield break用于结束返回(终止迭代),yield break例子:

通过此代码可以看出,此代码返回的结果是1和2,不是1、2和3,因为用了yield break,因此enumerableFuc方法中的第四行yield return 3这一行没有被执行。
- 引用拷贝:只复制对象的地址,并不会创建一个新的对象;
- 浅拷贝:浅拷贝会创建一个对象,并进行属性复制,但对于引用类型的属性,只会复制其对象地址;
- 深拷贝:深拷贝会完全复制整个对象,包括引用类型的属性。
浅拷贝代码举例,代码如下:
输出结果:
输出结果:
在C#中,string类型和数组类型也属于引用类型,而string类型比较特殊(具体可查资料),因此只需要手动克隆List类型的Hobbies属性即可实现深克隆。
1.Lazy简介
通过Lazy关键字,我们可以声明某个对象为仅仅当第一次使用的时候,再初始化,如果一直没有调用,那就不初始化,省去了一部分不必要的开销,提升了效率,同时Lazy是天生线程安全的。
2.应用场景
- 对象创建成本高且程序可能不会使用它;
- 对象创建成本高,且希望将其创建推迟到其他高成本操作完成后。
例如,假定程序在启动时加载多个对象实例,但是只需立即加载其中一部分。 可以通过推迟初始化不需要的对象,直到创建所需对象,提升程序的启动性能。
3.Lazy基本用法
用法1:构造时使用默认的初始化方式
在使用Lazy时,如果没有在构造函数中传入委托,则在首次访问值属性时,将会使用Activator.CreateInstance来创建类型的对象,如果此类型没有无参数的构造函数时将会引发运行时异常。
输出结果:
用法2:构造时使用指定的委托初始化
用法3:利用Lazy关键字来构造一个单例类
用法:
代码举例:
运行结果:
1.何时使用Dictionary而不是List
- 通常情况下,我们可以用int类型的索引来从数组或集合中来查询所需要的数据,但是当索引不是int(如string和double),这时就需要使用Dictionary字典。
- 当要存储的东西很多、列表很长时,可以使用Dictionary字典,字典的查询效率很高(List集合是循环遍历的查找方式,而字典是哈希查找)。
2.关于使用Dictionary时的注意事项
- 字典Dictionary在名称空间System.Collections.Generic下;
- 字典是一组键(Key)到一组值(Value)的映射;
- 键必须是唯一的且不能为空;
- 键和值可以是任何数据类型
3.Dictionary的一些简单用法
举个例子:
一个窗口如果里面有组件的话,那么每个组件也会有窗口句柄,这里的窗口提的是WINDOW,不带那个S的,表示的就是一个框,所以说,翻译上的不同,我认为也可以翻译成"框句柄",这比较符合实情,接下来,就可以对这个句柄进行操作了。
如果可以隐藏一个窗口,就发送消息让他隐藏,这里就用到API,当然API是比较多的,所有的功能都是通过API实现的。
更专业一点:
在Windows中,是一个32为无符号整数值,句柄是一个系统内部数据结构的引用,例如,当你操作一个窗口,或说是一个Delphi窗体时,系统会给你一个该窗口的句柄,系统会通知你:你正在操作142号窗口,就此,你的应用程序就能要求系统对142号窗口进行操作——移动窗口、改变窗口大小、把窗口极小化为图标,等等。实际上许多Windows API函数把句柄作为它的第一个参数,如GDI(图形设备接口)句柄、菜单句柄、实例句柄、位图句柄等等,不仅仅局限于窗口函数。
在实际编程中,IEnumerable接口常常和yield关键字配合使用。
比如像下面这样的方法,通过yield return每次返回一个数据,即产生了一个数据就返回一个:

又或者像下面这样的用法:

在一些返回集合数据的接口中,我们经常能看到IEnumerable接口的身影,那什么是Enumerable呢?首先它跟C#中的enum关键字所表达的意思是不同的, 从翻译上来看:可枚举的,展开来说就是它的数据是一枚一枚可以让我们列举出来。就像人们排队去打疫苗,排队的人就是可枚举的,他们有的开车,有走着,有早有晚全都按照先来后到的顺序排好队,当医生开始进行打疫苗的工作时,他并不关心有多少人在排队,也不关心是否有人迟到,当然也不能越过第一个人让其后边的人先进来打,他只能说“请下一个人进来打疫苗”,如果没人响应医生就等着,直到有人进来开始接种,当前这个人完成接种后,医生继续叫下一个人,直到所有人都打完疫苗。这样的情景在编程中就体现为对Enumerable数据的操作。
如下代码中有一个返回IEnumerable< string >的方法,用来模拟数据的产生,其中用到了一个yield关键字,yield return就是部分返回(产生了一个数据,就返回一个),这个方法最终的运行效果就是一秒钟返回一个当前时间构成一个IEnumerable
最终输出结果:
1.反射
反射就是我们在只知道一个对象的外部而不了解内部结构的情况下,可以知道这个对象的内部实现。
输出结果:
得到一个Type类型对象有三种方法:object.GetType()、Type.GetType()、typeof()
使用object.GetType()必须先创建一个实例,而后两种不需要创建实例,但使用typeof运算符仍然需要知道类型的编译时信息,Type.GetType()静态方法不需要知道类型的编译时信息,所以是首选方法。
2.特性
特性的使用很简单,在结构声明的上一行,用"[]"扩起特性类名即可:
3.反射和特性配合使用
属性的本质就是方法,get和set两个方法构成。
- 字段只管存值,不管对数据的操作,字段一定是占用内存的。
属性可以占用内存,也可以不占用,当属性中封装了字段时,那么属性会占用内存,当不封装字段而是做了其他操作时,是可以不占用内存的。
- 字段是给类自己内部用的,属性是给外部调用这个类的时候用的。
字段一般都声明为private私有的,而属性一般都声明为public公有的。
- 属性跟字段最根本区别就在于属性是类似于方法,字段就是变量。通过属性的set和get函数可以限制字段的一些功能,以达到某种目的。
感觉师傅给我讲的关于属性和字段的区别以及属性的优点,我还是没有理解核心区别,今天先写总结到这里,日后再有更深层次的理解了,继续完善。
- 错误使用赋值操作符导致原本的委托链被覆盖:
如果一个委托通过 "+=" 绑定了多个方法,那么当委托通过 "=" 绑定方法时,之前所有通过 “+=” 方式绑定的方法都将被覆盖。
- 在类的外部也可以调用委托:
尤其是委托可以在类的外部(即不同类之间)使用,就会显得很混乱,而且会容易造成错误操作。
总结下来就是:委托封装的不好,没有很严格规范的使用规则,事件则解决了这些问题:
事件本质是对委托的封装,事件只能在类内调用,+=和-=是事件允许的唯一运算符,可以为事件定义事件访问器。有两个访问器add和remove,声明事件的访问器和声明一个属性差不多:
关于序列化的详细知识可参考此篇文章C# Xml进行序列化与反序列化
- 序列号的概念
序列化就是把一个对象保存到一个文件或数据库字段中去,反序列化就是在适当的时候把这个文件在转化成原来的对象使用。对象的序列化不是类的序列化。对象的序列化表明C#提供了将运行的对象(实时数据)写入硬盘文件或数据库中,此功能可以运用在需要保留程序运行时状态信息的环境下。
- 使用序列化的两个重要的原因
第一个原因:是将对象的状态永久保存在存储媒体中,以便可以在以后重新创建精确的副本;
第二个原因:是通过值将对象从一个应用程序域发送到另一个应用程序域中。
前提是要将对象的声明为可以序列化。
- 最主要的作用有
第一个作用:在进程下一次启动的时侯读取上一次保存的对象的信息。
第二个作用:在不同的AppDomain或进程之间传递数据。
第三个作用:在分布式应用系统中传递数据。
序列化是把一个内存中的对象的信息转化成一个可以持久化保存的形式,以便于保存或传输,序列化的主要作用是不同平台之间进行通信,常用的序列化有json、xml、文件等。
- 代码举例:

输出结果:
- 通过FileInfo和DirectoryInfo类来读取文件和文件夹属性
包括:查看文件属性,创建文件,移动文件,重命名文件,判断路径是否存在,创建目录。 - 通过File来读写文件。
- 使用流来读写文件
FileStream,StreamReader(读取流,读取数据),StreamWriter(写入流,向别人传输)。
1.使用FileInfo查看文件属性
代码示例:
2.使用DirectoryInfo查看文件夹属性
代码示例:
3.使用File来读写文件
代码示例:
4.使用FileStream流来读写文件
FileStream(文件流) 这个类主要用于二进制文件中读写,也可以使用它读写任何文件(包括一些图像、音频文件)。
文件流FileStream 位于命名空间System.IO下,主要用来操作文件流,与File类的读取写入相比File类读取文件时是一次性读取,在操作大型文件时容易导致内存飙升,FileStream类则可以对一个文件分多次进行读取,每次只读取一部分,节省内存空间。FileStream就像把水缸里的水一瓢一瓢的取出来,而不像File类一次性倒出来,因此FileStream对电脑的内存占用资源占用方面相对较小,使用范围更广。
5.使用StreamReader和StreamWriter流读写文本文件
StreamReader(流读取器)和StreamWriter(流写入器)专门用于读写文本文件。
示例一:
示例二:
6.MemoryStream流
数据 —> 通过inputStream()、Read()转化为流 —> 通过outputStream()、Writer()转化为数据,就像流是通过一个小细管道传输的,input输入管道变为流,output输出管道变为数据,流类读取数据为流,流将流写入数据。
C#中的MemoryStream是一个实现了Stream类的内存流类,用于在内存中读写数据。
下面是一些常用的属性和方法:
属性:
方法:
代码举例:
7.C#中操作表格
如下例子,向一个csv文件中写入数据并读取:
以下代码需要注意一点:
注意File.Create方法返回的是一个流,必须要调用Close方法关闭流,否则会报异常提示正在被其他的线程占用,因此不能用File.Create(),要用File.Create.Close方法
运行结果:
8.C# 使用File.Create方法创建文件时,报进程被占用
在一个程序里偶然用了System.IO.File.Create去创建文件,运行时一直报错(进程被占用),后来在网上找到了解决办法,引用了一下。
判断是否有当前的文件存在,不存在则进行创建,在进行操作:
但是当我运行到发现没有当前的文件,就直接创建当前文件,之后直接进行操作,出问题了直接报出异常,当前文件正在另一个进程中使用……仔细一看 System.IO.File.Create(fileName)返回的类型是FileStream,ND文件流,文件流不关闭不出异常那才叫怪呢。
方法二:
9.C# 以非独占方式打开文件(FileShare)
使用C#开发中,当一个程序正在读写某个文件,另一个程序则无法操作此文件。
使用FileStream类,其中的FileShare参数可设置文件的共享方式:
- FileShare.None 谢绝共享当前文件
- FileShare.Read 允许别的程序读取当前文件
- FileShare.Write 允许别的程序写当前文件
- FileShare.ReadWrite 允许别的程序读写当前文件
下面代码中将文件的FileShare属性设置为Read,程序在读写文件时,其他程序可以查看此文件
每次以追加的方式向文件中写入数据,并且给本程序赋予读写的权限来操作该文件,但是其他程序只能读取该文件
XML指可扩展标记语言,XML被设计用来传输和存储数据。XML被设计用来结构化、存储以及传输信息。
1.MemoryStream和XmlSerializer配合来序列化和反序列化
输出结果:
输出结果:
这个用的比较多的是在 out 参数后,比如输出的out类型的参数用不到,就可以直接out var _。
1.?符号
2.?.符号
根据字符串的原理,如果进行不断的拼接,将会带来一点性能损耗,原因是每次拼接都会创建新的字符串对象。
如上面代码将会创建大量中间的字符串对象,而最终需要的对象仅仅只有一个字符串。一个优化的方法就是使用 StringBuilder 代替 string 此时能提升不少的性能。
需要将一个 URI 和另一个 URI 拼接如 https://blog.XXX.com/post/123 和 /api/12 拼接,拿到绝对路径 https://blog.XXX.com/api/12 可以使用下面方法:
输出结果:
今日遇到需求如下:查找一个集合类中某个属性的最大值和最小值,想使用Linq来实现。
需求复现代码如下:
代码运行结果如下:
什么时候该用where什么时候该用select。其实也挺简单的,就是假如后面的表达式返回的是true 或者false的bool值的时候就用where,要是后面能直接得到值就用select。
比如,以上代码Select(e=>e.Power)表达式中e=>e.Power返回的是值,因此就用select。
语法糖可以理解为,编码过程中写了一个关键字,但是编译的时候会把它编译成别的东西,主要是用来提升开发效率。
例如,C#中的async和await就是一组语法糖。
当一个集合作为入参传入另外一个方法时,我们首先需要判空处理,以免在空集合上处理引发异常,目前想到了以下几种判空处理的方式:
1.是否为null
如果一个集合没有实例化,那集合就是null,判null的常用以下几种方式:
- == null
- is null
2.是否为空
在上面我们先判断了集合不为null以后,如果还需要检查集合是否有元素,也是有多种方式可以实现:
- stuList.Count == 0
- !stuList.Any() // 确定序列是否包含任何元素。
// 返回结果:
// true 如果源序列中不包含任何元素,则否则为 false。
那如果既想检查他是否为空又想检查是否为null,这时候我们就可以用 ?. 来操作了。
上面那个就是 ?? 的用法。
1.?的使用
- 定义数据类型可为空,目的是用于对 int,double,bool 等无法直接赋值为 null 的类型进行 null 的赋值:
- 用于判断对象是否为 null,如果对象为 null,则不管调用什么都不会抛出异常,直接返回 null:
2.??的使用
- ??用于判断一个变量为 null 返回一个指定的值:
1. 这道题要考察的知识点:

2.问题
先看下面这段代码有没有问题?
3.分析
如果非常清楚属性的本质的话,那么上述代码可以进行转换,将属性转换为普通方法。(属性的本质就是方法嘛)
输出结果:
压根就没变,当然没有变啊,因为v2是副本,你更改的只是副本,并没有改变t中的v,同理,通过t.GetVector()也是一个副本,没有意义。
所以C#语法在对于这种情况,帮我们做了一个处理,如果写了这样的代码,直接给出编译报错。C#还是很智能的。就是如果我们错误的进行一个无意义的操作,会直接给出提示。这里给C#语法赞一个。
4.闲谈
这一段代码虽然好像很简单,但是真的藏的很深。因为枚举值传递是副本传递,再进行赋值操作没有意义。这一道很简单的问题,但是考察的东西真的很多很多。
1.用字符串分隔
运行结果:
2.用多个字符来分隔
运行结果:
3.用单个字符来分隔
运行结果:
1.概述
Image 类为Bitmap(位图) 和 Metafile(矢量图) 的类提供功能的抽象基类。Image类不能直接创建对象的,但Image.FromFile()返回的是Bitmap或者Metafile的对象。
初始化Image:
2.属性
- PixelFormat:获取此 Image 的像素格式。
- RawFormat:获取此 Image 的文件格式。
- Size:获取此图像的宽度和高度(以像素为单位)。
- Width:获取此 Image 的宽度(以像素为单位)。
- Height:获取此 Image 的高度(以像素为单位)。
3.方法
- FromFile(String):从指定的文件创建 Image。
- FromStream(Stream):从指定的数据流创建 Image。
- GetBounds(GraphicsUnit):以指定的单位获取图像的界限。
- GetThumbnailImage(Int32, Int32, Image+GetThumbnailImageAbort, IntPtr):返回此 Image 的缩略图。
- RotateFlip(RotateFlipType):旋转、翻转或者同时旋转和翻转 Image。
- Save(Stream, ImageFormat):将此图像以指定的格式保存到指定的流中。
- Save(String, ImageFormat):将此 Image 以指定格式保存到指定文件。
4.绘制图片
5.缩放
6.获取缩略图
7.旋转
8.双倍缓冲
9.格式转换与保存
使用Export和Import特性标签主要是为了让程序进行解耦。
代码举例如下:
输出结果:
C# Math 类主要用于一些与数学相关的计算。
一些常用的方法如下:
1.ConcurrentDictionary
对于c#中使用的List
ConcurrentDictionary的用法和Dictionary类似。
2.BlockingCollection
1.BlockingCollection简介
多线程操作集合时,ConcurrentQueue 是我常用的,一直用得也挺爽,突然发现了 BlockingCollection,原来还可以更简单。
BlockingCollection ,与经典的阻塞队列数据结构类似,能够适用于多个任务添加和删除数据,提供阻塞和限界能力,它是一个自带阻塞功能的线程安全集合。BlockingCollection
2.常用方法和属性
3.代码举例
先看这一段代码的作用:
BlockingCollection.GetConsumingEnumerable 方法是关键,这个方法会遍历集合取出数据,一旦发现集合空了,则阻塞自己,直到集合中又有元素了再开始遍历。
1.例子1
一个任务往容器里面添加数据,另一个任务把数据从容器中取出,进行处理。
下面的代码很简单,使用BlockingCollection定义一个消息队列,然后使用AddMessage方法向队列中添加消息。重点看一下Process方法,里面写了一个死循环,里面调用BlockingCollection的Take方法,当队列中如果没有消息时,则阻塞队列,所以并不会一直循环。等到有新消息进来时,它就会继续处理。还有一个,我们在这个类中使用单独的线程来作执行Process方法。
输出结果:
1.例子2
还可以将Action委托作为消息放到队列中,这样可以实现一个任务执行器。
输出结果:
3.ConcurrentQueue
线程安全的集合使用Concurrent开头的集合就可以了。 多线程操作集合时,可以考虑用ConcurrentQueue。
1.ConcurrentQueue简介
ConcurrentQueue队列是一个高效的线程安全的队列。
ConcurrentQueue表示线程安全的先进先出 (FIFO) 集合。
经典的多线程应用问题就是:有一个或多个线程(生产者线程)产生一些数据,还有一个或者多个线程(消费者线程)要取出这些数据并执行一些相应的工作。
2.常用方法和属性
- .Net所指的托管只是针对内存这一个方面,并不是对于所有的资源;因此对于Stream,数据库的连接,GDI+的相关对象,还有Com对象等等,这些资源并不是受到.Net管理而统称为非托管资源。而对于内存的释放和回收,系统提供了垃圾回收器(GC-Garbage Collector),而至于其他资源则需要手动进行释放。
- .Net类型分为两大类,一个就是值类型,另一个就是引用类型。前者是分配在栈上,并不需要GC回收;后者是分配在堆上,因此它的内存释放和回收需要通过GC来完成。GC的全称为“Garbage Collector”,顾名思义就是垃圾回收器,那么只有被称为垃圾的对象才能被GC回收。也就是说,一个引用类型对象所占用的内存需要被GC回收,需要先成为垃圾。那么.Net如何判定一个引用类型对象是垃圾呢,.Net的判断很简单,只要判定此对象或者其包含的子对象没有任何引用是有效的,那么系统就认为它是垃圾。对于内存中的垃圾分为两种,一种是需要调用对象的析构函数,另一种是不需要调用的。GC对于前者的回收需要通过两步完成,第一步是调用对象的析构函数,第二步是回收内存,但是要注意这两步不是在GC一次轮循完成,即需要两次轮循;相对于后者,则只是回收内存而已。
- 托管和非托管的资源指的是存储在托管或本机堆中的对象。
- C#编程的一个优点是程序员不需要担心具体的内存管理,垃圾回收器会自动处理所有的内存清理工作。尽管垃圾收集器释放存储在托管堆中的托管对象,但不释放本机堆中的对象。必须由开发人员自己释放它们。
垃圾回收器的出现意味着,通常不需要担心不再需要的对象,只要让这些对象的所有引用都超出作用域,并允许垃圾回收器在需要时释放内存即可。但是,垃圾回收器不知道如何释放非托管的资源(例如,文件句柄、网络连接和数据库连接)。 托管类在封装对非托管资源的直接或间接引用时,需要制定专门的规则,确保非托管的资源在回收类的一个
实例时释放。
在定义一个类时,可以使用两种机制来自动释放非托管的资源。这些机制常常放在一起实现,因为每种机制都为问题提供了略为不同的解决方法。这两种机制是:
- 声明一个析构函数(或终结器),作为类的一个成员;
- 在类中实现System.IDisposable接口;
众所周知,C#在大部分情况下,内存都是由.Net托管的,而有一些特殊的类,它能够进行一些非托管的操作,这通常用于在和c/C++代码进行交互时。Marshal是一个方法集合,主要应用在C#和非托管代码交互时,主要有如下方法:
- 分配非托管内存
- 复制非托管内存块
- 将托管类型转换为非托管类型
- 其他方法(与非托管代码交互时)
使用 Marshal 做出可以快速释放内存的大数组
使用方法:
这行代码会将内存减少到几M左右。
注意:析构函数只能被GC来调用。
.NET里面有一个GC.Collect()吧,它的功能就是强制对所有代进行垃圾回收。
首先举个例子:
.NET CLR中对于大于85000字节的内存既不像引用类型那样分配到普通堆上,也不像值类型那样分配到栈上,而是分配到了一个特殊的称为LOH的内部堆上,这部分的内存只有在GC执行完全回收,也就是回收二代内存的时候才会回收。因此,考虑如下情形:假设你的程序每次都要分配一个大型对象(大于85000字节),但却很少分配小对象,导致2代垃圾回收从不执行,即使这些大对象不再被引用,依然得不到释放,最终导致内存泄漏。示例代码:
以上有三个按钮,Add Buffer、Remove Buffer 和 Force GC。每点一次 Add Buffer 会申请 1 GB 的内存,并添加到 mBuffer 中。每点一次 Remove Buffer 会从 mBuffer 中移除 1 GB 的内存。当点击 Force GC 的时候会强制执行一次完全的垃圾回收。
在C#中,推荐使用System.IDisposable接口替代析构函数。
提示:实现Dispose方法的时候,一定要加上“GC.SuppressFinalize( this )”语句,避免再让GC调用对象的析构函数。
IDisposable接口定义了一种模式(具有语言级的支持),该模式为释放非托管的资源提供了确定的机制,并避免产生析构函数固有的与垃圾回收器相关的问题。
IDisposable接口声明了一个Dispose()方法,它不带参数,返回void。MyClass类的Dispose()方法的实现代码如下:
Dispose()方法的实现代码显式地释放由对象直接使用的所有非托管资源,并在所有也实现IDisposable接口的封装对象上调用Dispose()方法。这样,Dispose()方法为何时释放非托管资源提供了精确的控制。
例如下面的例子展示了析构函数和Dispose方法的调用顺序:
运行结果:
显然,以上代码,在出了Create函数外,myClass对象的析构函数没有被立刻调用,而是等显示调用GC.Collect才被调用。
对于Dispose来说,也需要显示的调用,但是对于继承了IDisposable的类型对象可以使用using这个关键字,这样对象的Dispose方法在出了using范围后会被自动调用。例如如下代码:
运行结果:
那么对于如上DisposeClass类型的Dispose实现来说,事实上GC还需要调用对象的析构函数,按 照前面的GC流程来说,GC对于需要调用析构函数的对象来说,至少经过两个步骤,即首先调用对象的析构函数,其次回收内存。也就是说,按照上面所写的 Dispose函数,虽说被执行了,但是GC还是需要执行析构函数,那么一个完整的Dispose函数,应该通过调用 GC.SuppressFinalize(this )来告诉GC,让它不用再调用对象的析构函数中。
例子如下:
运行结果:
显然加上了GC.SuppressFinalize(this)后,对象的析构函数没有被调用。
1.内存溢出和内存泄漏简介
内存溢出:通俗理解就是内存不够,系统中存在无法回收的内存或程序运行要用到的内存大于能提供的最大内存,从而造成“Out of memory”之类的错误,使程序不能正常运行。
内存泄露:内存泄漏指由于疏忽或错误造成程序不能释放或不能及时释放已经不再使用的内存的情况,是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存不能回收和不能及时回收,最后可能导致程序运行缓慢或者崩溃的问题。
2.使用未退订的event事件造成的内存泄漏
注意当使用+=来订阅事件的时候,在不用的时候要用-=来取消订阅,这样才能取消对事件的引用以减少内存泄漏现象。
3.静态变量
静态变量中的成员所占的内存,如果不手动处理是不会释放内存的,单态模式的对象也是静态的,所以需要特别注意。因为静态对象中的成员所占的内存不会释放,如果此成员是以个对象,同时此对象中的成员所占的内存也不会释放,以此类推,如果此对象很复杂,而且是静态的就很容易造成内存泄露。
C#中使用AppDomain.CurrentDomain.BaseDirectory来获取程序的运行路径。例如:
输出结果:

到此这篇pch头文件(头文件conio.h)的文章就介绍到这了,更多相关内容请继续浏览下面的相关推荐文章,希望大家都能在编程的领域有一番成就!首先项目要引用System.Management库。
版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权、违法违规、事实不符,请将相关资料发送至xkadmin@xkablog.com进行投诉反馈,一经查实,立即处理!
转载请注明出处,原文链接:https://www.xkablog.com/cjjbc/34680.html
