初识
类与命名空间
- 类:构成程序的主体
- 命名空间以树形结构组织类和其他类型 (类似C的头文件)
类库的引用:
-
类库引用时使用命名空间的物理基础
使用不同技术类型的项目会默认引用不同的类库
-
DLL(Dynamic Link Library)引用 (黑盒引用,无源代码) -
Nuget :个人理解为类似Maven的一个依赖管理工具
-
项目引用 (白盒引用,有源代码)
依赖关系
- 类(或对象)之间的耦合关系
- 优秀的程序 "低耦合,高内聚."
- UML(通用建模语言)类图
🚟类
类是对现实世界事物进行抽象所得到的结果
- 事物包括"物质"(实体)与"运动"(逻辑)
- 建模是一个去伪存真,由表及里的过程
类与对象的关系
- 对象也叫实例,是类经过"实例化"后得到的内存中的实体
- Formally "instance" is synonymous with "object" ---对象和实例是一回事
- 有些类是不能实例化的
- 依照类,我们可以创建对象,这就是"实例化"
- 使用new操作符创建类的实例
- 引用变量与实例的关系
- 孩子与气球
- 气球不一定有孩子牵着
- 多个孩子可以使用各自的绳子牵着同一个气球,也可以通过同一根绳子牵着气球
类三大成员
- 属性(property)
- 存储数据
- 方法(Method)
- 事件(Event)
- 类或对象通知其他类或对象的机制**,C#特有** (Java通过其他办法实现这个机制)
- 善用机事件机制特别重要
静态成员与实例成员
- 静态(static)成员在语义上表示它是"类的成员"
- 实例(非静态)成员在语义上表示他是"对象的成员"
- 绑定(binding)指的是编译器把一个成员与类或对象关联起来 (操作符
.成员访问)
🚈类型、变量与对象(*)
什么是类型?
-
类型又名数据类型(Data Type)
-
是数据在内存中存储时的型号
-
小内存容纳大尺寸数据会丢失精度,发生错误 (高精度转低精度)
-
大内存容纳小尺寸数据会导致浪费
-
编程语言的数据类型与数学中的数据类型不完全相同
-
-
强类型语言与弱类型语言的比较
- C语言:if条件
- JS:动态类型
- C#语言对弱类型/动态类型的模仿
类型在C#语言中的作用
- 存储此类型变量所需内存空间的大小
- 此类型的值可能表示的最大最小值范围
- 此类型所包含的成员(方法,属性,事件等) 反射
- 此类型由何基类派生
- 程序运行时,此类型的变量分配在内存的什么位置?
- Stack简介 (存方法)
- Stack overflow 栈溢出
- Heap简介 (存对象)
- 使用Performance Monitor查看进程的堆内存使用量
- 关于内存泄漏
- 此类型所允许的操作(运算)
C#语言的类型系统
- C#类型派生谱系
- Object
- 引用类型
- 类
- 接口
- 委托
- 值类型
- 结构体
- 枚举
- 引用类型
- Object
变量、对象与内存
什么是变量?
-
表面上看,变量的用途时存储数据
-
实际上, 变量表示了存储位置,并且每个变量都有一个类型,以决定什么样的值能够存入变量
-
变量一共有7中
静态变量,实例变量(成员变量,字段),数组元素,值参数,引用参数,输出形参,局部变量
-
狭义的变量指局部变量,因为其他种类的变量都有自己的约定名称
- 简单地讲,局部变量就是方法体里声明的变量
-
变量的声明
有效的修饰符组合 类型 变量名 初始化器
变量---以变量名所对应的内存地址为起点,以其数据类型所要求的存储空间为长度的一块内存区域
值类型的变量---值类型没有实例,所谓的实例与变量合而为一
引用类型的变量与实例关系---引用类型变量里存储的是对象的内存地址
常量---值不可变的变量
装箱与拆箱
- 装箱:将值类型转换为引用类型
- 拆箱:引用类型转换为值类型
🏎️方法的定义,调用与调试
方法的由来
- 方法的前身是C/C++的函数---方法是面向对象范畴的概念,在非面向对象语言中仍然被称为函数
- 永远都是类(或是结构体)的成员
- C#语言中函数不可能独立于类(或结构体)之外
- 只有作为类(或结构体)的成员时才被称为方法(C++中可以有全局函数)
- 是类(或结构体)最基本的成员之一 --- 最基本的成员只有两个---成员变量与成员方法
- 为什么需要方法和函数
- 隐藏复杂的逻辑
- 复用 reuse
构造器
- 构造器(constructor)是类的成员之一
- 狭义的构造器指的是"实例构造器(instance contractor)"
如何对方法进行debug
- 设置断点 break point --- 程序执行到断点会暂停执行
- 观察方法调用时的call stack --- 断点附近执行顺序
- Step-in Step-over Step-out
- 观察局部变量的值与变化
方法的调用与栈 *
与CPP压栈方法类似
🤷♂️操作符
操作符优先级
操作符(Operator)是用来操作数据的,被操作符所操作的数据称为操作数 (Operand)
优先级自上而下递减
| 类别 | 运算符 |
|---|---|
| 基本 | x,y f(x) a[x] x++ x-- new typeof default checked unchecked delegate sizeof -> |
| 一元 | + - ~ ! ++x --x (T)x await &x *x |
| 乘除 | + / % |
| 加减 | + - |
| 移位 | >> << |
| 关系和类型检测 | < > <= >= is as |
| 相等 | ` != |
| 逻辑与 | & |
| 逻辑XOR | ^ |
| 逻辑OR | | |
| 条件AND | && |
| 条件OR | || |
| null 合并 | ?? |
| 条件 三目 | ?: |
| 赋值和 lambda 表达式 | = *= /= %= += -= <<= >>= &= ^= |= => |
- 可以通过圆括号提升内部表达式优先级
- 圆括号可以嵌套
同级操作符运算顺序
- 除了带有赋值功能的操作符 同优先级操作符都是由左向右进行运算
- 带有赋值功能的操作符都是 从右向左运算
类型转换
as的使用
A as B 如果A是B类型则转换类型 如果不是则返回NULL
隐式(implicit)类型转换
- 不丢失精度的类型转换
- 子类向父类的转换
- 装箱
显式(explicit)类型转换
- 有可能丢失精度(甚至发生错误)的转换,即cast
- 拆箱
- 使用Convert类
常用类型转换工具类 - ToString方法与各数据类型的Parse/TryParse方法
自定义类型转换操作符
💻字段、属性、索引器、常量
字段
什么是字段?
- 字段是一种表示与对象或类型关联的变量
- 字段是类型的成员( 成员变量 )
- 与对象关联的字段也称为 "实例字段"
- 与类关联的字段称为"静态字段",由static修饰
字段的初始值
- 无显式初始化时,字段获得其类型的默认值,所以字段永远都不会未被初始化.
- 实例字段初始化的时机---对象创建时
- 静态字段初始化的时机---类被加载(load)时
属性
什么是属性?
-
属性(property)是一种用于访问对象或类型的特征的成员,特征反映了状态
-
属性是字段的自然拓展
- 从命名上看,field更偏向于对实例对象在内存中的布局,property更偏向于反映现实世界对象的特征
- 对外:暴露数据,数据可以是存储在字段里的,也可是动态计算出来的
- 对内:保护字段不被非法值"污染"
-
属性由 GET/SET方法对进化而来
public int Age { get => age; set => age = value; } -
又一个"语法糖"---属性背后的秘密
属性的声明
- 完整声明--后台(back)成员变量与访问器(注意使用code snippet和refactor工具)
- 简略声明--只有访问器(查看IL代码)
- 动态计算值的属性
- 注意实例属性和静态属性
- 属性的名字一定是名词
- 只读属性--只有Getter没有Setter(几乎没有人使用)
属性与字段的关系
- 一般情况下,他们都用于表示实体的状态
- 属性大多数情况下是字段的包装器(wrapper)
- 建议:永远使用属性(而不是字段)来暴露数据,即字段永远都是private或protected的
索引器---使对象能够像数组相同的方式(下标)进行索引
常量
什么是常量?
- 常量(constant)表示常量值(即,可以在编译时计算的值)的类成员
- 常量隶属于类型而不是对象,即没有"实例常量"
- "实例常量"的角色由只读实例字段来担当
- 注意区分成员常量与局部常量
"只读"的应用场景
- 为了提高程序可读性和执行效率--常量
- 为了防止对象的值被修改--只读字段
- 向外暴露不允许修改的数据--制度属性(静态或非静态),功能与常量有一些重叠
- 当希望成为常量的值其类型不能被常量声明接受时(类/自定义结构体)--静态只读字段
🛒参数传递
传值/输出/引用/数组/具名/可选参数,拓展方法(This参数)
传值参数
- 值参数创建变量的副本
- 对值参数的操作永远不会影响变量的值
传值参数 -> 值类型

传值参数 -> 引用类型,并且新创建对象
- 创建新对象,没有对原对象进行操作,所以原对象的值并不会被改变;

传值参数 -> 引用类型,只操作对象,不创建新对象
- 引用相当于自己指向传进来对象的地址

引用参数
引用参数 -> 值类型
- ref修饰的参数 如
ref int x; - 引用参数并不创建变量的副本,指向传入值的地址,修改后原值也会改变!!

引用参数 -> 引用类型,创建新对象
- 指向对象在堆内存中的地址
- hashcode改变 因为创建了新对象

引用参数 -> 引用类型,不创建新对象只改变对象值
- hashcode没有改变,因为没有创建新对象
- 此情形与传值效果一样,但是实现方式(内存机理)不一样
- 传值方式: 两个变量都划分了内存空间,且存储了相同的地址
- 引用方式: 只有一个变量划分了内存空间,两个变量指向同一个内存地址.

输出参数
- out修饰,通过参数向外输出值
- 从语义上看,
out是为了输出,ref是为了引用 - 输出形参不创建新的存储位置
- 变量在可以作为输出形参传递之前不一定需要明确赋值
- 在方法返回之前,该方法的每个输出形参都必须明确赋值
输出参数 -> 值类型

输出参数 -> 引用类型

数组参数
- 必须是形参列表最后一个,由params修饰
public static void Main(string[] args)
{
int res = CalculateSum(1, 2, 3, 4, 5);
Console.WriteLine(res);
}
static int CalculateSum(params int[] intArr)
{
int sum = 0;
foreach (var item in intArr)
{
sum += item;
}
return sum;
}
具名参数
- 参数的位置不再受约束
- 提高代码可读性
public static void Main(string[] args){
PrintInfo(name: "Aowu", age: 22);//具名参数
}
static void PrintInfo(string name, int age){
Console.WriteLine("Name={0} ,Age={1}",name,age);
}
可选参数
- 参数因为具有默认值而变得"可选"
- 不推荐使用
public static void Main(string[] args)
{
PrintInfo();//为空
}
static void PrintInfo(string name="Wuhu", int age=21)
{
Console.WriteLine("Name={0} ,Age={1}",name,age);
}
拓展方法(this参数)
- 方法必须是公有的,静态的,即被
public static修饰 - 必须是形参列表的第一个,由
this修饰 - 必须由一个静态类(一般命名为SomeTypeExtension)来统一收纳对SomeType类型的拓展方法
class Program
{
public static void Main(string[] args)
{
double x = 3.1415926;
double y = x.Round(3); //只有一个参数
Console.WriteLine(y);
}
}
static class DoubleExtension
{
public static double Round( this double input/*this 修饰*/,int digits)
{
double result = Math.Round(input, digits);
return result;
}
}
🤝委托
什么是委托?
- 委托(delegate)是函数指针的"升级版"
- 一切皆地址
- 变量(数据) 是以某个地址为起点的一段内存中所存储的值
- 函数(算法) 是以某个地址为起点的一段内存中所存储的一组机器语言指令
- 直接调用与间接调用
- 直接调用: 通过函数名来调用函数,CPU通过函数名直接获得函数所在地址并开始执行->返回
- 间接调用: 通过函数指针来调用函数,CPU通过读取函数指针存储的值获得函数所在地址并开始执行->返回
- Java没有与委托相对应的功能实体(Java禁止程序员直接访问内存地址)
- 简单使用
- Action委托 (无返回值)
- Func委托 (可有返回值)
class Program
{
public static void Main(string[] args)
{
Calculator calculator = new Calculator();
Action action = new Action(calculator.Report);
calculator.Report();//直接调用
action.Invoke();//间接调用
action();//同上 间接调用
Func<int, int, int> add = new Func<int, int, int>(calculator.Add);
Func<int, int, int> sub = new Func<int, int, int>(calculator.Sub);
Console.WriteLine(add.Invoke(1,2));
Console.WriteLine(sub.Invoke(2,1));
Console.WriteLine(add(1,2));
Console.WriteLine(sub(2,1));
}
}
class Calculator
{
public void Report()
{
Console.WriteLine("I have 3 methods!");
}
public int Add(int a, int b)
{
return a + b;
}
public int Sub(int a, int b)
{
return a - b;
}
}
委托的声明
- 委托是一种类,类是数据类型,所以委托也是一种数据类型.
- 它的什么方式与一般的类不同,主要是为了照顾可读性和C/C++传统
- 注意声明委托的位置
- 委托所封装的方法必须"类型兼容"
- 返回值的数据类型一致
- 参数列表数据类型一致
public delegate double Calc(double a, double b);//public:公有 delegate:委托 double:目标方法返回值类型 Calc:名 (...):参数列表
class Program
{
public static void Main(string[] args)
{
Calculator calculator = new Calculator();
Calc add = new Calc(calculator.Add);
Calc sub = new Calc(calculator.Sub);
Calc mul = new Calc(calculator.Mul);
Calc div = new Calc(calculator.Div);
Console.WriteLine(add(1,3));
Console.WriteLine(sub(1,3));
Console.WriteLine(mul(1,3));
Console.WriteLine(div(1,3));
}
}
class Calculator
{
public double Add(double a, double b)
{
return a + b;
}
public double Sub(double a, double b)
{
return a - b;
}
public double Mul(double a, double b)
{
return a * b;
}
public double Div(double a, double b)
{
return a / b;
}
}
委托的使用
实例:把方法当作参数传递给另一个方法
- 用法1: 模板方法,借用指定的外部方法来产生结果
- 常位于代码中部
- 有返回值
- 用法2: 回调方法(callback),调用指定的外部方法
- 常位于代码末尾
- 无返回值
缺点:
- 一种方法级别的耦合,现实工作中要慎之又慎
- 使可读性下降,debug难度增加
- 把委托回调,异步调用和多线程纠缠在一起,会让代码变得难以阅读和维护
- 委托使用不当可能会造成内存泄漏和程序性能下降
委托高级使用
多播委托 : 一个委托内封装着不止一个方法
Action action1 = new Action(student1.DoHomework);
Action action2 = new Action(student2.DoHomework);
Action action3 = new Action(student3.DoHomework);
action1 += action3;
action1 += action2;
action1.Invoke(); //多播 依次执行 1 ,2 ,3
隐式异步调用:
同步与异步 :
- 同步:你做完了我在你基础上接着做
- 异步:你我同时做
同步调用与异步调用对比
- 每一个运行的程序是一个进程(process)
- 每个进程可以有一个或多个线程(thread)
- 同步调用是在同一线程内
- 异步调用的底层机理是
多线程 - 串行
同步单线程,并行异步多线程
隐式多线程vs显式多线程
-
直接同步调用:使用方法名
-
间接同步调用:使用单播/多播委托的
Invoke方法 -
隐式异步调用:使用委托的
BeginInvokeaction1.BeginInvoke(null, null); -
显式异步调用:使用Thread或Task
Thread thread1 = new Thread(new ThreadStart(student1.DoHomework));//Thread 方式实现多线程 thread1.Start(); Task task1 = new Task(new Action(student1.DoHomework));//Thread 方式实现 task1.Start();
应当适时使用接口(interface)代替一些委托的使用
Java完全地使用接口取代了委托的功能,即Java没有与C#中委托相对应的功能实体
class Program
{
public static void Main(string[] args)
{
IProductFactory pizzaFactory = new PizzaFactory();
IProductFactory toycarFactory = new ToycarFactory();
WrapFactory wrapFactory = new WrapFactory();
Box box1 = wrapFactory.WrapProduct(pizzaFactory);
Box box2 = wrapFactory.WrapProduct(toycarFactory);
Console.WriteLine(box1.Product.Name);
Console.WriteLine(box2.Product.Name);
}
}
interface IProductFactory
{
Product Make();
}
class PizzaFactory:IProductFactory
{
public Product Make()
{
Product product = new Product();
product.Name = "Pizza";
return product;
}
}
class ToycarFactory:IProductFactory
{
public Product Make()
{
Product product = new Product();
product.Name = "ToyCar";
return product;
}
}
class WrapFactory
{
public Box WrapProduct(IProductFactory productFactory)
{
Box box = new Box();
Product product = productFactory.Make();
box.Product = product;
return box;
}
}
🧑💻事件
初步了解事件
对象能够通过事件通知其他对象
-
事件的订阅者=事件消息的接收者=-事件的响应者=事件的处理者=被事件通知的对象
-
事件消息=事件信息=事件数据=事件参数****
定义: Event, -- 能够发生什么事
角色: 使对象或类具备通知能力的成员
使用:用于对象或类间动作协调与信息传递(消息推送)
原理: 事件模型(event model) 中两个 5
-
"
发生->响应"中的五个部分- 闹钟响了你起床 ...............这里隐含"订阅"关系
-
"
发生->响应"中的五个动作-
我有一个事件
-
一个人或一群人关心我的这个事件
-
我的这个事件发生了
-
关心这件事的人会被依次通知到
-
被通知的人根据拿到的事件信息(又称"事件数据","事件参数","通知")对事件进行响应(处理事件)
-
提示:
- 事件多用于桌面,手机等开发的客户端编程,因为这些程序经常使用户通过事件来"驱动"的
- 各种编程语言对这个机制的实现方法不尽相同
- Java语言里面没有事件这种成员,也没有委托这种数据类型,Java的事件使通过使用接口来实现的
- MVC.MVP,MVVM等模式,是事件模式更高级,更有效的玩法
- 日常开发的时候,使用已有事件的机会比较多,自己声明事件的机会比较少
事件的应用
事件不会主动发生,只能由事件拥有者某些内部逻辑触发之后才能发生!!!
事件模型的五个组成部分
- 事件的拥有者(event source,对象)
- 事件成员(event ,成员)
- 事件的响应者(event subscriber, 对象)
- 事件处理器(event handler,成员)--本质上是一个回调方法
- 事件订阅---把事件处理器与事件相关联在一起,本质上是一种以委托类型为基础的"约定"
注意:
- 事件处理器是方法成员
- 挂接事件处理器的时候,可以使用委托实例,也可以直接使用方法名,这是个"语法糖"
- 事件处理器对事件的订阅不是随意的,匹配与否由声明事件时所使用的委托类型来检测
- 事件可以同步调用也可以异步调用
//情况1
public static void Main(string[] args)
{
Timer timer = new Timer();
timer.Interval = 1000;
Boy boy = new Boy();
timer.Elapsed += boy.Action; //事件 += 事件响应者的事件处理器
timer.Start();
Console.ReadLine();
}
事件的声明
完整声明
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace EventFinal
{
internal class Program
{
static void Main(string[] args)
{
Customer customer = new Customer();
Waiter waiter = new Waiter();
customer.Order += waiter.Action;
customer.Action();
customer.PayBill();
}
}
//确保访问级别一致
public class OrderEventArgs: EventArgs//作为EventArgs(事件信息)时继承EventArgs类
{
public string DishName { get; set; }
public string Size { get; set; }
}
public delegate void OrderEventHandler(Customer customer,OrderEventArgs e);//声明委托
public class Customer
{
private OrderEventHandler orderEventHandler;
public event OrderEventHandler Order//完整声明
{
add
{
this.orderEventHandler += value;
}
remove
{
this.orderEventHandler -= value;
}
}
public double Bill { get; set; }
public void PayBill()
{
Console.WriteLine("I will pay {0}!",this.Bill);
}
public void WalkIn()
{
Console.WriteLine("Walk into thee restaurant.");
}
public void SitDown()
{
Console.WriteLine("Sit down");
}
public void Think()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("Let me think ...");
Thread.Sleep(300);
}
if (this.orderEventHandler != null)
{
OrderEventArgs e=new OrderEventArgs();
e.DishName = "Kongpao Chicken";
e.Size = "large";
this.orderEventHandler.Invoke(this, e);
}
}
public void Action()
{
Console.ReadLine();
this.WalkIn();
this.SitDown();
this.Think();
}
}
public class Waiter
{
public void Action(Customer customer, OrderEventArgs e)
{
Console.WriteLine("I will server you the dish - {0}",e.DishName);
double price = 10;
switch (e.Size)
{
case "small":
price = price * 0.5;
break;
case "large":
price *= 1.5;
break;
default:
break;
}
customer.Bill+=price;
}
}
}
简略声明(字段式声明 field-like)
public class Customer
{
public event OrderEventHandler Order;//字段式声明
public double Bill { get; set; }
public void PayBill()
{
Console.WriteLine("I will pay {0}!",this.Bill);
}
public void WalkIn()
{
Console.WriteLine("Walk into thee restaurant.");
}
public void SitDown()
{
Console.WriteLine("Sit down");
}
public void Think()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("Let me think ...");
Thread.Sleep(300);
}
if (this.Order != null)
{
OrderEventArgs e=new OrderEventArgs();
e.DishName = "Kongpao Chicken";
e.Size = "large";
this.Order.Invoke(this, e);
}
}
public void Action()
{
Console.ReadLine();
this.WalkIn();
this.SitDown();
this.Think();
}
}
最终版本 --使用EventHandler类而不用自己声明
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace EventFinal
{
internal class Program
{
static void Main(string[] args)
{
Customer customer = new Customer();
Waiter waiter = new Waiter();
customer.Order += waiter.Action;
customer.Action();
customer.PayBill();
}
}
//确保访问级别一致
public class OrderEventArgs: EventArgs//作为EventArgs(事件信息)时继承EventArgs类
{
public string DishName { get; set; }
public string Size { get; set; }
}
public delegate void OrderEventHandler(Customer customer,OrderEventArgs e);//声明委托
public class Customer
{
public event EventHandler Order;//使用EventHandler类而不用自己声明
public double Bill { get; set; }
public void PayBill()
{
Console.WriteLine("I will pay {0}!",this.Bill);
}
public void WalkIn()
{
Console.WriteLine("Walk into thee restaurant.");
}
public void SitDown()
{
Console.WriteLine("Sit down");
}
public void Think()
{
for (int i = 0; i < 5; i++)
{
Console.WriteLine("Let me think ...");
Thread.Sleep(300);
}
if (this.Order != null)
{
OrderEventArgs e=new OrderEventArgs();
e.DishName = "Kongpao Chicken";
e.Size = "large";
this.Order.Invoke(this, e);
}
}
public void Action()
{
Console.ReadLine();
this.WalkIn();
this.SitDown();
this.Think();
}
}
public class Waiter
{
public void Action(object sender, EventArgs e)//参数列表与EventHandler中一致
{
Customer customer1=sender as Customer;//类型转换
OrderEventArgs orderInfo=e as OrderEventArgs;
Console.WriteLine("I will server you the dish - {0}", orderInfo.DishName);
double price = 10;
switch (orderInfo.Size)
{
case "small":
price = price * 0.5;
break;
case "large":
price *= 1.5;
break;
default:
break;
}
customer1.Bill+=price;
}
}
}
有了委托字段/属性,为什么还需要事件?
- 为了程序的逻辑更加有道理,更加安全!!
所以事件的本质是委托字段的一个包装器
- 者包装器对委托字段的访问起限制作用,相当于一个 "蒙版"
- 封装 的 一个重要功能就是隐藏
- 事件对外界隐藏了委托实例的大部分功能,
仅暴露添加/移除事件处理器的功能 - 添加/一处事件处理器的时候可以直接使用方法名,这是委托实例所不具备的功能
用于声明事件的委托类型的命名约定
- 用于声明A事件的委托,一般命名为
AEventHandler(除非是一个非常通用的事件约束) AEventHandler委托的参数一般有两个- 第一个是
object类型,名字为sender,实际上就是事件的拥有者,事件的source - 第二个是
EventArgs类的派生类,命名一般为AEventArgs,参数名为e,也就是事件参数 - 一般将委托的参数列表看作是事件发生后发送给事件响应者的"
事件消息"
- 第一个是
- 触发A事件的方法一般命名为OnA,即事出有因
事件与委托关系
事件真的是"以特殊方式声明的委托字段/实例"吗?
不是!只是声明的时候 " 看起来像 "(对比委托字段与事件的简化声明)- 事件声明的时候使用了委托类型,简化声明造成事件看上去像一个委托字段(实例),而event关键字则更像是一个修饰符---这就是错觉的来源之一
- 订阅事件的时候 += 操作符后面可以是一个委托实例,这与委托实例的赋值方法语法相同,这也让事件看起来像是一个委托字段 -- 错觉的又一来源
- 事件本质是加装在委托字段上的一个"蒙版",是个起掩护左右的包装器.这个用于阻挡非法操作的蒙版绝对不是委托字段本身
为什么要使用委托类型来声明事件?
- 站在
source的角度来看,是为了表明source能对外传递哪些消息 - 站在
subscriber的角度来看,它是一种约定,是为了约束能够使用什么样签名的方法来处理(响应)事件 - 委托类型的实例将用于存储(引用)事件处理器
对比事件与属性
- 属性不是字段--很多时候属性是字段的包装器,这个包装器用来保护字段不被滥用
- 事件不是委托字段--他是委托字段的包装器,这个包装器用来保护委托字段不被滥用
- 包装器永远都不可能是被包装的东西!!
🚀类的继承和多态 *
类的继承
- 类在功能上的扩展 extend
- 只能有一个基类,但可以实现多个 (单继承多实现)
- 类访问级别对继承的影响
- sealed类不能被继承
成员的继承与访问
**本质:**派生类在基类原有成员基础上对基类进行横向或纵向拓展!
- 横向:成员数量增加
- 纵向:原有成员进行改造(版本扩展--重写)
子类访问级别不能超越父类!!
类成员的访问级别是以类成员的访问级别为上限的!!
- 派生类对继承成员的访问
- 派生类对基类成员的访问
- 构造器的不可继承性 -- 父类构造器不被子类继承
面向对象的实现风格
- Class-based
- Prototype-based
重写
- 重写父类方法
- 基类可重写方法需要加上
virtual关键字 - 子类重写父类方法需要加上
override
internal class Vehicle
{
public virtual void Run()
{
Console.WriteLine("Running");
}
}
class Car : Vehicle
{
public override void Run()
{
Console.WriteLine("Car is Running");
}
}
当用父类变量引用子类实例时,调用到的版本永远是继承链最新版本的子类实例 , 如果没有重写则调用继承链中最新版本的方法!!(可解释如下)
- 由于Run方法被重写 所以输出为
Car is Running - 若没有
override和virtual关键字则没有重写 只是隐藏了父类Run方法 则如下方法输出原父类方法Running - 与Java不一样 Java 默认为天然重写 加了
override和virtual关键字的情况!!!!!!!
Vehicle v = new Car(); //is a
v.Run(); //Car is Running 由于Run方法被重写 所以输出为 Car is Running
🪢接口
抽象类与开闭原则(开放/关闭原则)
开闭原则:
我们应该封装那些不变的,稳定的,固定的和确定的成员,而那些不确定的不稳定的成员应该交给子类去实现
什么是接口和抽象类?
- 接口和抽象类都是"软件工程"的产物
具体类->抽象类->接口:越来越抽象,内部实现的东西越来越少- 抽象类时未完全实现逻辑的类(可以有字段和非public成员,他们代表了"具体逻辑")
- 抽象类为复用而生:专门作为基类来使用,也具有
解耦功能 - 封装确定的,开放不确定的,推迟到合适的子类中去实现
- 接口是完全为实现逻辑的"类"
完全抽象类("纯虚类";只有函数成员;成员全部public) - 接口为解耦而生:"高内聚,低耦合",方便单元测试
- 接口是一个:协约,早已为工业生产所熟知(有分工必有协作,有写作必有协约)
- 他们
都不能实例化,只能用来声明变量,引用具体类(concrete class)的实例
**抽象类:**函数成员没有完全实现;
- 与虚方法区别:虚方法非必须重写,抽象类未被实现的方法子类必须重写
**接口:**函数成员全都没有被实现;(完全抽象类) *现在好像接口内可以实现方法 个人理解是开放了多继承
- 接口默认所有成员都是
public
interface IVehicle {//接口
void Stop();
void Run();
void Fill();
}
abstract class Vehicle:IVehicle{//抽象类
public void Stop(){
Console.WriteLine("Stopped!");
}
public void Fill() {
Console.WriteLine("Refule!!");
}
public abstract void Run();
}
接口与单元测试
- 接口的产生:自底向上(重构),自顶向下(设计)
- C#中接口的实现(隐式,显式,多接口)
- 语言对面向对象设计的内建支持:依赖反转,接口隔离,开/闭原则....

例子:由于Arraylist实现了IEnumerable接口,可以用IEnumerable接收可枚举的参数
class Program {
public static void Main(string[] args) {
int[] nums1 = new int[] { 1, 2, 3, 4, 5 };
ArrayList nums2 = new ArrayList { 1, 2, 3, 4, 5 };
Console.WriteLine(Sum(nums1));
Console.WriteLine(Avg(nums1));
Console.WriteLine(Sum(nums2));
Console.WriteLine(Avg(nums2));
}
static int Sum(IEnumerable nums) {
int sum = 0;
foreach (var num in nums) { sum += (int)num; }
return sum;
}
static double Avg(IEnumerable nums) {
int sum = 0;
double count = 0;
foreach (var item in nums) {
sum += (int)item;
count++;
}
return sum / count;
}
}
xUnit单元测试
- 新建xUnit项目
- 编写单元测试示例
- 运行单元测试
注意 : 单元测试与原项目.net版本和C#版本应该保持一致!!!!
namespace InterfaceLearn.Tests;
public class DeskFanTests {
[Fact]
public void LowerThanZero() {
DeskFan deskFan = new DeskFan(new BadPowerSupplyLowerThanZero());
Assert.Equal(expected:"Wont work.",actual:deskFan.Work());
}
[Fact]
public void HigherThan200() {
DeskFan deskFan = new DeskFan(new BadPowerSupplyHigherThan200());
Assert.Equal(expected:"Warning.",actual:deskFan.Work());
}
}
class BadPowerSupplyLowerThanZero : IPowerSupply {
public int GetPower() {
return 0;
}
}
class BadPowerSupplyHigherThan200 : IPowerSupply {
public int GetPower() {
return 300;
}
}
使用moq.framework配合xUnit进行单元测试
using Moq;
namespace InterfaceLearn.Tests;
public class DeskFanTests {
[Fact]
public void LowerThanZero() {
var mock = new Mock<IPowerSupply>();
mock.Setup(ps => ps.GetPower()).Returns(()=>0);//设置方法返回值
DeskFan deskFan = new DeskFan(mock.Object); //传递实例
Assert.Equal(expected:"Wont work.",actual:deskFan.Work());
}
[Fact]
public void HigherThan200() {
var mock = new Mock<IPowerSupply>();
mock.Setup(ps => ps.GetPower()).Returns(()=>300);
DeskFan deskFan = new DeskFan(mock.Object);
Assert.Equal(expected:"Warning.",actual:deskFan.Work());
}
}
接口隔离原则
调用者不也能多要,实现者不能多给.
- 控制接口和类的大小
- 多基接口实现 实现父类方法
using System;
namespace ISPLearn {
internal class Program {
public static void Main(string[] args) {
var driver = new Driver(new HeavyTank());//父类引用子类实例
driver.Drive();
}
}
class Driver {
private IVehicle _vehicle;//违反接口隔离原则 应该设法分裂成俩个接口
public Driver(IVehicle vehicle) {
_vehicle = vehicle;
}
public void Drive() {
_vehicle.Run();
}
}
interface IVehicle {
void Run();
}
class Car:IVehicle {
public void Run() {
Console.WriteLine("Car is running...");
}
}
class Truck:IVehicle {
public void Run() {
Console.WriteLine("Truck is running...");
}
}
interface IWeapon {//接口分离 武器开火 载具行驶
void Fire();
}
interface ITank:IVehicle,IWeapon {
void Fire();
void Run();
}//多基接口实现 此时实现父类IVehicle.Run
class LightTank:ITank {
public void Fire() {
Console.WriteLine("Boom!");
}
public void Run() {
Console.WriteLine("Ka ka ka ...");
}
}
class MediumTank:ITank {
public void Fire() {
Console.WriteLine("Boom!!");
}
public void Run() {
Console.WriteLine("Ka! ka! ka! ...");
}
}
class HeavyTank:ITank {
public void Fire() {
Console.WriteLine("Boom!!!");
}
public void Run() {
Console.WriteLine("Ka!! ka!! ka!! ...");
}
}
}
显式接口隔离 C#独有
- 只有实例为
IKiller类型时才能调用kill方法
using System;
namespace ISPLearn {
public class ISP3 {
public static void Main(string[] args) {
//显式接口隔离
var wk = new WarmKiller();
wk.Love();
IKiller wk2 = new WarmKiller();//只可访问Kill方法 不可访问Love方法
wk2.Kill();
var wk3 = wk2 as WarmKiller;//转换类型
wk3.Kill();
wk3.Love();
}
interface IGentleman {
void Love();
}
interface IKiller {
void Kill();
}
class WarmKiller:IGentleman,IKiller {
//Usage 1
/*private IKiller _killerImplementation;//显示接口隔离
public void Kill() {
Console.WriteLine("Let me kill enemy ...");
}*/
//Usage 2
void IKiller.Kill() {
Console.WriteLine("Let me kill enemy ...");
}
public void Love() {
Console.WriteLine("i love u forerver ...");
}
}
}
}
🪞反射( Reflection)与依赖注入
(69条消息) C#的反射_牛奶咖啡13的博客-CSDN博客_c# 反射
- 反射:以不变应万变(更松的耦合)
- 反射与接口的结合
- 反射与特性的结合
- 依赖注入:此DI非彼DI,但没有DI就没有此DI..
反射指程序可以动态访问、检测和修改它本身状态或行为的一种能力。
//其余部分代码见 接口隔离原则部分
public static void Main(string[] args) {
ITank tank = new HeavyTank();
// 依赖注入
// ======== 分割线 ===== 以下不再使用静态类型
var t = tank.GetType();
object o = Activator.CreateInstance(t);
MethodInfo fireMi = t.GetMethod("Fire");
MethodInfo runMi = t.GetMethod("Run");
fireMi.Invoke(o, null);
runMi.Invoke(o, null);
}
三方插件--一般使用
App.cs
using System.IO;
using System.Runtime.Loader;
namespace ReflectDILearn;
public class ReflectDILearn {
public static void Main(string[] args) {
var folder = Path.Combine(Environment.CurrentDirectory, "Animals");
var files = Directory.GetFiles(folder);
var animalTypes = new List<Type>();
foreach (var f in files) {
var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(f);
var types = assembly.GetTypes();
foreach (var t in types) {
if (t.GetMethod("Voice")!=null) {
animalTypes.Add(t);
}
}
}
while (true) {
for (int i = 0; i < animalTypes.Count; i++) {
Console.WriteLine($"{i+1}.{animalTypes[i].Name}");
}
Console.WriteLine("===================");
Console.WriteLine("Please Choose animal:");
int index = int.Parse(Console.ReadLine());
if (index>animalTypes.Count||index<1) {
Console.WriteLine("No such an animal.Try again!");
continue;
}
Console.WriteLine("Times?");
int times = int.Parse(Console.ReadLine());
var t = animalTypes[index - 1];
var m = t.GetMethod("Voice");
var o = Activator.CreateInstance(t);
m.Invoke(o, new object?[] { times });
}
}
}
animals.Lib
namespace Animals.Lib;
public class Cat {
public void Voice(int times) {
for (int i = 0; i < times; i++) {
Console.WriteLine("Meow!");
}
}
}
libs写好之后Build Solution 生成的dll文件放到app.cs中Animals文件夹 读取文件夹中动物类 并进行操作.....
三方插件--SDK使用
为了让其他开发者能够更轻松的二开程序 可以给出SDK
步骤
-
写一个数据接口 以Animals为例
namespace ReflectDILearn.SDK; public interface IAnimal { void Voice(int times); } -
定义一个
UnfinishedAttribute类,此类继承自Attribute(可以什么都不用干)namespace ReflectDILearn.SDK; public class UnfinishedAttribute:Attribute { } -
Build Solution
-
找到dll文件

-
第三方引用SDK.DLL

-
继承SDK接口
using ReflectDILearn.SDK; namespace Animals.Lib2; public class Dog:IAnimal { public void Voice(int times) { for (int i = 0; i < times; i++) { Console.WriteLine("Woof!"); } } } -
若部分内容未做完可以添加
[unfinished]using ReflectDILearn.SDK; namespace Animals.Lib2; [Unfinished] public class Cow:IAnimal { public void Voice(int times) { for (int i = 0; i < times; i++) { Console.WriteLine("Moo!"); } } } -
完成之后
build solution主体程序导入Animals 的dll文件- 注意修改之处与前文区别 因为此时已知类型为IAnimals 所以直接强制转换获取实例
using System.IO; using System.Runtime.Loader; using ReflectDILearn.SDK; namespace ReflectDILearn; public class ReflectDILearn { public static void Main(string[] args) { var folder = Path.Combine(Environment.CurrentDirectory, "Animals"); var files = Directory.GetFiles(folder); var animalTypes = new List<Type>(); foreach (var f in files) { var assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(f); var types = assembly.GetTypes(); foreach (var t in types) {//注意此处修改 if (t.GetInterfaces().Contains(typeof(IAnimal))) { if (t.GetInterfaces().Contains(typeof(IAnimal))) { //获得Unfinished标记 var isUnfinished = t.GetCustomAttributes(false).Any(a => a.GetType() == typeof(UnfinishedAttribute)); if (isUnfinished) continue; animalTypes.Add(t); } } } } while (true) { for (int i = 0; i < animalTypes.Count; i++) { Console.WriteLine($"{i+1}.{animalTypes[i].Name}"); } Console.WriteLine("==================="); Console.WriteLine("Please Choose animal:"); int index = int.Parse(Console.ReadLine()); if (index>animalTypes.Count||index<1) { Console.WriteLine("No such an animal.Try again!"); continue; } Console.WriteLine("Times?"); int times = int.Parse(Console.ReadLine()); var t = animalTypes[index - 1]; var m = t.GetMethod("Voice"); var o = Activator.CreateInstance(t); var a = o as IAnimal;//此处已修改 a.Voice(times); } } }
依赖注入
- nuget 安装
Microsoft.Extensions.DependencyInjection - 我的理解是类似Spring的容器的
AutoWire
public static void Main(string[] args) {
var sc = new ServiceCollection();//容器
//ITank是静态类型 typeof(ITank)才是ITank动态的类型描述
sc.AddScoped(typeof(IVehicle), typeof(Car));
sc.AddScoped<Driver>();
var sp = sc.BuildServiceProvider();
// ==== 分割线== 以上为注册 以下为向ServiceProvider获取对象
var Driver = sp.GetService<Driver>();
Driver.Drive(); //AutoWire
}
⚙️泛型
泛型(generic)无处不在
-
Q:为什么需要泛型? A:避免成员膨胀或者类膨胀
当需要增加更多的类型时,在添加新的类称为类型膨胀,或者一直添加某个类中的成员称为成员膨胀class Apple { public string Color { get; set; } } class Book { public string Name { get; set; } } //.....若此处一直添加新类型 一般情况下会有如下方法 class AppleBox {//添加新类 类型膨胀 public Apple Cargo { get; set; } } class Box { public Apple Cargo { get; set; } public Book Book { get; set; }// 添加新成员 成员膨胀 } -
正交性:
泛型类型(类/接口/委托...) 泛型成员(属性/方法/字段 ...)
-
类型方法的参数判断
-
泛型与委托,lambda表达式
泛型一般使用
泛型类
using System.Drawing;
namespace TemplateLearn;
class Program {
public static void Main(string[] args) {
Apple apple = new Apple(){Color = "Red"};
Book book = new Book() { Name = "A Book" };
Box<Apple> box1 = new Box<Apple>() { Cargo = apple };//使用泛型
Box<Book> box2 = new Box<Book>() { Cargo = book };
Console.WriteLine(box1.Cargo.Color);
Console.WriteLine(box2.Cargo.Name);
}
}
class Apple {
public string Color { get; set; }
}
class Book {
public string Name { get; set; }
}
class Box<TCargo> { //<>中为类型参数 定义
public TCargo Cargo { get; set; }
}
泛型接口
namespace TemplateLearn;
class Program {
public static void Main(string[] args) {
Student<int> student = new Student<int>();//表示id为int类型的学生id 可以设置为其他类型如 ulong....
student.ID = 1;
student.Name = "Aowu";
Console.WriteLine(student.ID);
}
}
interface IUnique<Tid> {
Tid ID { get; set; }
}
class Student<Tid> : IUnique<Tid> {//实现泛型接口的类 也是泛型类
public Tid ID { get; set; }
public string Name { get; set; }
}
class Student : IUnique<ulong> { //若指定泛型接口类型 则此类不是泛型类 实现的是特化之后的泛型接口
public ulong ID { get; set; }
}
泛型委托
Action
Action委托只能引用没有返回值的方法 <>中的类型为方法入参类型
using System.Collections.Generic;
namespace TemplateLearn;
class Program {
public static void Main(string[] args) {
//Action委托只能引用没有返回值的方法 <>中的类型为方法入参类型列表
Action<string> say = Say;//委托
say.Invoke("Aowu");
say("Aowu!");
Action<int> mul = Mul;
mul.Invoke(10);
}
static void Say(string str) {//参数为string
Console.WriteLine($"Hello,{str}!");
}
static void Mul(int x) {//参数为int
Console.WriteLine(x*100);
}
}
Func
Func委托可以引用带返回值的方法
namespace TemplateLearn;
class Program {
public static void Main(string[] args) {
//类型参数为 入参类型列表, 最后一个参数为返回值类型
Func<double, double, double> addInt = Add;
var res = addInt.Invoke(100.2, 300.1);
Console.WriteLine(res);
}
static int Add(int a, int b) {
return a + b;
}
static double Add(double a, double b) {
return a + b;
}
}
lambda表达式
(参数列表)=>{方法体}
public static void Main(string[] args) {
// (参数列表)=>{方法体} 应该就是匿名函数吧
Func<double, double, double> addInt = (a,b) => { return a + b;};
var res = addInt.Invoke(100.2, 300.1);
Console.WriteLine(res);
}
partial类
C#编译器允许我们把一个类分成多个部分进行编译,每一块是自己的逻辑单元并可以进行自己的版本更新.
- 所有部分类的命名空间与类名 需要保持一致
- 甚至允许不同部分用不同编程语言编译
namespace mynamespace;
public partial class ClassPartial{}//partial修饰的类为部分类
//... 部分类其余部分的命名空间需要保持一致
- 减少类的派生
- partial类与Entity Framework
- partial类与Windows Forms,WPF,ASP.Net Core
⛓️枚举类型
- 认为界定取值范围的整数
- 整数值的对应
- 比特位式用法
using System.Collections.Generic;
namespace TemplateLearn;
class Program {
public static void Main(string[] args) {
Person bigBoss = new Person();
bigBoss.Level = Level.BigBoss;
Person employee = new Person();
employee.Level = Level.BigBoss;
Console.WriteLine(bigBoss.Level>employee.Level);//可以比较大小
//======== 比特位用法 ↓ ===
//可以同时选择多个枚举值
bigBoss.Skill = Skill.Drive | Skill.Cook | Skill.Program | Skill.Teach;//按位取或 =相加
Console.WriteLine(bigBoss.Skill); //15 1111
Console.WriteLine((bigBoss.Skill & Skill.Cook)>0);//若大于0 则表示会做饭
Console.WriteLine((bigBoss.Skill & Skill.Cook)==Skill.Cook);//若相等 则表示会做饭
}
}
enum Skill {
Drive=1, //0001 //这四个数各位1错开
Cook=2, //0010
Program=4,//0100
Teach=8,//1000
}
enum Level {
Employee=100,
//可以手动指定对应值 如果只赋值了一个 则后面的依次+1
//如现在Employee=100,则Manager=101 Boss=102 ...
Manager,
Boss,
BigBoss,
}
class Person {
public Level Level { get; set; }
}
📃结构体
- 值类型,可装箱拆箱
- 可实现接口,不能派生自类/结构体
- 不能有显式无参构造器
值类型: 变量之间赋值的时候,变量之间Copy的是一个完整的对象
引用类型:变量之间赋值的时候,变量之间Copy的是同一个对象的引用
class Program {
public static void Main(string[] args) {
Student stu = new Student(){ID=101,Name = "Aowu"};
object obj = stu;//装箱 obj引用堆内存中的 Student实例
Student student = (Student)obj;//拆箱
Console.WriteLine($"{student.Name},{student.ID}");//Aowu,101
Student stu2 = stu;
Console.WriteLine($"{stu2.Name},{stu2.ID}"); //Aowu,101
stu2.ID = 102;
stu2.Name = "Wuhu";
Console.WriteLine($"{stu2.Name},{stu2.ID}");//Wuhu,102
stu2.Speak(); //Wuhu is speaking...
Student stu3 = new Student(1, "Oyo");
Console.WriteLine($"{stu3.Name},{stu3.ID}");//Oyo,1
}
}
interface ISpeak {
void Speak();
}
struct Student:ISpeak {
public int ID { get; set; }
public string Name { get; set; }
public Student(int id, string name) {
ID = id;
Name = name;
}
public void Speak() {
Console.WriteLine($"{this.Name} is speaking...");
}
}
💼委托
什么是委托?
**委托类型:**实际上是一种类类型.(引用类型)
一般情况下类是用来反映现实世界的事物,委托类型是用来包裹着一些方法的,通过委托类型实例间接调用一些方法 (方法的包装器)
**功能:**一个函数或者一组函数的包装器
委托类型是怎么声明出来的?
class Program {
public static void Main(string[] args) {
//使用委托类型
//包裹Add方法
MyDele dele1 = new MyDele(Add);
//可以包裹多个方法
//多播委托:一次调用多个方法
//dele1 += Method1;
//通过委托实例间接调用此方法
Console.WriteLine(dele1.Invoke(1,2)); //也可使用dele1();
}
static int Add(int x,int y) {
return x + y;
}
}
delegate int MyDele(int a,int b);//声明委托类型 返回类型为:int 参数列表为: int int
泛型委托
class Program {
public static void Main(string[] args) {
//泛型委托
//特化为int 特化:指定泛型的类型
MyDele<int> deleAdd = new MyDele<int>(Add);
Console.WriteLine(deleAdd(100,210));//Console:310
MyDele<double> deleMul = new MyDele<double>(Mul);
Console.WriteLine(deleMul(1.3,21.0));//Console:27.3
//委托是一种类类型
Console.WriteLine(deleAdd.GetType().IsClass);//Console:True
}
static int Add(int a, int b) {
return a + b;
}
static double Mul(double a, double b) {
return a * b;
}
}
delegate T MyDele<T>(T a, T b);
我们必须自己创建委托类型吗?
并不是,大多数情况下我们不必创建自己的委托类型,C#已经为我们准备了两个泛型委托,具体详见泛型--泛型委托
🧩lambda表达式
Lambda
- 匿名方法
- Inline方法
- (参数列表)=>{方法体}
public static void Main(string[] args) {
//泛型委托的类型推断,由于输入的 100,200 都是标准的int类型所以认定T类型为int
DoSomeCalc(((a, b) => { return a * b;}),100,200);
}
static void DoSomeCalc<T>(Func<T, T, T> func,T x,T y) {
Console.WriteLine(func(x, y));
}
🎈LINQ
LINQ:.NET Language Integrated Query
..未完
