C#
1 类库的引用
(1) 类库的引用是使用名称空间的物理基础,不同技术类型的项目会默认引用不同的类库
(2) DLL引用,黑盒引用,无源代码
- 可用nuget打包引入。依赖库会自行解决
- 自己在solution里新建了类库,project需要引入reference里,然后程序里写using namespace
(3) 项目引用,白盒引用,有源代码:solution里添加项目,然后再reference里添加刚才添加的项目
- 类与类,类库与类库之间是有依赖(耦合)关系的,低层类库不正常,上层也不行。
- 程序追求高内聚(数据和功能该属于哪个类就放到那里)低耦合(类与类之间的依赖性低)。
- UML(通用建模语言)类图:
2 类与对象
类是现实世界事物的模型,是对现实世界事物进行抽象所得到的结果。事物包括物质(实体)与运动(逻辑)。
建模是一个去伪存真(根据这个类的特性,保留你需要的属性)、由表及里的过程(通过它的表象外表接口去分析它应该是哪些属性在操纵它,分析内部的逻辑)。
(1) 对象object也叫实例instance,是类经过实例化后得到的内存中的实体;
- 对象和实例是一回事;
- 天上有一架飞机,必须是实例飞,概念是不能飞的;
- 有些类不能实例化,比如数学;
- 依照类,使用new操作符创建类的实例;
(2) 引用变量与实例的关系
- 孩子(引用变量)与气球(实例)
- 气球不一定有孩子牵着
- 多个孩子可以使用各自的绳子牵着同一个气球,也可以通过一根绳子牵着气球
Form myForm; //引用变量,孩子 Form myForm = new Form(); //一个孩子与一个气球; Form myForm2 = myForm; //2个孩子用各自的绳子牵着同一个气球
(3) 类的三大成员
- 属性property:存储数据,组合起来表示类或对象当前状态
- 方法method:表示类或对象能做什么,是真正做事、构成逻辑的成员
- 事件event:类或对象通知其他类或对象的机制,C#特有
某些特殊类或对象在成员方面侧重点不同,如模型类或对象侧重属性,工具类侧重方法,通知类侧重事件
(4) 静态成员于实例成员
- 静态static成员在语义上表示它是类的成员;人抽象出来的类:总数,平均身高,平均体重
- 实例(非静态)成员在语义上表示它是对象的成员;某个对象的身高、体重
- 绑定binding指的是编译器如何把一个成员与类或对象关联起来(早binding是编译器知道,晚binding是运行后才知道);‘.’操作符用于成员访问
快捷键:tab,可以快速补齐未写完代码的常规部分,比如for循环,事件注册等;
ctrl+E+C注释,ctrl+K+U去掉注释
3 C#基本元素
关键字,标识符(不以数字开头,用关键字的话前面加@),操作符(运算符),标点符号,文本(字面值:null,整数,实数,字符,字符串,bool),注释。
前5个是标记token,是对编译器有意义的值。
-
变量名都用驼峰法 Camel
-
首字母小写,后续单词首字母大写
-
例:
apple
smallApple
-
方法、类、名称空间都用帕斯卡 Pascal
-
每个单词的首字母都大写
-
例:
Apple
SmallApple
- 方法名应该是动词或动词短语,例如 Today 错,GetToday 对。
ctrl+e+d,自动对齐格式;
public int SumFrom1ToX(int x) { if (x == 1) return 1; else { return x + SumFrom1ToX(x - 1);//递归 } }
4 类型、变量与对象
(1) 强类型:数据类型与数据强绑定,能保证数据完整性,比如将double赋值给int会报错;
dynamic类型用来模仿弱类型(var编译器推断类型,之后不能更改);
(2) 类型在C#中的包含的信息
- 存储此类型所需的内存空间大小
- 此类型的值可表示是的最大值最小值范围
- 此类型所包含的成员(方法、属性、事件等);
- 静态程序:程序没有执行的时候,写代码或编译,编译器可以验证类型是否包含某个成员;
- 动态程序:程序执行起来,调试或运行,比如反射;
- 此类型由何种基类派生而来
- 程序运行时,此类型的变量在分配在内存的什么位置
- stack栈:方法调用,比较小,比较快;stack overflow
- heap堆:存储实例等;内存泄露
- 此类型所允许的操作(运算3/4与3.0/4区别)
(3) C#的五大数据类型
- 类class:form, windows,string
- 结构体struct:int, long, double
- 枚举enum
- 接口
- 委托
object为根,蓝色是基本数据类型,太常用的成为了关键字;黑色是定义蓝色数据类型的关键字;
(4) 什么是变量
- 表面来看,变量用途是存储数据
- 变量表示了存储位置,并且每一个变量都有一个类型,以决定什么样的值能够存入变量
- 有7种变量:静态变量,实例变量(成员变量,字段),数组元素,值参数,引用参数ref,输出形参out,局部变量
- 声明变量:有效的修饰符组合(opt) 类型变量名 初始化器(opt) public static int p =10
- 变量是以变量名所对应的内存地址为起点,以其数据类型所需要的存储空间为长度的一块内存区域
(5) 值类型的变量,值类型没有实例,所谓的实例与变量合而为一
(6) 引用类型的变量与实例的关系:引用类型变量里存储的数据是实例在堆内存里的内存地址;
Student stu;//声明后,引用类型在内存中用全0的数据分配4个字节内存(内存A); stu = new Student();//实例化后,分配student类所占字节的内存B,并把此地址保存在内存A中; class Student{uint ID};
(7) 局部变量在栈上分配内存
(8) 注意变量的默认值;常量const初始化赋值,不能再次赋值;
装箱:
int x = 100; object obj = x;object引用的值不是堆上的实例而是栈上的值类型的值时,先把x的值复制封装成objet的实例,在堆上找内存放,再把此内存放在obj里。
拆箱:
int y=(int)obj; 先在栈上分配y的内存A,再把堆上obj实例的值按要求拆成目标类型存储在栈上内存A里去;
装箱与拆箱都会损失性能;
5 方法的定义、调用与调试
(1) 方法
- 方法的前身是C/C++语言的函数
- 方法是类或结构体的成员,C#中方法不可能独立于类或结构体之外,只有作为类或结构体的成员才被称为方法,C++中是可以的,称为全局函数;
- 类或结构体最基本的成员:方法与字段(成员函数与成员变量),本质还是数据+算法,方法表示能做什么事
- 方法可以:隐藏复杂的逻辑,把大算法分解为小算法,复用;
- 方法首字母大写,用动词或动词短语命名
- parameter形参,argument实参
(2) 构造器
ctor+tap+tap 自动补齐构造器代码
- 构造器是类型的一种,狭义的构造器指的是实例构造器;
- 构造器的内存原理
class Student { pubilc Student(int id, string name) {this.ID=id; this.Name = name;} pubilc int ID; public string Name; } ... Student stu = new Student(2,"no");
//创建引用变量stu,在从高往低分内存空间的栈上分配内存空间(未赋值)
//new创建实例(在堆上分配内存空间),8byte
//(2,"no")调用实例的构造函数,切割int分4byte内存A,string分4byte内存B,int设为2;
//string是class类型,需要在堆中再找一个内存C存放string的初始值"no",内存C的地址放在内存B中;内存A的地址存放在栈上stu处
(3) 方法的重载
- 方法签名由方法的名称、类型形参的个数和它的每一个形参(从左到右的顺序)的类型和种类(值,引用和输出)组成。方法签名不包含返回类型。
- 实例构造函数签名由它的每一个形参(按从左到右的顺序)的类型和种类(值、引用、输出)组成。
- 重载决策(到底调用哪一个重载):用于在给定了参数列表和一组候选函数成员的情况下,选择一个最佳函数成员来调用。
(4) 调试
-
step into:单步执行,遇到子函数就进入并且继续单步执行(简而言之,进入子函数);
-
step over:在单步执行时,在函数内遇到子函数时不会进入子函数内单步执行,而是将子函数整个执行完再停止,也就是把子函数整个作为一步。有一点,经过我们简单的调试,在不存在子函数的情况下是和step into效果一样的(简而言之,越过子函数,但子函数会执行)。
-
step out:当单步执行到子函数内时,用step out就可以执行完子函数余下部分,并返回到上一层函数。
(5) 方法调用时内存分配,stack frame 是方法被调用是在栈内存的布局
- main在栈里已经占一部分内存,把main(主调用者)里子函数(被调用者)的参数压入栈中,先压左边后压右边,谁调用谁负责;
- 进入到子函数,子函数把自己变量压到栈里
- 返回值存在cpu的寄存器里,上一层函数从寄存器中读取放在对应局部变量的内存里
- return返回后,一个函数占用的栈内存清空,返回上一层函数
6 操作符
(1) 操作符本质
- 操作符就是函数的简记法,3+4,即为Add(3,4)
- 操作符不能脱离与它关联的数据类型,可以为自定义的数据类型创建操作符
(2) 操作符优先级,除了带赋值功能的操作符运算顺序是从右往左,其他同优先级是从左到右;
(3) 基本操作符
- Type t = typeof(int); t.Name等,用来获取类型内部结构;
- default设默认值,一般为0;Form my = default(Form);表示NULL;枚举的话,数字值里要有0,要不无法匹配;
- Form my = new Form(); new操作符创建类型实例,调用实例构造器(), 得到实例的内存地址并通过=交给变量my;
- new调用初始化器,Form my = new Form(Text = "my"); new操作符创建匿名类型 var person = new{ Name="l", Age=1}:
- new做修饰符,在子类隐藏父类的方法,不常见
- checked,unchecked检查数据类型是否溢出,可以检查一个数据类型或者一段代码;
- delegate做操作符被lambda取代了,匿名函数
- sizeof,只能获取基本数据类型,即结构体类型,除了string,object;也能获取自定义的结构体类型的字节数,但要放在unsafe里;
- ->需要放在unsafe里,类似指针的结构体,间接访问成员;
- is用于判断一个对象是否可以转换为指定的类型,不会抛出异常,返回bool值用来表示是否转换成功;
- as用于执行引用类型的显式类型转换。如果要转换的类型与指定的类型兼容,转换就会成功进行;如果类型不兼容,as运算符就会返回值null
1、Object => 已知引用类型 使用as操作符来完成 2、Object => 已知值类型或引用类型 先使用is操作符来进行判断,再用类型强转方式进行转换 3、已知引用类型之间转换 首先需要相应类型提供转换函数,再用类型强转方式进行转换 4、已知值类型之间转换 最好使用系统提供的Convert类所涉及的静态方法
- null合并运算符??
double? num1 = null; double? num2 = 3.14157;//num2.HasValue,,,num2.Value double num3; num3 = num1 ?? 5.34; Console.WriteLine("num3 的值: {0}", num3); num3 = num2 ?? 5.34; Console.WriteLine("num3 的值: {0}", num3); Console.ReadLine(); num3 的值: 5.34 num3 的值: 3.14157
(4) 类型转换
- 隐式类型转换
- 不丢失精度的转换,比如int转double
- 子类向父类的转换,Human t = teacher h; t只能看到Huaman的方法,看不到teacher独有的;你看到的东西取决于你的身份,而不是前面有什么;
- 装箱
- 显式类型转换
- 有可能丢失精度或有错误,即cast,ushort y = (uint) x;注意符号位
- 拆箱
- Convert.ToString,或者调用实例的方法result.ToString(),或者实例的Parse解析方法,只能解析格式正确的数据;tryParse()
7 字段
(1) 什么是字段
- field是一种表示与对象或类型(类与结构体)关联的变量
- 字段是类型的成员,旧称成员变量
- 与对象相关联的称为实例字段
- 与类型关联的字段称为静态字段,static修饰,表示类型当前状态,比如平均身高等;
(2)字段的声明
- 字段不是语句,语句要写在函数体里;字段的名字一定是名词;
- 字段声明要写在类里,写在方法里就是局部变量了;
(3) 字段的初始值
- 无显示初始化时,字段获得其类型的默认值,所以字段永远都不会未被初始化;
- 实例字段初始化的时机---对象创建时;
- 静态字段初始化的时机---类型被加载时;在被运行环境加载时初始化,静态构造器static Student(){}只执行一次;一般初始化在声明的时候初始化;
(4) 只读字段
readonly修饰的字段只能在构造器里初始化,不能再次赋值;
8 属性
(1) 属性含义
- 属性时一种用于访问对象或者类型的特征的成员,特征反应了状态;
- 属性时字段的自然拓展
- field更前向与实例对象在内存中的布局,property更偏向于反映现实世界对象的特征;
- 对外:暴露数据,数据可以是存储在字段里的,属性也可以是动态计算出来的;
- 对内:保护字段不被非法值修改;
- 属性有get/set方法对进化而来;get/set,set里value是上下文关键字,在特定上下文中是关键字,value在set中代表用户传进来的值,出了set就不是了。
(2) 属性声明
- 完整声明——后台成员变量与访问器,属性修饰符多为public 或 public static;
propfull+tab+tab可以自动写出属性定义,再按tab可修改名称;
prop+tab+tab属性的简略声明public int MyProperty { get; set; }跟字段一样不能保护字段不被非法值修改;
ctrl+R+E放在你打算封装为属性的变量上,可以自动填充剩下代码;
- 简略声明——只有访问器;
- 只读方法,访问器里没有set。只写属性语法正确,不常用,因为属性的主要目的是向外暴露数据而表示对象或类型的状态;
- 动态计算值的属性,属性的名字一定是名词,也分实例属性与静态属性;
(3) 属性和字段关系
- 一般都用于表示对象或类型的状态;
- 属性大多情况下是字段的包装器
- 建议永远用属性暴露数据,字段永远是private或protected;
9 索引器
索引器能使对象能够用与数组相同的方式即用下标进行索引;
声明索引器:Index+tab+tab快速填充代码
class Student { Dictionary<string, int> list = new Dictionary<string, int>(); public int? this[string index] { get { if (this.list.ContainsKey(index)) { return this.list[index]; } else { return null; } } set { if(value.HasValue == false) { throw new Exception("false"); } if (this.list.ContainsKey(index)) { this.list[index] = value.Value; } else { this.list.Add(index, value.Value); } } } }
main>>>>>>>
Student stu = new Student();
stu["math"] = 90;
10 常量
常量是表示常量值(编译时计算的值)的类成员,常量只能时基本数据类型;常量属于类型,实例没有常量;局部常量指的时函数体里加const修饰的;
- 提高程序效率和可读性:常量;
- 为了防止对象的值被改变:只读字段;
- 向外暴露不允许修改的数据:只读属性;
- 当希望成为常量的值其类型不能被常量声明接受是(类或自定义结构体):静态只读字段static readonly
11 参数
(1) 传值参数,参数的默认传递方式
- 值类型:值参数创建变量的副本,参数的初始值是变量的值,对值参数操作不影响变量的值;
- 引用类型并且创建新对象:创建副本,对值参数操作不影响变量的值;
- 引用类型,创建引用变量的副本,只操作对象不创建新对象:对象还是那个对象,但对象里的值已经改变;相当与把对象值的地址传进去了,所以修改值,外面也随之变化;
object.GetHashCode()代表对象的唯一值;某一个变量换名字,变量上按ctrl+。即可调出菜单让程序中其他地方自动修改。
(2) 引用参数,用于修改实际参数值的场景
ref修饰,并不创建新的存储位置,引用形参表示的存储位置是方法调用中作为实参给出的变量所表示的存储位置;
变量在座位引用形参传递前必须先明确赋值;
- 值类型:并不创建变量的副本,可修改实参的值;
- 引用类型,创建新对象:创建新对象后,外部实参指向了新对象的地址,改变了实参的值;
- 引用类型,不创建对象,只改变对象值:与传值参数效果相同,机理不同,传值参数创建了副本,相当于两个地址指向一个实例;ref相当于直接操作实参;
(3) 输出参数,用于除返回值外还需要输出的场景
out修饰,不创建新的存储位置。在作为输出参数传递之前不一定要明确赋值;但是在方法内部,在返回之前,方法的每个输出形参都必须明确赋值;
- 值类型:通过参数向外输出值;
- 引用类型,创建新对象:方法外的变量也引用了新创建的对象
(4) 数组参数,用于简化方法的调用
必须是形参列表中的最后一个,params修饰;
static int Add(params int[] intArray);//声明 Add(1,2,3);//调用,编译器自动声明数组并把值传进去
ref是为了改变值,out是为了输出,
(5) 具名参数,提高可读性
参数位置不受约束
static void Print(string name, int age);//声明 Print(name:"dd", age:1);//调用
(6) 可选参数
参数具有默认值,不推荐使用
(7) 扩展方法:this参数,为目标数据类型追加方法
- 方法必须是公有的静态的,被public static修饰
- 必须是形参列表总的第一个,this修饰
- 必须由一个静态类(一般类名为SomeTypeExtension)来统一收纳对SomeTypeExtension类型的扩展方法;
class Program { static void Main(string[] args) { double x = 3.1415926; double y = x.Round(3);//x就是Round里的第一个参数 } } static class DoubleExtension { public static double Round(this double input, int digits) { double result = Math.Round(input, digits); return result; } }
常用LINQ方法,using System.Linq
List<int> my = new List<int>() { 1, 2, 3, 9 }; my.All(i => i > 10);
12 委托
(1) 什么是委托
- 委托是函数指针的升级版;委托的简单使用:Action, func
- 一切皆地址:变量是以某个地址为起点的一段内存中存储的值;函数是以某个地址为起点的一段内存中所存储的一组机器语言指令;
- 直接调用:通过函数名来调用函数,CPU通过函数名直接获得函数所在地址并执行->返回;
- 间接调用:通过函数指针来调用函数,CPU通过读取函数指针存储的值获得函数所在地址并开始执行->返回;
class Program { static void Main(string[] args) { Calculator calculator = new Calculator(); Action action = new Action(calculator.Report); calculator.Report(); action();//action.Invoke(): Func<int, int, int> func = new Func<int, int, int>(calculator.Add); Func<int, int, int> func1 = new Func<int, int, int>(calculator.Sub); int x = 100; int y = 200; int z = 0; z = func.Invoke(x, y); Console.WriteLine(z); z = func1.Invoke(x, y); Console.WriteLine(z); Type t = typeof(Action); Console.WriteLine(t.IsClass); Console.ReadLine(); } } class Calculator { public void Report() { Console.WriteLine("3 methods"); } public int Add(int a, int b) { int result = a + b; return result; } public int Sub(int a, int b) { int result = a - b; return result; } }
(2) 委托的声明(自定义委托)
- 委托是一种类,类是数据类型,要在命名空间里声明;注意声明委托的位置,防止写出嵌套类型
- 委托所封装的方法必须类型兼容,返回值的数据类型一致,参数列表在个数和数据类型上一致,参数名不需要一样
public delegate int Calc(int x, int y); class Program { static void Main(string[] args) { Calculator calculator = new Calculator(); Calc calc1 = new Calc(calculator.Add); Calc calc2 = new Calc(calculator.Sub); int x = 100; int y = 200; int z = 0; z = calc1.Invoke(x, y); Console.WriteLine(z); z = calc2.Invoke(x, y); Console.WriteLine(z); Console.ReadLine(); } }
(3) 委托把方法当作参数传给另一个方法
- 模板方法:你写了一个方法,通过传进来的委托参数,借用指定的外部方法来产生结果;相当于填空题,常位于代码中部,委托有返回值;
- 回调方法callback:
- 回调指我给了你名片,你需要我就会打电话,不需要就不会。回调关系指某个方法可以调用,也可以不调用,可以动态选择后续被调用的方法(不止我一个人的名片,有很多人给了名片,从里面选需要的);
- 委托类型的参数传进主调方法里去,委托类型的参数封装了回调方法;主调函数会根据自己的逻辑决定是不是调用;主调函数会在主要逻辑执行完了后决定要不要调用,来执行后续工作,所以常位于代码尾部,委托无返回值,相当于流水线;
class Program { static void Main(string[] args) { ProductFactory productfactory = new ProductFactory(); WrapFactory wrapFactory = new WrapFactory(); Func<Product> func1 = new Func<Product>(productfactory.MakePizza); Func<Product> func2 = new Func<Product>(productfactory.MakeCar); Logger logger = new Logger(); Action<Product> log = new Action<Product>(logger.Log); Box box1 = wrapFactory.WrapProduct(func1, log); Console.WriteLine(box1.Product.Name); Box box2 = wrapFactory.WrapProduct(func2, log); Console.WriteLine(box2.Product.Name); Console.ReadLine(); } } class Logger { public void Log(Product product) { Console.WriteLine("product {0} created at {1} Price is {2}", product.Name, DateTime.UtcNow, product.Price); } } class Product { public string Name { get; set; } public double Price { get; set; } } class Box { public Product Product { get; set; } } class WrapFactory { public Box WrapProduct(Func<Product> getProduct, Action<Product> logCallback) { Box box = new Box(); Product product = getProduct.Invoke(); if (product.Price >=50) { logCallback(product); } box.Product = product; return box; } } class ProductFactory { public Product MakePizza() { Product product = new Product(); product.Name = "Pizza"; product.Price = 12; return product; } public Product MakeCar() { Product product = new Product(); product.Name = "Car"; product.Price = 100; return product; } }
(4) 多播委托
多播委托是指一个委托封装多个方法,执行顺序与封装顺序相同。
class Program { static void Main(string[] args) { Student stu1 = new Student(){ID = 1, PenColor= ConsoleColor.Yellow}; Student stu2 = new Student(){ID = 2, PenColor= ConsoleColor.Green}; Student stu3 = new Student() { ID = 3, PenColor = ConsoleColor.Red }; Action action1 = new Action(stu1.DoHomework); Action action2 = new Action(stu2.DoHomework); Action action3 = new Action(stu3.DoHomework); //action1.Invoke(); //action2.Invoke(); //action3.Invoke(); action1 += action2; action1 += action3; action1.Invoke(); Console.ReadLine(); } } class Student { public int ID { get; set; } public ConsoleColor PenColor { get; set; } public void DoHomework() { for (int i = 0; i < 5; i++) { Console.ForegroundColor = this.PenColor; Console.WriteLine("Student {0} do homework {1} hours", this.ID, i); Thread.Sleep(1000); } } }
(5) 隐式异步调用
- 同步:你做完了我在你的基础上接着做;异步:咱俩同时做,相当于同步进行;
- 同步调用时在单线程里进行串行的调用,异步调用使用多线程进行并行调用;
- 直接同步调用:使用方法名;间接同步调用:使用单播或者多播委托的invoke方法;隐式异步调用:使用委托的beginInvoke;显示异步调用:使用Thread或task;
static void Main(string[] args) { Student stu1 = new Student(){ID = 1, PenColor= ConsoleColor.Yellow}; Student stu2 = new Student(){ID = 2, PenColor= ConsoleColor.Green}; Student stu3 = new Student() { ID = 3, PenColor = ConsoleColor.Red }; //stu1.DoHomework(); //stu2.DoHomework(); //stu3.DoHomework();//同步 Action action1 = new Action(stu1.DoHomework); Action action2 = new Action(stu2.DoHomework); Action action3 = new Action(stu3.DoHomework); //action1.BeginInvoke(null, null); //action2.BeginInvoke(null, null); //action3.BeginInvoke(null, null);//隐式异步调用,自动生成分支线程,在分支线程里调用方法; //显示异步调用1 //Thread thread1 = new Thread(new ThreadStart(stu1.DoHomework)); //Thread thread2 = new Thread(new ThreadStart(stu2.DoHomework)); //Thread thread3 = new Thread(new ThreadStart(stu3.DoHomework)); //thread1.Start(); //thread2.Start(); //thread3.Start(); //显示异步调用2 Task task1 = new Task(new Action(stu1.DoHomework)); Task task2 = new Task(new Action(stu2.DoHomework)); Task task3 = new Task(new Action(stu3.DoHomework)); task1.Start(); task2.Start(); task3.Start(); for (int i = 0; i < 10; i++) { Console.ForegroundColor = ConsoleColor.White; Console.WriteLine("main thread {0}", i); Thread.Sleep(1000); } Console.ReadLine(); }
- 可以使用接口取代一些对委托的使用
13 事件
(1) 事件
- event,能够发生的什么事件,能给发生当主语的是事件
- 事件是一种使对象或者类型能够提供通知的成员,即使对象或类具备通知能力的成员;经由事件发送出来的与事件本身相关是数据称为事件参数(可选);根据通知和事件参数来采取行动的行为称为响应事件,即事件处理器。
- 使用:用于对象或类间的动作协调与信息传递(消息推送)
- 事件模型的两个5:
- 发生->响应的5个部分:闹钟响了你起床,隐含订阅关系
- 发生->响应的5个动作:我有一个事件->一个人或一群人关心我的这个事件->我的这个事件发生了->关心时间的人会被依次通知到->被通知到的根据事件参数对事件进行响应;
(2) 事件应用
- 事件模型:事件拥有者(event source,对象, 是object sender), 是拥有者的内部逻辑触发的;事件成员(event,成员);事件响应者(event subscriber);事件处理器(event hanlder)本质是一种回调方法;事件订阅(把事件处理器与事件关联在一起,本质上是一种以委托类型为基础的约定;
- 事件拥有者通过内部逻辑触发事件:
用户按下按钮执行操作,看似是用户的外部操作引起按钮的 Click 事件触发,实际不然,详细情况大致如下:
- 当用户点击图形界面的按钮时,实际是用户的鼠标向计算机硬件发送了一个电信号。Windows 检测到该电信号后,就查看一下鼠标当前在屏幕上的位置。当 Windows 发现鼠标位置处有个按钮,且包含该按钮的窗口处于激活状态,它就通知该按钮,用户按下了,然后按钮的内部逻辑开始执行
- 典型的逻辑是按钮快速地把自己绘制一遍,绘制成自己被按下的样子,然后记录当前的状态为被按下了。紧接着如果用户松开了鼠标,Windows 就把消息传递给按钮,按钮内部逻辑又开始执行,把自己绘制成弹起的状态,记录当前的状态为未被按下
- 按钮内部逻辑检测到,按钮被执行了连续的按下、松开动作,即按钮被点击了。按钮马上使用自己的 Click 事件通知外界,自己被点击了。如果有别的对象订阅了该按钮的 Click 事件,这些事件的订阅者就开始工作
简言之:用户操作通过 Windows 调用了按钮的内部逻辑,最终还是按钮的内部逻辑触发了 Click 事件。
-
一个事件可以挂接多个事件处理器,一个事件处理器也可以被多个事件所挂接。
(3) 事件的声明
- 完整声明(代码注释部分);简略声明(字段式声明,field-like)
class Program { static void Main(string[] args) { Customer customer = new Customer(); Waiter waiter = new Waiter(); customer.Order += new OrderEventHandler(waiter.Action); customer.Action(); customer.PayBill(); Console.ReadLine(); } }
// 注意委托类型的声明和类声明是平级的 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 event OrderEventHandler Order; public double Bill { get; set; } public void PayBill() { Console.WriteLine("p will pay the bill ${0}", this.Bill); } public void WalkerIn() { Console.WriteLine("walker into the room"); } public void SitDown() { Console.WriteLine("Sit down."); } public void Think() { Console.WriteLine("think"); Thread.Sleep(1000); if (this.Order != null)//this.orderEventHandler != null { OrderEventArgs e = new OrderEventArgs(); e.DishName = "chicken"; e.Size = "large"; this.Order.Invoke(this, e);//this.orderEventHandler.Invoke(this, e); } } public void Action() { Console.ReadLine(); this.WalkerIn(); this.SitDown(); this.Think(); } } public class OrderEventArgs:EventArgs//凡是用来传递事件数据的类都从EventArgs派生而来 { public string DishName { get; set; } public string Size { get; set; } } 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 = price * 1.5; break; default: break; } customer.Bill += price; } }
- 事件的本质是委托字段的一个包装器。包装器对委托字段的访问起限制作用;封装的一个重要功能就是隐藏;事件对外界隐藏了委托实例的大部分功能,仅仅对外暴露添加/移除事件功能;
- 用于声明事件的委托类型的命名约定
- 用于声明Foo事件的委托,一般命名为FooEventHandler,除非是一个非常通用的事件约束:厂商定义的EventHandler;
- FooEventHandler的委托参数一般有两个,第一个是object类型,sender,是事件的拥有者,事件的source; 第二个是EventArgs类的派生类,名字一般为FooEventArgs,参数为e, 是事件参数;可以把委托的参数列表看作是事件发生后发送给事件响应者的事件消息;
- 触发Foo事件的方法一般命名为OnFoo,即因何而起事出有因;访问级别为protected;
-
使用 EventHandler
class Program { static void Main(string[] args) { var customer = new Customer(); var waiter = new Waiter(); customer.Order += waiter.Action; customer.Action(); customer.PayTheBill(); } } public class OrderEventArgs : EventArgs { public string DishName { get; set; } public string Size { get; set; } } //public delegate void OrderEventHandler(Customer customer, OrderEventArgs e); public class Customer { // 使用默认的 EventHandler,而不是声明自己的 public event EventHandler Order; public double Bill { get; set; } public void PayTheBill() { Console.WriteLine("I will pay ${0}.", this.Bill); } public void WalkIn() { Console.WriteLine("Walk into the 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(1000); } if (this.Order != null) { var 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) { // 类型转换 var customer = sender as Customer; var orderInfo = e as OrderEventArgs; Console.WriteLine("I will serve you the dish - {0}.", orderInfo.DishName); double price = 10; switch (orderInfo.Size) { case "small": price *= 0.5; break; case "large": price *= 1.5; break; default: break; } customer.Bill += price; } }
- 上面代码Think()思考还触发事件,一个方法两件事情,违反sing-responsibility原则;改为下
public void Think() { Console.WriteLine("think"); Thread.Sleep(1000); this.OnOrder("chicken", "large"); } protected void OnOrder(string dishName, string size) { if (this.Order != null)//this.orderEventHandler != null { OrderEventArgs e = new OrderEventArgs(); e.DishName = dishName; e.Size = size; this.Order.Invoke(this, e); } }
- 事件本身,命名是带有时态的动词或动词短语;
(4) 事件与委托的关系
- 事件不是以特殊方式声明的委托;事件本质是加装在委托字段上的一个蒙版mask,是起掩蔽作用的包装器,这个阻挡非法操作的蒙版绝不是委托字段本身;
- 为何用委托:source角度是为了表明source能对外传递哪些消息;subscriber角度看,它是一种约定,为了约束能使用什么样签名的方法来处理事件;委托实例存储事件处理器;
14 类
(1) 类的声明
- 一个项目就是一个程序集。一个程序集可以体现为一个dll文件,或者exe文件。 internal限定的是只有在同一程序集中可访问, public可以跨程序集;
- private限制访问在定义的类体里,是类内成员的默认访问级别,保证数据的安全性,可继承不可访问;protected限制访问在继承链上,多用在方法上(纵向扩展);internal protected放在一起用是或的关系;internal是类的默认访问级别。
- 是一个is a: 一个子类的实例在语义上讲也是父类的实例,一个派生类的实例在语义上讲也是基类的实例;Car is Vehicle;可以用父类的引用变量引用子类的实例;但父类的实例并不一定是子类的实例。
- sealed封闭类,不能当基类了;一个类最多有一个基类(继承、派生),可实现多个基接口(实现);子类的访问级别不能超过父类的范围;
F12:跳至类定义
Ctrl + - (减号):跳回至之前所在位置
(2) 类的继承
- 派生类是在基类已有的成员基础上,对基类进行的横向和纵向的拓展。
- 横向扩展:对类成员个数的扩充
- 纵向扩展:对类成员(重写)版本的更新
- base引用基类对象,只能访问上一级父类;this指用这个类构建的对象它自身
- 实例构造器是不能被继承的。
class Vehicle { public Vehicle(string owner) { this.Owner = owner; } public string Owner { get; set; } } class Car:Vehicle { public Car(string owner) : base(owner) { this.Owner = owner;//可省略 } }
~~~~~~~~~~~~~~~~
Car car = new Car("o");//先触发基类构造器,再触发触发子类构造器,所以要保证基类构造器能成功被调用
- 重写override ,父类须加virtual,就是子类更新父类的成员版本,子类里只存在一个最新版本;被标记为 override 的成员,隐含也是 virtual 的,可以继续被重写。重写的发生条件:函数成员、可见(public, protected,父类成员是对子类可见的)、签名一致(函数名和参数,不包括返回值;方法签名:方法名称 + 类型形参的个数 + 每个形参(从左往右)的类型和种类(值、引用或输出)。
public class Vehicle { public virtual void Run() { Console.WriteLine("Running"); } } public class Car:Vehicle { public override void Run() { Console.WriteLine("Car is Running"); } }
- 隐藏:如果子类和父类中函数成员签名相同,但又没标记 virtual 和 override,称为 hide 隐藏。这会导致 Car 类里面有两个 Run 方法,一个是从 Vehicle 继承的 base.Run(),一个是自己声明的 this.Run()。原则上是不推荐用 Hide 。
Vehicle v = new Car(); v.Run(); //重写时Run调用的是Car里的,因为具体调用哪个版本的逻辑是跟实例相关的;hide时Run调用的是Vehicle里的,
//可以理解为 v 作为 Vehicle 类型,它本来应该顺着继承链往下(一直到 Car)找 Run 的具体实现,但由于 Car 没有 Override,所以它找不下去,只能调用 Vehicle 里面的 Run。
(3) 多态:基于重写机制,函数成员的具体行为由对象决定,具体调用哪个版本的逻辑是跟实例相关的,调用最新的版本
(父类引用子类的实例)C#语言的变量和对象都是有类型的,所以会有代差;
15 抽象类与开闭原则
(1) 接口和抽象类
- 抽象类指的是函数成员没有被完全实现的类,类里有至少一个没有被实现的类;abstract修饰,不能是private;
-
因为抽象类内部还有未实现的函数成员,计算机不知道怎么调用这类成员,于是编译器干脆不允许你实例化抽象类。一个类不允许实例化,它就只剩两个用处了:
-
作为基类,在派生类里面实现基类中的 abstract 成员
-
声明基类(抽象类)类型变量去引用子类(已实现基类中的 abstract 成员)类型的实例,这又称为多态
-
- 开闭原则,稳定的成员封装,有可能改变的声明为抽象成员,让子类去实现;
- 如果不是为了修复 bug 和添加新功能,别总去修改类的代码,特别是类当中函数成员的代码。
- 我们应该封装那些不变的、稳定的、固定的和确定的成员,而把那些不确定的,有可能改变的成员声明为抽象成员,并且留给子类去实现。
- 开放修复 bug 和添加新功能,关闭对类的更改。
abstract class Vehicle { public void Stop() { Console.WriteLine("Stopped"); } public abstract void Run(); } class Car:Vehicle { public override void Run() { Console.WriteLine("Car is Running"); } }
(2) 纯抽象类版(接口)
一个抽象类里面的所有方法都是抽象方法。VehicleBase 是纯虚类,它将成员的实现向下推,推到 Vehicle。Vehicle 实现了 Stop 和 Fill 后将 Run 的实现继续向下推。
abstract class VehicleBase { public abstract void Stop(); public abstract void Fill(); public abstract void Run(); } // 抽象 abstract class Vehicle:VehicleBase { public override void Stop() { Console.WriteLine("Stopped!"); } public override void Fill() { Console.WriteLine("Pay and fill..."); } } // 具体 class Car : Vehicle { public override void Run() { Console.WriteLine("Car is running..."); } }
在 C++ 中能看到这种纯虚类的写法,但在 C# 和 Java 中,纯虚类其实就是接口。
- 因为 interface 要求其内部所有成员都是 public 的,所以就把 public 去掉了
- 接口本身就包含了“是纯抽象类”的含义(所有成员一定是抽象的),所以 abstract 也去掉了
- 因为 abstract 关键字去掉了,所以实现过程中的 override 关键字也去掉了;接口在 C# 中的命名约定以 I 开头
interface IVehicle { void Stop(); void Fill(); void Run(); } abstract class Vehicle : IVehicle { public void Stop() { Console.WriteLine("Stopped!"); } public void Fill() { Console.WriteLine("Pay and fill..."); } // Run 暂未实现,所以依然是 abstract 的 public abstract void Run(); } class Car : Vehicle { public override void Run() { Console.WriteLine("Car is running..."); } }
-
什么是接口和抽象类
-
接口和抽象类都是“软件工程产物”
-
具体类 -> 抽象类 -> 接口:越来越抽象,内部实现的东西越来越少
-
抽象类是未完全实现逻辑的类(可以有字段和非 public 成员,它们代表了“具体逻辑”)
-
抽象类为复用而生:专门作为基类来使用。也具有解耦功能
-
封装确定的,开放不确定的(开闭原则),推迟到合适的子类中去实观
-
接口是完全未实现逻辑的“类”(“纯虚类”;只有函数成员;成员全部 public)
-
接口为解耦而生:“高内聚,低耦合”,方便单元测试
-
接口是一个“协约”。早已为工业生产所熟知(有分工必有协作,有协作必有协约)
-
它们都不能实例化。只能用来声明变量、引用具体类(concrete class)的实例
对于一个方法来说,方法体就是它的实现;对于数据成员,如字段,它就是对类存储数据的实现。
-
alt+鼠标左键:选中多行同时编辑;Ctrl+E,F ----格式化选中的代码 ;Ctrl+E,D ----格式化全部代码
16 接口
(1) 接口
- 接口是服务消费者和服务提供者之间的契约。既然是契约,那就必须是透明的,对双方都是可见的,public。
- 依赖的同时就出现了耦合。依赖越直接,耦合就越紧。紧耦合的问题:
-
基础类一旦出问题,上层类写得再好也没辙
-
程序调试时很难定位问题源头
-
基础类修改时,会影响写上层类的其他程序员的工作
-
- 程序开发中要尽量避免紧耦合,解决方法就是接口。接口:
-
约束调用者只能调用接口中包含的方法
-
让调用者放心去调,不必关心方法怎么实现的、谁提供的
- 松耦合最大的好处就是让功能的提供方变得可替换,从而降低紧耦合时“功能的提供方不可替换”带来的高风险和高成本。
以老式手机举例,对用户来说他只关心手机可以接(打)电话。对于手机厂商,接口约束了他只要造的是手机,就必须可靠实现上面的四个功能。用户如果丢了个手机,他只要再买个手机,不必关心是那个牌子的,肯定也包含这四个功能,上手就可以用。用术语来说就是“人和手机是解耦的”。
static void Main(string[] args) { User user = new User(new HonorPhone()); user.Use(); Console.ReadLine(); } interface Iphone { void Dail(); void PickUp(); } class User { private Iphone _phone; public User(Iphone phone) { _phone = phone; } public void Use() { _phone.Dail(); _phone.PickUp(); } } class HuaweiPhone : Iphone { public void Dail() { Console.WriteLine("Huawei is dailing"); } public void PickUp() { Console.WriteLine("Huawei, who are you?"); } } class HonorPhone : Iphone { public void Dail() { Console.WriteLine("Honor is dailing"); } public void PickUp() { Console.WriteLine("Honor, who are you?"); } }
(2)单元测试
(3)接口隔离原则:客户端不应该依赖它不需要的接口;类间的依赖关系应该建立在最小的接口上。
- 胖接口:
https://www.yuque.com/yuejiangliu/dotnet/timothy-csharp-029