(七)c#高级编程
1. struct结构体
- 是一种值类型数据类型
![image]()
- 和class一样也可以创建对象
- 使用结构体来声明和初始化对象的时候,我们不需要使用new关键词。
- 直接在声明对象完成以后,对象就会被同时创建完成,并且在内存中分配了相应的内存。
- 同样可以使用对象的链式调用方式来给结构赋值
- 结构体是从c++流传下来的一种比较特殊的语法结构,而类似java和python之类的高级语言已经抛弃这种语法了。

对于一个坐标点点、矩形和颜色这样的轻量级的对象,我们可以通过使用结构体保存在栈内存中,这样,运行效率会有明显的提升,所以使用结构的成本会比较低。
而如果我们需要使用到抽象的概念、或者需要多个层级来表现对象关系的时候,类class则是我们最好的选择,因为结构的保存方式注定了它不能支持过于复杂的结构,所以,结构不支持继承、不支持抽象。
1.1 特点
• 可带有方法、字段、索引、属性、运算符方法和事件。
• 结构不能定义无参的默认构造方法,因为默认构造函数是结构体预定一的,无法更改。
○ 不过,除了无参默认构造方法,我们可以定义其他的有参数的构造函数。
• 结构可实现接口,但它不能继承,也不能被继承。
○ 因为无法继承,所以我们也不能在结构中使用 abstract、virtual、与 protected等关键词。
• 我们可以使用new来创建结构对象,不过,不用new同样也可以创建结构实例。
○ 如果不使用 New 操作符,只有在所有的字段都被初始化、被赋值以后,对象才能被使用。
1.2 代码
struct Game
{
public string name;
public string developer;
public DateTime releaseDate;
//有参构造要包含所有字段,且初始化
public Game(string name, string developer, DateTime releaseDate)
{
this.name = name;
this.developer = developer;
this.releaseDate = releaseDate;
}
public void GetInfo()
{
Console.WriteLine($"游戏名:{name}");
Console.WriteLine("developer:"+developer);
Console.WriteLine("date:"+ releaseDate);
}
}
main
Game game;
//没有初始化成员变量是不可以访问他的方法
//但是类可以,结构体不可以为null
game.name = "xiaoqiao";
game.developer = "huihui";
game.releaseDate = DateTime.Today;
game.GetInfo();
2.枚举
枚举的就是“一一列举”的意思,在计算机领域,被称作enumerate,可以表示一个常数的集合,比如,表示一周7天、从周一到日,MONDAY、TUESDAY、WEDNESDAY、THURSDAY、FRIDAY、SATURDAY、SUNDAY。这就是一个枚举集合。
-
enum所声明的集合中预定义的数据是不可改变了,所以,他是常量,constant。
-
一般来说,每个元素在底层实现上会使用他的位置信息作为内存中所保存的真实数据,所以,从原理上来说,默认情况下的enmu中每个元素的类型为常量整数。
![image]()
![image]()
-
使用等号给枚举成员赋值的时机很重要,c#会根据上一次赋值通过累加位置数据来计算接下来的每一个枚举成员的数据。
-
枚举类型很适合与switch语句配合使用。
Weekday day = Weekday.MONDAY;
switch(day)
{
case Weekday.MONDAY:
case Weekday.TUESDAY:
case Weekday.WEDNESDAY:
case Weekday.THURSDAY:
case Weekday.FRIDAY:
Console.WriteLine("今天要上班");
break;
case Weekday.SUNDAY:
case Weekday.SATURDAY:
Console.WriteLine("家里蹲");
break;
}
3. 泛型

public class GenericList<T>
{
public void Add(T order)
{
throw new NotImplementedException();
}
public T this[int index]
{
get { throw new NotImplementedException(); }
}
}
3.1 简介
- 当我们使用object存储一个值类型数据的时候,会发送装箱操作,把值数据转化为引用类型,当通过索引输出时,又会发生拆箱操作,如此频繁的装箱与拆箱会导致性能问题。
- 而泛型可以做到一个类,一次创建到处使用的效果,同时也不存在性能上的缺陷
- 泛型要求带有参数,用<>存放,约定俗成用T表示参数,实际上是个占位符
- 工作中我们很少使用自己创建的类,使用c#自带的
System.Collections.Generic.List<> - 也包含很多其他泛型类
![image]()
3.2 进阶
-
可以指定多重泛型参数
-
如双参数字典类型,用希哈表,希哈值通过键值对关系高效访问数据
-
绝大多数数据库实现原理就是这种键值对关系
-
key,value可以用任意类型表示
![image]()
-
可以通过传递接口限制泛型,基于方法处理泛型也可以,不一定要基于类处理
![image]()
⬇
//where T:IComparable
//where T:class(product,book)
//where T:struct
//where T:new()
public class Utility<T> where T :IComparable,new()
{
public void DoSomething()
{
var obj =new T();
}
public int FindMax(int a ,int b)
{
return a>b?a:b;
}
public T FindMax(T a ,T b)
{
return a.CompareTo(b)>0?a:b;
}
}
在c#中值对象是不可以被赋值为null的,编译器初始化之前会给int,float等直接赋值为0,但是很多情况我们需要判断如int类型赋值为null看是否存在,以下方法用来解决这个问题
public class Nullable<T> where T : struct
{
//创建一个内部私有变量来存储泛型数据
private object _value;
//无参构造,创造一个value值为空的对象
public Nullable() { }
//通过有参构造来创建带有真实数据的value
public Nullable(T value)
{
_value = value;
}
//属性:判断value是否有值
public bool HasValue
{
get { return _value != null; }
}
//创建读取value的方法
public T GetValueOrDefault()
{
if (HasValue)
{
return (T)_value;//如果存在返回T类型的数据5
}
return default(T);
//默认值指的是该类型只有声明没有初始化时候的值,整数,浮点数等就是0
//布尔值为false
}
}
如何使用:
Nullable<int> number = new Nullable<int>();
//可以调用方法检查
Console.WriteLine(number.HasValue);
Console.WriteLine(number.GetValueOrDefault());
//如果带参
var number2 = new Nullable<int>(5);
//值为5,可以验证
Console.WriteLine(number2.GetValueOrDefault());
4. Nullables处理


c#在system命名空间下已经自带了nullbale,所以不需要自定义
//Nullable<DateTime> date = null;
DateTime? date= null;//自己的语法糖
测试:
Console.WriteLine(date.GetValueOrDefault());
Console.WriteLine(date.HasValue);
Console.WriteLine(date.Value);//无法输出报错

??运算符:当左侧运算符的数据为null就返回右侧数据

DateTime? date = new DateTime(2023, 1, 1);
DateTime date2 = date.GetValueOrDefault();
//没有方法获取,不可以把对象类型直接赋值给值类型
DateTime? date3 = date2;//数据可以赋值给对象
if (date3 != null)
{
Console.WriteLine(date3.GetValueOrDefault());
}
else
{
Console.WriteLine(DateTime.Today);
}
if等同于下面
var result = date3 ?? DateTime.Today;
Console.WriteLine(result);
5.拓展方法

对于这个不能被继承的字符串类,我们有什么办法来实现他的功能增强呢?
- 通过静态拓展,在不改变string本身代码的前提下来实现这个功能。
//切个字符串使用 str.Substring, 从第0位直到第number位。
public static class StringExtension {
public static string ShortTerm(this string str, int num)
{
return str.Substring(0, num);
}
}
hello.ShortTerm(2); // He
使用IEnumerable 来创建一个列表。
IEnumerable list = new List<int>();
我们使用using关键词引入linq的命名空间。再次调用 list 点的时候,我们会发现这个IEnumerable 的功能一下子就丰富起来了。不仅包含了原有的5个功能,还包含了linq对列表的语法增强。
IEnumerable简单介绍
IEnumerable是可枚举类型,一般在迭代时应用广泛,如foreach中要循环访问的集合或数组都实现了IEnumerable接口。只要能够遍历,都直接或间接实现了IEnumerable接口。如:String类型的对象,可遍历,输出时以字符输出,间接实现了IEnumerable接口,"OOP"遍历打印就是'O','O','P';又如int类型没有实现IEnumerable接口,就无法依赖foreach遍历。
IEnumerable只有一个抽象方法:GetEnumerator(),而IEnumerator又是一个迭代器,真正实现了访问集合的功能。 IEnumerator只有一个Current属性,MoveNext和Reset两个方法。
6.dynamics 动态类型
程语言可以被分为两种类型,静态类型语言和动态类型语言。不过,静态语言和动态语言有什么区别呢?
- 静态类型语言会在编译的时候检查数据类型,如果数据类型不匹配,那么编译会失败,程序也无法运行。也就是说对于静态语言来说,我们在写代码的时候就要要声明所有变量的数据类型。c++、java、c# 是静态语言的典型代表。
- 而动态类型则相反,在写代码的时候完全不需要考虑数据类型。直到程序运行起来,数据类型才会被确定下来。像JavaScript这样的解释性脚本语言则是动态类型的典型代表,除此以外python也属于动态类型语言。
![image]()

这段代码在c#中肯定是运行不了的,因为2是整数,而3是字符串,我们没办法把字符串和数字做乘法运算。无法在运行过程中处理动态的类型转换,这就是强类型,他可以提供类型的绝对安全。
但是这段代码在JavaScript中却是合法的,输出结果为6。因为JavaScript在运行的过程中会把字符串“3”自动转化为数字3,然后再进行乘法计算。


6.1 如何实现
例子:
public class Excel
{
public string Table { get; set; }
public void ShowTable()
{
Console.WriteLine("打印excel表格");
}
}

第一种解决思路,我们可以通过反射来调用ShowTable方法。
- 获得obj对象的类型,GetType();
- 通过方法的字符串名称来获取引用关系,GetMethod(“ShowTable”);
- 返回一个方法信息对象,methodInfo object
var methodInfo = obj.GetType().GetMethod("ShowTable");
methodInfo.Invoke(excel, null);
- 通过methodInfo 来 invoke、来调用这个方法了。方法内第一个参数传入excel对象,第二个参数传入null,表示没有showtable方法不需要参数。
- 使用反射,会令到我们的代码繁琐同时降低可读性。
- 用 dynamic 关键词来代替object类型,现在,obj.ShowTable()的报错就消失了。
- 相较于反射,使用对象的链式操作,代码会更加简洁,更具有可读性。
在c#的底层运行中,代码将会被转化为中间语言IL,intermidient language,然后,运行在.net 虚拟环境 CLR 中。而在.net环境中有一个组件叫做DLR,dynamic language runtime,而正是这个组件提供了动态类型的编译和处理能力。
var和dynamic有本质上的区别
-
对于关键词var,可以代替我们程序猿来自动做出类型的判断,这样,我们程序猿在写代码的时候就可以少些几个字了。从本质上来说,使用var则代表我们的对象类型能够在编译前、写代码的时候100%的确定下来。
-
而使用dynamic则表示不到程序运行到这行代码的时候,我们都不能确定所声明的变量的类型。
![image]()
使用dynamic
![image]()
-
这样对类型无所谓的态度,必然会影响到系统的稳定性
7.反射元与元数据
7.1 反射
- 反射机制是.net 框架的功能,而不是C#语言的本身功能。
- 只要有.net框架,无论你使用什么编程语言都可以进行反射,甚至可以实现语言的混合编译,比如说使用c#开发,但我们却可以调用基于c++或F#的类库插件。
- c#中的反射,指的就是我们通过反射系统,在不使用new关键词并且在不知道目标对象类型的情况下,仅仅通过对象的名称、创建一个一模一样的实例的过程。
- 我们甚至可以通过反射机制来访问、甚至操作这个对象。就好像我站在镜子前面挠一下头、踢一下腿,而镜子里的那个我也会执行形同的动作
![image]()
7.2 元数据metadata

一般来说,我们的程序就是用来处理数据的,比方说,数据库的数据、存在文件上的数据;但是,别忘了,我们的程序本身,其实也同样是一种数据的表现形式。

所以,程序运行的过程,就是是数据在处理数据。而描述程序数据本身的数据,或者说有关程序以及它类型的数据,我们就称为元数据。

在运行的过程中查看或者使用元数据的过程就叫做反射。
7.3 反射的过程
程序经过编译以后,元数据就会报存在程序集 assambly 中。

代码:
创建一个列表类,list class。这个列表类中有个添加元素的方法,add,载这个方法中,我们随便打印点东西。
public class List
{
public void Add()
{
Console.WriteLine("ddddddd");
}
}
接下来,回到main方法。基于c#的反射机制,程序中所有对象的类型结构说明,都会以 System.Reflaction 命名空间中的 Type 类型来进行保存。
如果我们知道一个class的项目名称、命名空间等信息,我们就可以使用这个类的字符串名称在程序运行的时候动态的获得它的类型。
- 如果我们知道一个class的项目名称、命名空间等信息,我们就可以使用这个类的字符串名称在程序运行的时候动态的获得它的类型。
string classLocation = "CsharpStudyReflection.List,CsharpStudyReflection"; - 使用 Type.GetType 方法,然后再参数中把关于这个类的说明信息、或者叫做元数据metadata传递进来,然后就可以得到对象的动态类型了。
Type objectType = Type.GetType(“命名空间.类名, 项目名称”); - 我们获得动态类型以后,便可以使用反射类型 Activator 来实例化对象了。对象初始化完以后是一个object 类型,我们可以把他的输出保存起来。
object obj = Activator.CreateInstance(objectType); - 在取得实例化对象以后,接着就可以使用他的方法的名称字符串,通过methodinfo类来调用执行了。
MethodInfo method = objectType.GetMethod("方法名"); - 怎么去run这个add方法呢?很简单。method.Invoke 调用,在哪个对象上调用?就是在我们的这个obj对象上调用。因为这个add方法是没有参数的,所以,Invoke方法传入参数null。
method.Invoke(obj, null);
7.4 特点

一旦我们使用new来实例化对象的时候,对象后面必须跟着对象类型,是吧。只要你一跟类型,系统就有依赖了,而且这种依赖还是紧耦合。
使用 ServiceCollection 取得ioc容器,然后把需要使用的组件插入容器中。ioc容器会自动通过反射获得组件的动态类型,完成类的自动实例化,实现组件之间的解耦。

7.5 为什么使用反射
某些业务逻辑并不是我们在写程序的时候就能确定的,相反的是在程序与用户交互的过程中才能够被确定下来。
比如说,系统可能会通过插件进行功能拓展,而插件是即插即用的。那么这个时候,请同学们注意了,程序实际上已经处于运行的状态了。而插件就是在动态运行的过程中通过反射机制来执行的。
所谓的动态指得就是程序的运行过程,而相对于的静态则指我们写代码、处理编译的过程。
当我们在写程序的时候,也就是在静态环境中预测用户的行为,实际上是一个非常困难的过程。

我们可以想象一下,如果在给用户行为建模的时候,使用枚举的方式来穷举用户的所有行为,那么我们的程序将会变得非常臃肿,非常难以维护。而更重要的是,在大多数情况下,我们根本没法预测用户的行为。
7.6 应用场景

8.实战
因为电脑、u盘、固态硬盘的生成厂家很有可能是三个完全不同的厂商。所以,他们之间唯一可以遵循的标准就是USB接口。一般来说,接口协议是产业链中多个厂家坐在一起商量出来的。而为了让下游厂家的开发更加方便,一般来说,上游厂家都会将接口进行二次封装,制作做一个sdk给下游厂商使用。比如说,我们开发ios应用,那苹果肯定会给我们提供一整套的开发方案,这就是sdk,software development toolkit。
代码在CsharpStudy07
9.异常
程序崩溃其实是开发过程中最糟糕情况,我们必须要避免。

异常信息显示,system.DivideByZeroException。这个 exception 异常信息是在system命名空间下的,在.net中,它同样依然是一个对象。
而exception的详细信息,我们会把这部分信息称作Stack Trace,栈回溯、或栈追踪。他会通过回溯的方式告诉我们,就像剥洋葱一样,一层一层向内核代码回溯,直到找到当前抛除异常的方法为止。
比较常用的信息:
- message 就是异常信息的文字提示,一般来说,不同类型的异常所提供的异常信息是不一样的。
- source 就是产生异常的应用名称,有时候你的程序可能包含不同的assmbly、不同的程序集,需要使用不同的dll。所以,source 可以帮助我们精确识别出异常的输出的程序集。
- stack trace,我们刚刚讲过了,这里会一层一层罗列call stack中异常的源头,以及异常被抛出后的路径。
- targeSite,就是异常发生的方法
![image]()
9.1 处理方法
把有可能爆出异常的代码放入 try block
在catch block中,一般来说,我们有两个选择。第一,从异常状态中恢复过来,防止程序崩溃,也就是直接在这里把异常处理掉。第二个选择就是,使用throw关键词,向更上一层的代码抛出异常,让更上一层的调用方来处理。
9.2 多重catch block
这个异常的名称就叫做DivideByZeroException,这是一个类,请同学们按下F12,可以打开对象浏览窗口。
可以看到它继承于 ArithmeticException 类,而ArithmeticException 又继承于 SystemException ,SystemException 又继承于 Exception。这个Exception则是所有异常的基类,所有的异常都可以通过Exception来处理。
对于DivideByZeroException来说,我们看到了4个层次的级别关系。所以,从集合关系上来说,Exception > SystemException > ArithmeticException > DivideByZeroException
所以,对于多重异常处理关系来说,我们首先要处理的应该是最小的子集,也就是DivideByZeroException。
然后,才是更大的集合,比如说ArithmeticException 。而最后,我们还可以使用最普通的Exception 来涵盖所有的处理类型,以免遗漏。
try
{
var calculator = new Calculator();
var result = calculator.Divde(5, 0);
//DivideByZeroException
}
//因为c#的语言之行顺序总是由上到下的,所有,越小的子集越应该放在多重catch的首位。
catch(DivideByZeroException ex)
{
Console.WriteLine("分母不能为0");
}
catch (ArithmeticException ex)
{
Console.WriteLine("算术异常");
}
catch (Exception ex)
{
Console.WriteLine("系统异常");
}
9.3 finally
被finally包裹起来的代码将不会受到try catch的限制,无论系统有没有抛出异常,无论系统执行的是try block中的代码还是执行catch中的代码,使用finally包裹起来的代码将总会在try catch结束以后执行。
在程序开发过程中,我们总是会访问一些外部资源文件,比如说打开一个文件、连接数据库、或者获取网络连接等等。而这些外部资源是不被CLR所管理的,c#的垃圾回收机制没办法对这些外部起作用。所以,如果系统抛出异常打断了正常的程序执行流程,我们就需要在finally block中手动对这些资源进行回收。













浙公网安备 33010602011771号